} [options.data]
+ * @param {string} [options.target]
+ * @param {function} [options.success]
+ * @param {function} [options.error]
+ * @param {function} [options.complete]
+ * @param {string} [selector]
+ * @returns {void}
+ */
+ function loadPage(options, selector)
+ {
+ if(typeof options === 'string') options = {url: options};
+ options = options || {};
+
+ if(is18version && !options.partial) return $.apps.openUrl(options.url, options);
+
+ if ($.apps.isOldPage(options.url)) return loadOldPage(options.url);
+ else hideOldPage();
+
+ if(typeof selector === 'string') options.selector = selector;
+ else if(typeof selector === 'object') options = $.extend({}, options, selector);
+ if (!options.selector && options.url && options.url.includes(' '))
+ {
+ const parts = url.split(' ', 2);
+ options.url = parts[0];
+ options.selector = parts[1];
+ }
+
+ options = $.extend({url: currentAppUrl, id: options.selector || options.target || 'page'}, options);
+ if(!options.selector)
+ {
+ if($('#main').length)
+ {
+ options.selector = '#main>*,pageCSS/.zin-page-css>*,pageJS/.zin-page-js,#configJS,title>*,#heading>*,#navbar>*';
+ }
+ else
+ {
+ options.selector = 'body>*,title>*,#configJS';
+ }
+ $.share = {};
+ }
+
+ if(DEBUG) console.log('[APP] ', 'load:', options.url);
+ fetchContent(options.url, options.selector, options);
+ }
+
+ function loadPartial(url, selector, options)
+ {
+ loadPage($.extend({partial: true, url: url, selector: selector}, options));
+ }
+
+ /** Load zui component. */
+ function loadComponent(target, options)
+ {
+ if(target[0] !== '#' && target[0] !== '.') target = `#${target}`;
+ options = $.extend({url: currentAppUrl, id: target, target: target}, options);
+ if(!$(target).length) return loadPage({url: options.url, id: target});
+
+ if(!options.selector)
+ {
+ let name = options.component;
+ if(!name)
+ {
+ const component = $(target).zui();
+ if(!component) return;
+ name = component.constructor.ZUI;
+ }
+ options.selector = `${name}/${target}:component`;
+ }
+ loadPage(options);
+ }
+
+ /**
+ * Load dtable content.
+ *
+ * @param {string} [url]
+ * @param {string} [target]
+ * @param {Object} [options]
+ * @returns
+ */
+ function loadTable(url, target, options)
+ {
+ if(!target)
+ {
+ const urlInfo = $.parseLink(url);
+ target = urlInfo.moduleName ? `table-${urlInfo.moduleName}-${urlInfo.methodName}` : ($('.dtable').attr('id') || 'dtable');
+ }
+ if(target[0] !== '#' && target[0] !== '.') target = `#${target}`;
+ return loadComponent(target, $.extend({component: 'dtable', url: url, selector: `dtable/${target}:component,#featureBar>*`}, options));
+ }
+
+ /**
+ * Load modal content.
+ *
+ * @param {string} [url]
+ * @param {string} [target]
+ * @param {Object} [options]
+ * @returns
+ */
+ function loadModal(url, target, options)
+ {
+ options = $.extend({url}, options);
+ if(!target) return zui.Modal.open(options);
+ else if(target === 'current') target = zui.Modal.query().id;
+
+ if(target[0] !== '#' && target[0] !== '.') target = `#${target}`;
+ const modal = zui.Modal.query(target);
+ if(!modal) return;
+ modal.render(options);
+ }
+
+ function loadTarget(url, target, options)
+ {
+ options = $.extend({}, options);
+ if(typeof target === 'string') options.target = target;
+ else if(typeof target === 'object') options = $.extend(options, target);
+ if(typeof url === 'string') options.url = url;
+ else if(typeof url === 'object') options = $.extend(options, url);
+ if(!options.target) return loadPage(options);
+
+ let remoteData;
+ let loadError;
+ const ajaxOptions =
+ {
+ url: url,
+ header: options.header,
+ type: options.method || 'GET',
+ data: options.data,
+ contentType: options.contentType,
+ beforeSend: () =>
+ {
+ toggleLoading(options.target, true);
+ if(options.beforeSend) return options.beforeSend();
+ },
+ success: (data) =>
+ {
+ remoteData = data;
+ if(options.beforeUpdate)
+ {
+ const result = options.beforeUpdate(data, options);
+ if(result === false) return;
+ if(typeof result === 'string') data = result;
+ }
+ let target = options.target;
+ if(target[0] !== '#' && target[0] !== '.') target = `#${target}`;
+ const $target = $(target);
+ if($target.length)
+ {
+ if(options.success) options.success(data, options);
+ let $content = $(data);
+ if(options.selector) $content = $('').append($content).find(options.selector);
+ if(options.replace) $target.replaceWith($content);
+ else $target.empty().append($content);
+ }
+ else
+ {
+ loadError = new Error(`ZIN: Target "${target}" not found.`);
+ if(options.error) options.error(data, loadError);
+ }
+ },
+ error: (xhr, type, error) =>
+ {
+ loadError = error;
+ if(options.error) options.error(error, options);
+ if(DEBUG) console.error('[ZIN] ', 'Fetch data failed from ' + url, {xhr, type, error});
+ zui.Messager.show('ZIN: Fetch data failed from ' + url);
+ },
+ complete: () =>
+ {
+ toggleLoading(target, false);
+ if(options.complete) options.complete(remoteData, loadError, options);
+ }
+ };
+ return $.ajax(ajaxOptions);
+ }
+
+ function postAndLoadPage(url, data, selector, options)
+ {
+ if(typeof selector === 'object')
+ {
+ options = selector;
+ selector = null;
+ }
+ options = $.extend({url: url, selector: selector, method: 'POST', data, contentType: false}, options);
+ if(options.app) openPage(url, options.app, options);
+ else loadPage(options);
+ }
+
+ function loadCurrentPage(options)
+ {
+ if(typeof options === 'string') options = {selector: options};
+ return loadPage($.extend({url: currentAppUrl}, options));
+ }
+
+ function reloadPage()
+ {
+ loadPage({url: currentAppUrl, selector: 'body>*,title>*,#configJS'});
+ }
+
+ function openPage(url, appCode, options)
+ {
+ if(DEBUG) console.log('[APP] ', 'open:', url, appCode);
+ if(!window.config.zin)
+ {
+ location.href = $.createLink('index', 'app', 'url=' + btoa(url));
+ return;
+ }
+ $.apps.openApp(url, $.extend({code: appCode, forceReload: true}, options));
+ }
+
+ /**
+ * Search history and go back to specified path.
+ *
+ * @param {string} target Back target, can be app name or module-method path.
+ * @param {string} url Fallback url.
+ * @returns {void}
+ */
+ function goBack(target, url)
+ {
+ if(!target || target === 'APP' || target === true) target = currentCode;
+ else if(target === 'GLOBAL') target = '';
+
+ if(target && target.includes('-'))
+ {
+ const parts = target.split('-');
+ return loadPage($.createLink(parts[0], parts[1]));
+ }
+ if(url) return loadPage(url);
+
+ window.history.back();
+ }
+
+ /**
+ * Open url in app.
+ * @param {string} url
+ * @param {Object} options
+ * @param {string} options.url
+ * @param {string} options.app
+ * @param {string} options.load
+ * @param {string} options.back
+ * @param {Event} event
+ */
+ function openUrl(url, options, event)
+ {
+ if(typeof url === 'object')
+ {
+ options = url;
+ url = options.url;
+ }
+ else
+ {
+ options = options || {};
+ options.url = url;
+ }
+
+ if(is18version) return $.apps.open(options.url, options.app); // 18+zin.
+
+ if(DEBUG) console.log('[APP] open url', url, options);
+
+ if(options.confirm)
+ {
+ return zui.Modal.confirm(options.confirm).then(confirmed =>
+ {
+ if(!confirmed) return;
+ openUrl(url, $.extend({}, options, {confirm: false}), event);
+ });
+ }
+
+ const load = options.load;
+ if(typeof load === 'string' || load)
+ {
+ if(!options.target) options.target = options.loadId;
+ if(url) options.url = url;
+ if(load)
+ {
+ if(load === 'table')
+ {
+ if(!options.target && event) options.target = $(event.target).closest('.dtable').attr('id');
+ return loadTable(options.url, options.target, options);
+ }
+ if(load === 'modal')
+ {
+ if(!options.target && event) options.target = $(event.target).closest('.modal').attr('id');
+ return loadModal(options.url, options.target, options);
+ }
+ if(load === 'target') return loadTarget(options);
+ if(load !== 'APP' && typeof load === 'string') options.selector = load;
+ delete options.load;
+ }
+ return loadPage(options);
+ }
+
+ if(typeof options.back === 'string') return goBack(options.back, url);
+
+ openPage(url, options.app);
+ }
+
+ /**
+ * Parse wg selector.
+ * @param {string} selector
+ * @return {object|null}
+ */
+ function parseSelector(selector)
+ {
+ selector = selector.trim();
+ let len = selector.length;
+
+ if(len < 1) return null;
+
+ const result = {class: [], id: '', tag: '', inner: false, name: '', first: false, selector: selector};
+ if(selector.includes('/'))
+ {
+ const parts = selector.split('/', 2);
+ result.name = parts[0];
+ selector = parts[1];
+ len = selector.length;
+ }
+ selector = selector.replace('> *', '>*');
+ if(selector.endsWith('>*'))
+ {
+ result.inner = true;
+ selector = selector.substring(0, selector.length - 2);
+ len = selector.length;
+ }
+
+ let type = 'tag';
+ let current = '';
+ let updateResult = function(result, current, type)
+ {
+ if(!current.length) return;
+
+ if(type === 'class')
+ {
+ result[type].push(current);
+ }
+ else if(type === 'option')
+ {
+ current.split('&').forEach(function(option)
+ {
+ const parts = option.split('=');
+ result[parts[0]] = parts[1] || true;
+ });
+ }
+ else
+ {
+ result[type] = current;
+ }
+ };
+
+ for(let i = 0; i < len; i++)
+ {
+ let c = selector[i];
+ let t = '';
+
+ if(c === '#' && type !== 'option')
+ {
+ t = 'id';
+ }
+ else if(c === '.' && type !== 'option')
+ {
+ t = 'class';
+ }
+ else if(c === '(' && type !== 'option' && selector.endsWith(')'))
+ {
+ let command = selector.substring(i + 1, selector.length - 1);
+ if(!command) command = current;
+ result.command = command;
+ break;
+ }
+ else if(c === ':')
+ {
+ t = 'option';
+ }
+
+ if(!t)
+ {
+ current += c;
+ }
+ else
+ {
+ updateResult(result, current, type);
+ current = '';
+ type = t;
+ }
+ }
+ updateResult(result, current, type);
+
+ if(!result.name.length)
+ {
+ if(result.id.length) result.name = result.id;
+ else if(result.tag) result.name = result.tag;
+ else result.name = selector;
+ }
+ result.select = [result.tag, result.id.length ? '#' + result.id : '', result.class.length ? '.' + result.class.join('.') : ''].join('');
+
+ return result;
+ }
+
+ function handleClickLinkIn18(e) // 18+zin.
+ {
+ var $link = $(e.target).closest('a,.open-url');
+ if(!$link.length) return;
+
+ if(!$link.length || $link.hasClass('ajax-submit') || $link.hasClass('not-open-url') || ($link.attr('target') || '')[0] === '_') return;
+
+ const options = $link.dataset();
+ const url = options.url || $link.attr('href');
+ if(options.toggle)
+ {
+ if(options.toggle === 'iframeModal')
+ {
+ var modalTrigger = $link.data('zui.modaltrigger');
+ if(!modalTrigger)
+ {
+ delete options.toggle;
+ if(url) options.url = url;
+ options.type = 'iframe';
+ modalTrigger = new window.parent.$.zui.ModalTrigger(options);
+ $link.data('zui.modaltrigger', modalTrigger);
+ }
+ modalTrigger.show();
+ }
+ e.preventDefault();
+ return;
+ }
+
+ if(typeof options.back === 'string') return goBack(options.back, url);
+
+ var appCode = options.app;
+ if(url)
+ {
+ if(appCode === 'help') return $.apps.openUrl(url, {app: appCode});
+ if(url.indexOf('javascript:') === 0 || url[0] === '#') return;
+ var urlInfo = $.parseLink(url);
+ if(urlInfo.external || (urlInfo.moduleName === 'file' && urlInfo.methodName === 'download')) return;
+ if(urlInfo.moduleName === 'index' && urlInfo.methodName === 'index')
+ {
+ window.location.reload();
+ e.preventDefault();
+ return;
+ }
+ }
+ else
+ {
+ if(!appCode) return;
+ }
+ if(!appCode) appCode = $.apps.getAppCode(url);
+ if(!appCode) return;
+
+ $.apps.openUrl(url, {app: appCode});
+ e.preventDefault();
+ }
+
+ function handleClickIn18(event) // 18+zin.
+ {
+ handleClickLinkIn18(event);
+
+ var iframe = parent.document.getElementById(window.name);
+ if(!iframe) return;
+ var iframeContainer = parent.document.getElementById(iframe.name) || iframe;
+ if(!iframeContainer) return;
+
+ var customEvent;
+ if(typeof Event === 'function') {
+ customEvent = new Event(event.type, {bubbles: true});
+ } else {
+ customEvent = document.createEvent('Event');
+ customEvent.initEvent(event.type, true, true);
+ }
+ iframeContainer.dispatchEvent(customEvent);
+ }
+
+ /**
+ * Ajax send score to server.
+ * @param {string} method
+ */
+ function ajaxSendScore(method)
+ {
+ $.get($.createLink('score', 'ajax', 'method=' + method));
+ }
+
+ /**
+ * Change current app language.
+ * @param {string} lang
+ */
+ function changeAppLang(lang)
+ {
+ if($('html').attr('lang') === lang) return;
+ reloadPage();
+ $('html').attr('lang', lang);
+ }
+
+ /**
+ * Change current app theme.
+ * @param {string} lang
+ */
+ function changeAppTheme(theme)
+ {
+ const classList = $('html').attr('class').split(' ').filter(x => x.length && !x.startsWith('theme-'));
+ classList.push('theme-' + theme);
+ $('html').attr('class', classList.join(' '));
+ const $theme = $('#zuiTheme');
+ const oldPath = $theme.attr('href').split('/');
+ oldPath.pop();
+ oldPath.push(theme + '.css');
+ $theme.attr('href', oldPath.join('/'));
+ const userMenu = $('#userMenu-toggle').zui();
+ if(!userMenu) return;
+ const themeItem = userMenu.options.menu.items.find(x => x.key === 'theme');
+ themeItem.items.forEach(item => {item.active = item['data-value'] === theme;});
+ userMenu.render({menu: userMenu.options.menu});
+ }
+
+ /**
+ * Select UI language.
+ * @param {string} lang
+ */
+ function selectLang(lang)
+ {
+ parent.selectLang(lang);
+ }
+
+ /**
+ * Select UI theme.
+ * @param {string} theme
+ */
+ function selectTheme(theme)
+ {
+ parent.selectTheme(theme);
+ }
+
+ $.extend(window, {registerRender: registerRender, fetchContent: fetchContent, loadTable: loadTable, loadPage: loadPage, postAndLoadPage: postAndLoadPage, loadCurrentPage: loadCurrentPage, parseSelector: parseSelector, toggleLoading: toggleLoading, openUrl: openUrl, goBack: goBack, registerTimer: registerTimer, loadModal: loadModal, loadTarget: loadTarget, loadComponent: loadComponent, loadPartial: loadPartial, reloadPage: reloadPage, selectLang: selectLang, selectTheme: selectTheme, changeAppLang, changeAppTheme: changeAppTheme});
+ $.extend($.apps, {openUrl: openUrl});
+ $.extend($, {ajaxSendScore: ajaxSendScore, selectLang: selectLang});
+
+ /* Transfer click event to parent */
+ $(document).on('click', (e) =>
+ {
+ if(is18version) return handleClickIn18(e); // 18+zin.
+
+ if(isInAppTab) window.parent.$('body').trigger('click');
+
+ const $link = $(e.target).closest('a,.open-url');
+ if(!$link.length || $link.hasClass('ajax-submit') || $link.hasClass('not-open-url') || ($link.attr('target') || '')[0] === '_') return;
+
+ const options = $link.dataset();
+ if(options.toggle) return;
+
+ const url = options.url || $link.attr('href');
+ const $modal = $link.closest('.modal');
+ if(options.loadId)
+ {
+ options.target = options.loadId;
+ delete options.loadId;
+ }
+ if($modal.length)
+ {
+ if(!options.load)
+ {
+ if(!url) return;
+ options.load = 'modal';
+ }
+ if(options.load === 'modal' && !options.target) options.target = $modal.attr('id');
+ if(options.load === 'table')
+ {
+ options.partial = true;
+ if(!options.url) options.url = $modal.data('zui.Modal').options.url;
+ }
+ }
+ else
+ {
+ if(options.load === 'modal' && !options.target) delete options.load;
+ }
+ if(url && (/^(https?|javascript):/.test(url) || url.startsWith('#'))) return;
+ if(!url && $link.is('a') && !options.back && !options.load) return;
+
+ openUrl(url, options, e);
+ e.preventDefault();
+ }).on('locate.zt', (_e, data) =>
+ {
+ if(!data) return;
+ if(data === true) return loadCurrentPage();
+ if(typeof data === 'string')
+ {
+ if(data === 'table') return loadTable();
+ if(data === 'modal') return loadModal();
+ data = {url: data};
+ }
+
+ if(data.confirm)
+ {
+ return zui.Modal.confirm(data.confirm).then(confirmed =>
+ {
+ if(confirmed) $(document).trigger('locate.zt', data.confirmed);
+ else $(document).trigger('locate.zt', data.cancelled);
+ });
+ }
+ if(data.load) return openUrl(data);
+ if(data.app) return openPage(data.url + (data.selector ? (' ' + data.selector) : ''), data.app);
+ loadPage(data.url, data.selector);
+ });
+
+ if(!isInAppTab)
+ {
+ $(window).on('popstate', function(event)
+ {
+ const state = event.state;
+ if(DEBUG) console.log('[APP]', 'popstate:', state);
+ openPage(state.url);
+ });
+ }
+
+ $(() =>
+ {
+ initZinbar();
+
+ if(window.defaultAppUrl) loadPage(window.defaultAppUrl);
+ if(isInAppTab)
+ {
+ const frameElement = window.frameElement;
+ if(frameElement && parent.window.$) parent.window.$(frameElement).trigger('ready.app');
+ }
+
+ if(is18version) // 18+zin.
+ {
+ if(window.afterPageUpdate) window.afterPageUpdate($('body'));
+
+ if(isInAppTab && parent.$.apps)
+ {
+ var windowName = window.name;
+ if(windowName && windowName.startsWith('app-'))
+ {
+ var appCode = windowName.substring(4);
+ var currentApp = parent.$.apps.openedApps[appCode];
+ if(currentApp) currentApp.$app.removeClass('loading');
+ }
+ }
+ }
+
+ if(DEBUG)
+ {
+ if(window.zinDebug)
+ {
+ let requestBegin = startTime;
+ if(performance.timing) requestBegin -= ((performance.timing.loadEventStart || Date.now()) - (performance.timing.navigationStart || Date.now()));
+ else if(window.zinDebug.trace && window.zinDebug.trace.request) requestBegin -= window.zinDebug.trace.request.timeUsed;
+ updatePerfInfo({id: 'page'}, 'renderEnd', {id: 'page', perf: {requestBegin: Math.max(0, requestBegin), requestEnd: startTime, renderBegin: startTime}});
+ showZinDebugInfo(window.zinDebug, {id: 'page'});
+ }
+ if(!isInAppTab && !zui.store.get('Zinbar:hidden')) loadCurrentPage();
+ }
+ });
+}());