zin.js 79 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040
  1. (function()
  2. {
  3. if(config.skipRedirect || window.skipRedirect) return;
  4. const parent = window.parent;
  5. const currentModule = config.currentModule;
  6. const currentMethod = config.currentMethod;
  7. const isIndexPage = currentModule === 'index' && currentMethod === 'index';
  8. const moduleMethod = `${currentModule}-${currentMethod}`;
  9. const selfOpenList = new Set('index|tutorial|install|upgrade|sso|cron|misc|user-login|user-deny|user-logout|user-reset|user-forgetpassword|user-resetpassword|my-changepassword|my-preference|file-read|file-download|file-preview|file-uploadimages|file-ajaxwopifiles|report-annualdata|misc-captcha|execution-printkanban|traincourse-ajaxuploadlargefile|traincourse-playvideo|screen-view|zanode-create|screen-ajaxgetchart|instance-terminal|ai-chat|instance-logs'.split('|'));
  10. const iframeList = new Set(['cron-index', 'zanode-create']);
  11. const isAllowSelfOpen = !iframeList.has(moduleMethod) &&
  12. (isIndexPage
  13. || location.hash === '#_single'
  14. || /(\?|\&)_single/.test(location.search)
  15. || currentMethod.startsWith('ajax')
  16. || selfOpenList.has(moduleMethod)
  17. || selfOpenList.has(currentModule)
  18. || $('body').hasClass('allow-self-open'));
  19. if(parent === window && !isAllowSelfOpen)
  20. {
  21. const shortUrl = location.pathname + location.search + location.hash;
  22. location.href = $.createLink('index', 'index', `open=${btoa(shortUrl)}`);
  23. return;
  24. }
  25. }());
  26. (function()
  27. {
  28. let config = window.config;
  29. const isIndexPage = config.currentModule === 'index' && config.currentMethod === 'index';
  30. const DEBUG = config.debug;
  31. const currentCode = (window.frameElement ? window.frameElement.name : window.name).split('-')[1];
  32. const isInAppTab = parent.window !== window;
  33. const fetchTasks = new Map();
  34. const ridSet = new Set();
  35. const timers = {timeout: [], interval: []};
  36. let currentAppUrl = isInAppTab ? '' : location.href;
  37. let zinbar = null;
  38. let historyState = parent.window.history.state;
  39. const hasZinBar = DEBUG && window.zin && window.zin.zinTool && !isIndexPage;
  40. const localCacheFirst = config.clientCache === 'local-first';
  41. const isTutorial = top.config.currentModule === 'tutorial';
  42. let openedOldPage = false;
  43. let oldPageCofnig = null;
  44. let workspaceType = '';
  45. const zinCallbacks = {
  46. onSelectLang: null,
  47. onSelectVision: null,
  48. onChangeApp: null
  49. };
  50. /**
  51. * 注册Zin回调函数。
  52. * Register a Zin callback function.
  53. *
  54. * @param {string} name - 回调函数名称。
  55. * @param {function} callback - 回调函数。
  56. */
  57. function registerZinCallback(name, callback)
  58. {
  59. if(zinCallbacks[name] === undefined)
  60. {
  61. console.warn('[ZIN] registerZinCallback unknown callback name:', name);
  62. return;
  63. }
  64. if(typeof callback === 'function')
  65. {
  66. zinCallbacks[name] = callback;
  67. }
  68. else
  69. {
  70. console.warn('[ZIN] registerZinCallback expects a function for', name);
  71. }
  72. }
  73. function getPageInfo()
  74. {
  75. let pageConfig = openedOldPage ? oldPageCofnig : window.config;
  76. if(openedOldPage && !pageConfig)
  77. {
  78. const oldPageLink = $.parseLink(openedOldPage);
  79. pageConfig = {currentModule: oldPageLink.moduleName, currentMethod: oldPageLink.methodName};
  80. }
  81. return {
  82. app : currentCode,
  83. id : `${currentCode}.${pageConfig.currentModule}-${pageConfig.currentMethod}`,
  84. path : `${pageConfig.currentModule}-${pageConfig.currentMethod}`,
  85. url : currentAppUrl,
  86. config : pageConfig,
  87. currentModule: pageConfig.currentModule,
  88. currentMethod: pageConfig.currentMethod,
  89. };
  90. }
  91. function getUrlID(url)
  92. {
  93. const info = $.parseLink(url || currentAppUrl);
  94. return info.moduleName ? `${info.moduleName}-${info.methodName}` : '';
  95. }
  96. function isDiffPage(newUrl, oldUrl)
  97. {
  98. return getUrlID(newUrl) !== getUrlID(oldUrl || currentAppUrl);
  99. }
  100. function showLog(name, moreTitles, trace, moreInfos)
  101. {
  102. const titles = [`%c APP • ${currentCode} `];
  103. const titleColors = ['color:#fff;font-weight:bold;background:#2196f3'];
  104. const urlID = getUrlID();
  105. if(urlID)
  106. {
  107. titles.push(`%c ${urlID} `);
  108. titleColors.push('background:rgba(33, 150, 243, 0.2);color:#2196f3;');
  109. }
  110. if(name)
  111. {
  112. titles.push(`%c ${name} `);
  113. titleColors.push('color:#2196f3;font-weight:bold;');
  114. }
  115. if(!Array.isArray(moreTitles)) moreTitles = [moreTitles];
  116. if(typeof moreTitles[0] === 'string' && (moreTitles[0].startsWith('success:') || moreTitles[0].startsWith('error:')))
  117. {
  118. const message = moreTitles.shift();
  119. const [type, content] = message.split(':', 2);
  120. titles.push(`%c ${content} `);
  121. titleColors.push(`color:${type === 'error' ? '#f56c6c' : '#67c23a'};`);
  122. }
  123. if(trace || moreInfos)
  124. {
  125. console.groupCollapsed(titles.join(''), ...titleColors, ...moreTitles);
  126. if(trace) console.trace(trace);
  127. if(moreInfos)
  128. {
  129. if($.isPlainObject(moreInfos)) Object.keys(moreInfos).forEach((infoName) => console.log(infoName, moreInfos[infoName]));
  130. else console.log(moreInfos);
  131. }
  132. console.groupEnd();
  133. }
  134. else
  135. {
  136. console.log(titles.join(''), ...titleColors, ...moreTitles);
  137. }
  138. }
  139. $.apps = $.extend(
  140. {
  141. currentCode: currentCode,
  142. updateApp: function(code, url, title)
  143. {
  144. const state = typeof code === 'object' ? code : {url: url, title: title};
  145. const oldState = window.history.state;
  146. if(title) document.title = title;
  147. if(oldState && oldState.url === url)
  148. {
  149. if(DEBUG) showLog('Update skipped', [code, title], {oldState, state, code, url, title});
  150. return;
  151. }
  152. window.history.pushState(state, title, url);
  153. if(DEBUG) showLog('Update', [code, title], {state, code, url, title});
  154. return state;
  155. },
  156. updateAppUrl: function(url, title)
  157. {
  158. currentAppUrl = url;
  159. return $.apps.updateApp(currentCode, url, title);
  160. },
  161. isOldPage: () => false,
  162. reloadApp: function(_code, url){loadPage(url);},
  163. openApp: function(url, options){loadPage(url, options);},
  164. goBack: function(){history.go(-1);},
  165. changeAppsLang: changeAppLang,
  166. changeAppsTheme: changeAppTheme,
  167. updateUserToolbar: function(){loadPage({selector: '#toolbar', partial: true, target: '#toolbar'})},
  168. triggerEvent: triggerEvent,
  169. }, parent.window.$.apps);
  170. const renderMap =
  171. {
  172. html: updatePageWithHtml,
  173. body: (data) => $('body').html(data),
  174. title: (data) => document.title = data,
  175. featureBar: updateFeatureBar,
  176. pageCSS: updatePageCSS,
  177. pageJS: updatePageJS,
  178. configJS: updateConfigJS,
  179. activeMenu: (data) => activeNav(data),
  180. navbar: updateNavbar,
  181. heading: updateHeading,
  182. fatal: showFatalError,
  183. hookCode: updateHookCode,
  184. zinDebug: (data, _info, options) => showZinDebugInfo(data, options),
  185. zinErrors: (data, _info, options) => showErrors(data, options.id === 'page'),
  186. };
  187. function registerRender(name, callback)
  188. {
  189. renderMap[name] = callback;
  190. }
  191. function showFatalError(data, _info, options)
  192. {
  193. const isNormalPage = data.startsWith('<!DOCTYPE html');
  194. zui.Modal.showError({error: isNormalPage ? data : `<b>URL</b>: ${options.url}<br>${data}`, size: 'lg', html: !isNormalPage})
  195. }
  196. function initZinbar()
  197. {
  198. if(!hasZinBar) return;
  199. let $bar = $('#zinbar');
  200. if($bar.length) return;
  201. $bar = $('<div id="zinbar"></div>').insertAfter('body');
  202. zinbar = new zui.Zinbar($bar[0], typeof window.zin.zinTool === 'object' ? window.zin.zinTool : {});
  203. }
  204. function registerTimer(callback, time, type)
  205. {
  206. type = type || 'timeout';
  207. const id = type === 'interval' ? setInterval(callback, time) : setTimeout(callback, time);
  208. timers[type].push(id);
  209. return id;
  210. }
  211. function updateConfigJS(data)
  212. {
  213. $('#configJS').replaceWith(data);
  214. config = window.config;
  215. const $body = $(document.body);
  216. const classList = ($body.attr('class') || '').split(' ').filter(x => x.length && !x.startsWith('m-'));
  217. const pageID = `${config.currentModule}-${config.currentMethod}`;
  218. classList.push(`m-${pageID}`);
  219. $body.attr('class', classList.join(' ')).attr('data-page', pageID).attr('data-page-raw', `${config.rawModule}-${config.rawMethod}`);
  220. }
  221. function updatePageCSS(data, _info, options)
  222. {
  223. let $style = $(`style.zin-page-css[data-id="${options.pageID}"]`);
  224. if(!$style.length) $style = $('body>style.zin-page-css');
  225. $style.html(data, false);
  226. }
  227. function updatePageJS(data, _info, options)
  228. {
  229. if(window.onPageUnmount) window.onPageUnmount();
  230. $(document).trigger('pageunmount.app');
  231. ['beforePageLoad', 'beforeRequestContent', 'onPageUnmount', 'beforePageUpdate', 'afterPageUpdate', 'onPageRender', 'afterPageRender', 'getPageFormHelper'].forEach(key =>
  232. {
  233. if(window[key]) delete window[key];
  234. });
  235. if(timers.interval.length) timers.interval.forEach(clearInterval);
  236. if(timers.timeout.length) timers.timeout.forEach(clearTimeout);
  237. timers.interval = [];
  238. timers.timeout = [];
  239. if(!options.modal) zui.Modal.getAll().forEach(m => !m.options.modal && m.hide());
  240. $('script.zin-page-js').replaceWith(data);
  241. }
  242. function updateHookCode(data)
  243. {
  244. let $code = $('#zinHookCode');
  245. if(!$code.length) $code = $('<div id="zinHookCode" class="hidden"></div>').appendTo('body');
  246. $code.html(data);
  247. }
  248. function updateZinbar(perf, errors, basePath)
  249. {
  250. if(!hasZinBar) return;
  251. if(zinbar && zinbar.$) zinbar.$.update(perf, errors, basePath);
  252. else requestAnimationFrame(() => updateZinbar(perf, errors, basePath));
  253. }
  254. function getPerfData()
  255. {
  256. return (zinbar && zinbar.$) ? zinbar.$.state.pagePerf : null;
  257. }
  258. function triggerEvent(event, args, options)
  259. {
  260. if(!isInAppTab ||!$.apps.triggerAppEvent) return;
  261. $.apps.triggerAppEvent(currentCode, event, [getPageInfo(), args], options);
  262. }
  263. function triggerPerfEvent(stage)
  264. {
  265. if(!zinbar || !zinbar.$) return;
  266. if(zinbar.lastPerfEventType === stage) clearTimeout(zinbar.lastPerfEventTimer);
  267. zinbar.lastPerfEventTimer = setTimeout(() => {
  268. triggerEvent('updatePerfData.app', {stage: stage, perf: getPerfData()}, {silent: true});
  269. }, 100);
  270. zinbar.lastPerfEventType = stage;
  271. }
  272. function updatePerfInfo(options, stage, info)
  273. {
  274. if(!hasZinBar) return;
  275. const perf = {id: options.id, url: options.url || currentAppUrl};
  276. perf[stage] = options.time || performance.now();
  277. if(stage === 'requestBegin') $.extend(perf, {requestEnd: undefined});
  278. else if(stage === 'renderBegin') zinbar.waitZUI = {time: perf[stage], id: options.id, url: options.url};
  279. if(info)
  280. {
  281. if(info.perf) $.extend(perf, info.perf);
  282. if(info.dataSize) perf.dataSize = info.dataSize;
  283. }
  284. updateZinbar(perf);
  285. triggerPerfEvent(stage);
  286. }
  287. function showZinDebugInfo(data, options)
  288. {
  289. if(!DEBUG) return;
  290. if(data.debug)
  291. {
  292. window.zinDebug = data.debug;
  293. data.debug.forEach(dump =>
  294. {
  295. console.groupCollapsed(`%c ${currentCode.toUpperCase()} %c ${getUrlID()} %c DEBUG %c${dump.name}`, 'color:#fff;font-weight:bold;background:#ec4899', 'background:rgba(233,30,99,0.2);color:#ec4899;font-weight:bold', 'color:#ec4899;font-weight:bold', 'font-weight:bold');
  296. dump.data.forEach(item =>
  297. {
  298. if(typeof item === 'string') console.log('%c' + item, 'font-family:ui-monospace,monospace; padding:0 4px;border-left:1px solid #ff0000;opacity:0.8');
  299. else console.log(item);
  300. });
  301. console.groupCollapsed('trace');
  302. console.log('%c' + dump.trace.join('\n'), 'font-family: ui-monospace,monospace; padding:0 4px;');
  303. console.groupEnd();
  304. console.groupEnd();
  305. });
  306. }
  307. updateZinbar({id: options.id, trace: data.trace, xhprof: data.xhprof}, data.errors, data.basePath);
  308. }
  309. function updatePageWithHtml(data)
  310. {
  311. const html = [];
  312. const skipTags = new Set(['SCRIPT', 'META']);
  313. $(data).each(function(_idx, node)
  314. {
  315. const nodeName = node.nodeName;
  316. if(nodeName === '#text') html.push(node.textContent);
  317. else if(nodeName === 'SCRIPT' && node.innerText.startsWith('window.config={')) html.push(node.outerHTML);
  318. else if(nodeName === 'TITLE') document.title = node.innerText;
  319. else if(skipTags.has(nodeName)) return;
  320. else html.push(node.outerHTML);
  321. });
  322. $('body').html(html.join(''));
  323. window.zin = {config: window.config};
  324. if(DEBUG) console.log('[ZIN] ', window.zin);
  325. console.warn('ZIN: load an old page.');
  326. }
  327. function updateNavbar(data)
  328. {
  329. $('#navbar').morphInner(`<div>${data}</div>`);
  330. }
  331. function updateFeatureBar(data)
  332. {
  333. const $featureBar = $('#featureBar');
  334. const isOpenSearch = $featureBar.find('.search-form-toggle.active').length && $featureBar.closest('.show-search-form').length;
  335. $featureBar.html(data);
  336. if(isOpenSearch)
  337. {
  338. const $searchToggle = $featureBar.find('.search-form-toggle');
  339. if(!$searchToggle.hasClass('active'))
  340. {
  341. zui.toggleSearchForm({module: $searchToggle.data('module'), show: false});
  342. }
  343. }
  344. }
  345. function updateHeading(data, info)
  346. {
  347. const $heading = $('#heading');
  348. if(!$heading.length) return;
  349. const selector = parseSelector(info.selector);
  350. renderWithHtml($heading, data, selector);
  351. }
  352. function activeNav(activeID, nav)
  353. {
  354. const $nav = $(nav || '#navbar');
  355. const $active = $nav.find('.nav-item>a.active');
  356. if($active.data('id') === activeID) return;
  357. $active.removeClass('active');
  358. $nav.find('.nav-item>a[data-id="' + activeID + '"]').addClass('active');
  359. }
  360. function beforeUpdate($target, info, options)
  361. {
  362. if($target && $target.length) $target.find('[data-zin-events]').off('.zin.on');
  363. if(window.beforePageUpdate) window.beforePageUpdate($target, info, options);
  364. }
  365. function afterUpdate($target, info, options)
  366. {
  367. if(window.afterPageUpdate) window.afterPageUpdate($target, info, options);
  368. triggerEvent('updatepart.app', {target: $target, info, options}, {silent: true});
  369. }
  370. function renderWithHtml($target, html, selector, noMorph)
  371. {
  372. if(typeof selector === 'string') selector = parseSelector(selector);
  373. if(config.morphUpdate === false || $target.hasClass('no-morph')) noMorph = true;
  374. if(selector.inner)
  375. {
  376. if(noMorph)
  377. {
  378. $target.html(html);
  379. }
  380. else
  381. {
  382. const tagName = $target.prop('tagName').toLowerCase();
  383. $target.morphInner(`<${tagName}>${html}</${tagName}>`);
  384. }
  385. }
  386. else
  387. {
  388. if(noMorph)
  389. {
  390. $target.replaceWith(html);
  391. $target = $(selector.select);
  392. }
  393. else
  394. {
  395. $target.morph(html);
  396. }
  397. }
  398. return $target;
  399. }
  400. function renderPartial(info, options)
  401. {
  402. if(window.onPageRender && window.onPageRender(info, options)) return;
  403. if(options.onRender && options.onRender(info, options)) return;
  404. const render = (options.renders ? options.renders[info.name] : null) || renderMap[info.name];
  405. const isHtml = info.type === 'html';
  406. const selector = parseSelector(info.selector);
  407. let $target = selector ? $(selector.select) : null;
  408. if(render)
  409. {
  410. if(isHtml) beforeUpdate($target, info, options);
  411. render(info.data, info, options);
  412. if(isHtml) afterUpdate($target, info, options);
  413. return;
  414. }
  415. /* Common render */
  416. if(!selector)
  417. {
  418. if(DEBUG) showLog('Render partial', 'error:cannot render partial content without selector.', {info, options});
  419. else console.warn('[APP] ', 'cannot render partial content with data', info);
  420. return;
  421. }
  422. if(!$target.length)
  423. {
  424. if(DEBUG) showLog('Render partial', 'cannot find target element with selector.', {info, selector, options});
  425. else console.warn('[APP] ', 'cannot find target element with selector.', info);
  426. return;
  427. }
  428. if(selector.first) $target = $target.first();
  429. if(selector.type === 'json')
  430. {
  431. const props = info.data.props;
  432. if(typeof props !== 'object') return;
  433. const component = $target.zui(info.name);
  434. if(typeof component === 'object' && typeof component.render === 'function')
  435. {
  436. $target.closest('[zui-create-dtable]').attr(`zui-create-${component.constructor.ZUI || info.name}`, '');
  437. beforeUpdate($target, info, options);
  438. component.render(props);
  439. afterUpdate($target, info, options);
  440. }
  441. return;
  442. }
  443. beforeUpdate($target, info, options);
  444. $target = renderWithHtml($target, info.data, selector, options.noMorph);
  445. afterUpdate($target, info, options);
  446. }
  447. function renderPage(list, options)
  448. {
  449. if(DEBUG) showLog('Render', [options.id, list.length, options.noMorph ? 'html' : 'morph'], list, {options});
  450. let updateFullPage = false;
  451. list.forEach(item =>
  452. {
  453. try{renderPartial(item, options);}
  454. catch(error) {console.error('[ZIN] ', 'Render partial failed', error, {item, options});}
  455. if(item.name === 'html' || item.name === 'body') updateFullPage = true;
  456. });
  457. if(updateFullPage)
  458. {
  459. $('html').enableScroll();
  460. }
  461. if(window.afterPageRender) window.afterPageRender(list, options);
  462. triggerEvent('updatepage.app', {updateFullPage: updateFullPage, options});
  463. if(!options.partial)
  464. {
  465. const newState = $.apps.updateApp(currentCode, currentAppUrl, document.title);
  466. if(newState) historyState = newState;
  467. }
  468. }
  469. function toggleLoading(target, isLoading, loadingClass, loadingIndicatorDelay)
  470. {
  471. var $target = $(target);
  472. loadingClass = loadingClass || 'loading';
  473. const position = $target.css('position');
  474. if(!['relative', 'absolute', 'fixed'].includes(position)) $target.css('position', 'relative');
  475. if(isLoading === undefined) isLoading = !$target.hasClass(loadingClass);
  476. $target.css('--load-indicator-delay', isLoading && loadingIndicatorDelay ? loadingIndicatorDelay : null);
  477. if(!$target.hasClass('load-indicator')) $target.addClass('load-indicator');
  478. if(isLoading) setTimeout(() => $target.find('.load-indicator.loading').removeClass('loading'), loadingIndicatorDelay || 0);
  479. $target.toggleClass(loadingClass, isLoading);
  480. }
  481. /**
  482. * Request data from remote server
  483. * @param {Object} options
  484. * @param {string} options.id
  485. * @param {string} options.url
  486. * @param {string} options.selector
  487. * @param {string} [options.target]
  488. * @param {string} [options.loadingTarget]
  489. * @param {string} [options.method]
  490. * @param {FormData|Form|Record<string, unknown>} [options.data]
  491. * @param {{selector: string, type: string}} [options.zinOptions]
  492. * @param {function} [options.success]
  493. * @param {function} [options.error]
  494. * @param {function} [options.complete]
  495. * @param {function} [onFinish]
  496. */
  497. function requestContent(options, onFinish)
  498. {
  499. const target = options.target || '#main';
  500. const selectors = (Array.isArray(options.selector) ? options.selector : options.selector.split(',')).map(selector => selector.replace(':component', ':type=json&data=props'));
  501. const url = options.url;
  502. if(DEBUG && !selectors.includes('zinDebug()')) selectors.push('zinDebug()');
  503. const isDebugRequest = DEBUG && selectors.length === 1 || selectors[0] === 'zinDebug()';
  504. if(options.modal === undefined) options.modal = $(target[0] !== '#' && target[0] !== '.' ? `#${target}` : target).closest('.modal').length;
  505. const headers =
  506. {
  507. 'X-ZIN-Options': JSON.stringify($.extend({selector: selectors, type: 'list'}, options.zinOptions)),
  508. 'X-ZIN-App': currentCode,
  509. 'X-Zin-Cache-Time': 0,
  510. 'X-ZIN-UID': zui.uid ? zui.uid() : ''
  511. };
  512. if(options.modal) headers['X-Zui-Modal'] = 'true';
  513. if(isTutorial && top.getCurrentStepID) headers['X-ZIN-Tutorial'] = top.getCurrentStepID();
  514. const requestMethod = (options.method || 'GET').toUpperCase();
  515. if(!options.cache && options.cache !== false) options.cache = !!(requestMethod === 'GET' && config.clientCache && !options.partial);
  516. if(options.cache === true) options.cache = url + (url.includes('?') ? '&zin=' : '?zin=') + encodeURIComponent(selectors.join(','));
  517. const cacheKey = options.cache;
  518. const rid = options.rid;
  519. let cache;
  520. let cacheHit;
  521. const renderPageData = (data, onlyZinDebug) =>
  522. {
  523. const renderOptions = $.extend({noMorph: !config.morphUpdate || (!options.partial && options.isDiffPage)}, options);
  524. if(!onlyZinDebug) updatePerfInfo(options, 'renderBegin');
  525. renderPage(data.reduce((list, item, idx) =>
  526. {
  527. if(Array.isArray(item)) item = {name: item[0].split(':')[0], data: item[1], type: item[2] || 'data'};
  528. item.selector = selectors[idx];
  529. if(!onlyZinDebug || item.name === 'zinDebug') list.push(item);
  530. return list;
  531. }, []), renderOptions);
  532. if(!onlyZinDebug) updatePerfInfo(options, 'renderEnd', {perf: {clientCache: cacheHit ? cacheKey : null}});
  533. };
  534. const callCallback = (name, args) =>
  535. {
  536. let callback = options[name];
  537. if(!callback) return;
  538. if(typeof callback === 'string') callback = zui.evalValue(callback);
  539. return callback.apply(null, args);
  540. };
  541. const ajax = new zui.Ajax(
  542. {
  543. url: url + (url.includes('?') ? '&zin=1' : '?zin=1'),
  544. headers: headers,
  545. type: requestMethod,
  546. data: options.data,
  547. convert: options.convert,
  548. type: (options.method || 'GET').toUpperCase(),
  549. beforeSend: () =>
  550. {
  551. updatePerfInfo(options, 'requestBegin');
  552. if(isDebugRequest) return;
  553. if(options.loadingTarget !== false) toggleLoading(options.loadingTarget || target, true, options.loadingClass, options.loadingIndicatorDelay || (options.partial ? '.2s' : (cache ? '5s' : '1s')));
  554. if(options.before) options.before();
  555. if(!options.partial) $('body').addClass('loading-page');
  556. },
  557. success(rawData)
  558. {
  559. if(!ridSet.has(rid))
  560. {
  561. if(DEBUG) showLog('Request', `error:canceled request ${rid} on success`, {url, rid, options});
  562. return;
  563. }
  564. const response = this.response;
  565. if(response && response.redirected)
  566. {
  567. if(!isInAppTab)
  568. {
  569. location.href = response.url;
  570. return;
  571. }
  572. if(isInAppTab && currentCode !== $.apps.getAppCode(response.url, currentCode))
  573. {
  574. $.apps.openApp(response.url, response.url.includes('#app=') ? null : currentCode);
  575. return;
  576. }
  577. }
  578. updatePerfInfo(options, 'requestEnd', {dataSize: rawData.length, perf: {clientCache: cache ? cacheKey : null}});
  579. options.result = 'success';
  580. let hasFatal = false;
  581. let data;
  582. if(!rawData && !ajax.sendedAgain)
  583. {
  584. ajax.sendedAgain = true;
  585. ajax.setting.headers['X-Zin-Debug'] = true;
  586. delete ajax.data;
  587. delete ajax.error;
  588. ajax.send();
  589. ajax.canceled = true;
  590. return;
  591. }
  592. ajax.canceled = false;
  593. try
  594. {
  595. data = $.parseRawData(rawData);
  596. }
  597. catch(e)
  598. {
  599. if(DEBUG) showLog('Request', 'error:parse data failed', e, {url, options, rawData});
  600. if(!isInAppTab && config.zin) return;
  601. hasFatal = rawData.includes('Fatal error') || rawData.includes('Uncaught TypeError:') || rawData.startsWith('<!DOCTYPE html');
  602. ;
  603. data = [{name: hasFatal ? 'fatal' : 'html', data: rawData}];
  604. }
  605. if(Array.isArray(data))
  606. {
  607. if(workspaceType)
  608. {
  609. const navbarData = data.find(x => x.name === 'navbar');
  610. if(navbarData && !navbarData.data.includes(`__WORKSPACE_${workspaceType}__`))
  611. {
  612. window.open(response.url, '_blank');
  613. ajax.canceled = true;
  614. return;
  615. }
  616. }
  617. if(!options.partial && !hasFatal) currentAppUrl = (response && response.url) ? (response.url.split('?zin=')[0].split('&zin=')[0]) : url;
  618. let newCacheData = (cacheKey && !hasFatal) ? rawData : null;
  619. if(newCacheData && DEBUG)
  620. {
  621. const parts = newCacheData.split(',["zinDebug:<BEGIN>",');
  622. if(parts.length > 1) newCacheData = parts[0] + ']';
  623. }
  624. cacheHit = !hasFatal && cache && newCacheData === cache.data.data;
  625. if(!cacheHit || !localCacheFirst)
  626. {
  627. renderPageData(data);
  628. }
  629. else if(DEBUG)
  630. {
  631. showLog('Request', 'success:skip render with effective caching', {cacheKey, data})
  632. renderPageData(data, true);
  633. }
  634. $(document).trigger('pagerender.app');
  635. callCallback('success', [data]);
  636. if(cacheKey)
  637. {
  638. const cacheTime = +response.headers.get('X-Zin-Cache-Time');
  639. if(cacheTime)
  640. {
  641. $.db.setCacheData(cacheKey, {data: newCacheData, url: currentAppUrl, partial: options.partial, selectors: selectors.join(','), clientTime: Date.now()}, cacheTime, 'zinFetch');
  642. }
  643. }
  644. }
  645. else
  646. {
  647. if(options.success && options.success.call(ajax, data, options) === false) return;
  648. if(data.closeModal) zui.Modal.hide(typeof data.closeModal === 'string' ? data.closeModal : undefined);
  649. if(data.autoLoad) autoLoad(data.autoLoad);
  650. if(data.result === 'fail')
  651. {
  652. if(data.message) zui.Messager.show({content: data.message, type: 'danger', className: 'bg-danger text-canvas gap-2 messager-fail'});
  653. }
  654. if(data.open)
  655. {
  656. openUrl(data.open);
  657. }
  658. else if(data.load)
  659. {
  660. if(data.load === 'table') loadTable();
  661. if(data.load === 'login') window.top.location.href = $.createLink('user', 'login', 'referer=' + btoa(url));
  662. else if(typeof data.load === 'string') loadPage(data.load);
  663. else if(data.load === true) loadCurrentPage();
  664. else if(typeof data.load === 'object')
  665. {
  666. if(data.load.autoLoad) autoLoad(data.load.autoLoad);
  667. if('back' in data.load)
  668. {
  669. openUrl(data.load);
  670. }
  671. else if('confirm' in data.load)
  672. {
  673. $(document).trigger('locate.zt', data.load);
  674. }
  675. else if('alert' in data.load)
  676. {
  677. zui.Modal.alert(data.load.alert).then(function()
  678. {
  679. if(data.load.modal) loadModal(data.load.modal);
  680. if(data.load.locate) openUrl(data.load.locate);
  681. });
  682. }
  683. else
  684. {
  685. loadPage(data.load);
  686. }
  687. }
  688. }
  689. if(data.callback)
  690. {
  691. const callback = data.callback;
  692. if(typeof callback === 'string')
  693. {
  694. const func = $.runJS(callback);
  695. if (typeof func === 'function' && !callback.endsWith(';')) func();
  696. }
  697. else if(typeof callback === 'object')
  698. {
  699. const func = $.runJS(callback.name);
  700. if (typeof func === 'function')
  701. {
  702. func.apply(null, Array.isArray(callback.params) ? callback.params : [callback.params]);
  703. }
  704. }
  705. }
  706. }
  707. },
  708. error: (error, type) =>
  709. {
  710. if(!ridSet.has(rid))
  711. {
  712. if(DEBUG) showLog('Request', `error:canceled request ${rid} on error`, {url, rid, options});
  713. return;
  714. }
  715. const data = ajax.data;
  716. if(!data && !ajax.sendedAgain)
  717. {
  718. ajax.sendedAgain = true;
  719. ajax.setting.headers['X-Zin-Debug'] = true;
  720. delete ajax.data;
  721. delete ajax.error;
  722. ajax.send();
  723. ajax.canceled = true;
  724. return;
  725. }
  726. else if(data)
  727. {
  728. showFatalError(data, null, options);
  729. ajax.canceled = false;
  730. }
  731. if(ajax.canceled) return;
  732. updatePerfInfo(options, 'requestEnd', {error: error});
  733. if(type === 'abort') return console.log('[ZIN] ', 'Abord fetch data from ' + url, {type, error});
  734. if(DEBUG) console.error('[ZIN] ', 'Fetch data failed from ' + url, {type, error});
  735. if(!data) zui.Messager.show('ZIN: Fetch data failed from ' + url);
  736. callCallback('error', [data, error]);
  737. },
  738. complete: () =>
  739. {
  740. const frameElement = window.frameElement;
  741. if(frameElement)
  742. {
  743. frameElement.classList.remove('loading');
  744. frameElement.classList.add('in');
  745. }
  746. if(!options.partial) $('body').removeClass('loading-page');
  747. if(options.loadingTarget !== false) toggleLoading(options.loadingTarget || target, false, options.loadingClass);
  748. if(ajax.canceled) return;
  749. if(onFinish) onFinish();
  750. $(document).data('zinCache', null);
  751. callCallback('complete', []);
  752. $(document).trigger('pageload.app');
  753. }
  754. });
  755. ajax.sendedAgain = true; // Disable the request again.
  756. updatePerfInfo(options, 'requestBegin', {perf: {renderBegin: undefined, renderEnd: undefined}});
  757. if(DEBUG) showLog('Request', `${ajax.setting.type}:${options.id} ${getUrlID(url)} task ${rid}`, options, {cacheKey, ajax});
  758. if(currentCode) $.cookie.set('tab', currentCode, {expires: config.cookieLife, path: config.webRoot});
  759. if(cacheKey)
  760. {
  761. $.db.getCache(cacheKey, 'zinFetch').then(localCache =>
  762. {
  763. if(!ridSet.has(rid))
  764. {
  765. if(DEBUG) showLog('Request', `error:canceled request ${rid} on load cache`, {url, rid, options});
  766. return;
  767. }
  768. cache = localCache;
  769. if(cache && localCacheFirst)
  770. {
  771. ajax.setting.headers['X-Zin-Cache-Time'] = cache.updateTime;
  772. if($(document).data('zinCache') !== [cache.key, cache.time].join('#'))
  773. {
  774. try
  775. {
  776. const data = $.parseRawData(cache.data.data);
  777. if(DEBUG) showLog('Request', 'success:render with cache', {url, cacheKey, data, options});
  778. if(!cache.data.partial) currentAppUrl = cache.data.url;
  779. renderPageData(data);
  780. options.isDiffPage = false;
  781. $(document).data('zinCache', [cache.key, cache.time].join('#')).trigger('pagecaheload.app');
  782. }
  783. catch(error)
  784. {
  785. if(DEBUG) showLog('Request', 'error:Parse cache data failed', {url, cacheKey, options});
  786. $.db.setCacheData(cacheKey, null, cache.time, 'zinFetch');
  787. }
  788. }
  789. else
  790. {
  791. if(DEBUG) showLog('Request', 'success:skip render data with same cache', {url, cacheKey, options});
  792. }
  793. }
  794. else
  795. {
  796. if(DEBUG && localCacheFirst) showLog('Request', 'no local cache', {url, cacheKey, options});
  797. }
  798. ajax.send();
  799. });
  800. }
  801. else
  802. {
  803. ajax.send();
  804. }
  805. return ajax;
  806. }
  807. function fetchContent(url, selectors, options)
  808. {
  809. if(typeof url === 'object')
  810. {
  811. options = url;
  812. url = options.url;
  813. selectors = options.selector;
  814. }
  815. if(typeof options === 'string') options = {id: options};
  816. else if(typeof options === 'function') options = {success: options};
  817. selectors = Array.isArray(selectors) ? selectors.join(',') : selectors;
  818. const id = selectors.includes('pageJS') ? 'page' : (options.id || selectors);
  819. let task = fetchTasks.get(id);
  820. if(task && !options.partial && task.url === url)
  821. {
  822. if(DEBUG) showLog('Request', [`success:same request ${task.rid}`, url], {task, options});
  823. return;
  824. }
  825. options = $.extend({}, options, {url: url, selectors: selectors, id: id, isDiffPage: isDiffPage(url), pageID: getUrlID(url)});
  826. const dispatchTask = () =>
  827. {
  828. if(task)
  829. {
  830. if(task.xhr) task.xhr.abort();
  831. if(task.timerID) clearTimeout(task.timerID);
  832. if(task.rid) ridSet.delete(task.rid);
  833. }
  834. const rid = $.guid++;
  835. task = {url: url, selectors: selectors, options: options, rid: rid};
  836. options.rid = rid;
  837. fetchTasks.set(id, task);
  838. ridSet.add(rid);
  839. task.timerID = setTimeout(() =>
  840. {
  841. task.timerID = 0;
  842. task.xhr = requestContent(options, () =>
  843. {
  844. if(task.xhr)
  845. {
  846. const runID = task.xhr.getResponseHeader('Xhprof-RunID');
  847. if(runID) updateZinbar({id: id, xhprof: `${config.webRoot}xhprof/xhprof_html/index.php?run=${runID}&source=${config.currentModule}_${config.currentMethod}`});
  848. task.xhr = null;
  849. }
  850. fetchTasks.delete(id);
  851. ridSet.delete(rid);
  852. });
  853. }, options.delayTime || 0);
  854. };
  855. if(window.beforeRequestContent)
  856. {
  857. const result = window.beforeRequestContent(options, task);
  858. if(result === false) return;
  859. if(result instanceof Promise)
  860. {
  861. result.then((result) => result && dispatchTask());
  862. return;
  863. }
  864. }
  865. dispatchTask();
  866. }
  867. /** Load an old page. */
  868. function loadOldPage(url)
  869. {
  870. let $page = $('#oldPage');
  871. const clearTimer = () =>
  872. {
  873. const timer = $page.data('timer');
  874. if(timer)
  875. {
  876. clearTimeout(timer);
  877. $page.data('timer', 0);
  878. }
  879. };
  880. if(!$page.length)
  881. {
  882. $page = $('<div/>')
  883. .append($('<iframe />').attr({name: `app-${currentCode}-old`, frameborder: 'no', scrolling: 'auto', style: 'width:100%;height:100%;'}))
  884. .attr({id: 'oldPage', class: 'canvas fixed w-full h-full top-0 left-0', style: 'z-index:9999;'})
  885. .insertAfter('body')
  886. .on('oldPageLoad.app', () =>
  887. {
  888. const frame = window.frameElement;
  889. frame.classList.remove('loading');
  890. frame.classList.add('in');
  891. const $iframe = $page.find('iframe').removeClass('invisible');
  892. $page.removeClass('loading').find('iframe').addClass('in');
  893. $(document).trigger('pageload.app');
  894. const iframeWindow = $iframe[0].contentWindow;
  895. oldPageCofnig = iframeWindow.config;
  896. if(iframeWindow.$) iframeWindow.$(iframeWindow.document).on('click', () => window.parent.$('body').trigger('click'));
  897. clearTimer();
  898. });
  899. }
  900. clearTimer();
  901. if($page.hasClass('hidden')) $page.addClass('loading').removeClass('hidden');
  902. const $iframe = $page.find('iframe').removeClass('in').addClass('invisible');
  903. if($iframe.attr('src') === url && $iframe[0].contentWindow.location.href === url)
  904. {
  905. $iframe[0].contentWindow.location.reload();
  906. }
  907. else
  908. {
  909. $iframe.attr('src', url);
  910. setTimeout(() =>
  911. {
  912. /* Fix firefox not load page when iframe is not ready. */
  913. if($iframe[0].contentWindow.location.href === 'about:blank') $iframe[0].contentWindow.location.href = url;
  914. }, 500);
  915. }
  916. currentAppUrl = url;
  917. openedOldPage = url;
  918. triggerEvent('openOldPage');
  919. $page.data('timer', setTimeout(() =>
  920. {
  921. if($page.hasClass('loading') || $iframe.hasClass('invisible')) $page.trigger('oldPageLoad.app');
  922. else clearTimer();
  923. }, 1500));
  924. }
  925. /** Hide old page content. */
  926. function hideOldPage()
  927. {
  928. const $page = $('#oldPage');
  929. if(!$page.length) return;
  930. $page.addClass('in hidden');
  931. openedOldPage = null;
  932. oldPageCofnig = null;
  933. }
  934. function getLoadSelector(selector)
  935. {
  936. if(!selector) return $('#main').length ? `#configJS,pageCSS/.zin-page-css>*,pageJS/.zin-page-js,hookCode(),title>*,${$('#header').length ? '#heading>*,#navbar>*,#pageToolbar>*,' : ''}#main>*,` : '#configJS,title>*,body>*';
  937. if(selector[0] === '+') return getLoadSelector() + ',' + selector.substring(1);
  938. return selector;
  939. }
  940. /**
  941. * Load page with zin way.
  942. *
  943. * @param {Object} [options]
  944. * @param {string} [options.url]
  945. * @param {string} [options.selector]
  946. * @param {string} [options.id]
  947. * @param {string} [options.method]
  948. * @param {FormData|Form|Record<string, unknown>} [options.data]
  949. * @param {string} [options.target]
  950. * @param {function} [options.success]
  951. * @param {function} [options.error]
  952. * @param {function} [options.complete]
  953. * @param {string} [selector]
  954. * @returns {void}
  955. */
  956. function loadPage(options, selector)
  957. {
  958. if(typeof options === 'string') options = {url: options};
  959. options = options || {};
  960. if(options.url && (options.method || '').toLowerCase() !== 'post' && $.apps.isOldPage(options.url)) return loadOldPage(options.url);
  961. else hideOldPage();
  962. if(typeof selector === 'string') options.selector = selector;
  963. else if(typeof selector === 'object') options = $.extend({}, options, selector);
  964. if(!options.selector && options.url && options.url.includes(' '))
  965. {
  966. const parts = options.url.split(' ', 2);
  967. options.url = parts[0];
  968. options.selector = parts[1];
  969. }
  970. options = $.extend({url: currentAppUrl, id: options.selector || options.target || 'page'}, options, {selector: getLoadSelector(options.selector)});
  971. if(window.beforePageLoad)
  972. {
  973. const result = window.beforePageLoad(options);
  974. if(result === false) return;
  975. if(result) $.extend(options, result);
  976. }
  977. if(DEBUG) showLog('Load', getUrlID(options.url), options);
  978. fetchContent(options.url, options.selector, options);
  979. }
  980. function loadPartial(url, selector, options)
  981. {
  982. loadPage($.extend({partial: true, url: url, selector: selector, target: selector.replace('>*','')}, options));
  983. }
  984. /** Load zui component. */
  985. function loadComponent(target, options)
  986. {
  987. if(target[0] !== '#' && target[0] !== '.') target = `#${target}`;
  988. options = $.extend({url: currentAppUrl, id: target, target: target}, options);
  989. const $target = $(target);
  990. if(!$target.length) return loadPage({url: options.url, id: target});
  991. if($target.closest('.modal').length)
  992. {
  993. if(options.modal === undefined) options.modal = true;
  994. if(options.partial === undefined) options.partial = true;
  995. }
  996. if(!options.selector)
  997. {
  998. let name = options.component;
  999. if(!name)
  1000. {
  1001. const component = $(target).zui();
  1002. if(!component) return;
  1003. name = component.constructor.ZUI;
  1004. }
  1005. options.selector = `${name}/${target}:component`;
  1006. }
  1007. loadPage(options);
  1008. }
  1009. /**
  1010. * Load dtable content.
  1011. *
  1012. * @param {string} [url]
  1013. * @param {string} [target]
  1014. * @param {Object} [options]
  1015. * @returns
  1016. */
  1017. function loadTable(url, target, options)
  1018. {
  1019. if(!url) url = currentAppUrl;
  1020. if(!target)
  1021. {
  1022. const urlInfo = $.parseLink(url);
  1023. target = urlInfo.moduleName ? `table-${urlInfo.moduleName}-${urlInfo.methodName}` : ($('.dtable').attr('id') || 'dtable');
  1024. }
  1025. if(target[0] !== '#' && target[0] !== '.') target = `#${target}`;
  1026. let selector = `dtable/${target}:component`;
  1027. options = options || {};
  1028. if(options.selector && options.selector !== 'dtable') selector = options.selector;
  1029. const isInModal = $(target).closest('.modal').length;
  1030. if(!isInModal && !options.selector)
  1031. {
  1032. selector = 'pageJS/.zin-page-js,' + selector + ',#mainMenu>*,hookCode()';
  1033. if($('#mainNavbar a > .label').length) selector += ',#mainNavbar>*';
  1034. if($('#moduleMenu').length) selector += ',#moduleMenu>*,.module-menu-header';
  1035. if($('#docDropmenu').length) selector += ',#docDropmenu,.module-menu';
  1036. }
  1037. delete options.selector;
  1038. return loadComponent(target, $.extend({cache: isDiffPage(url), component: 'dtable', url: url, selector: selector, modal: isInModal}, options));
  1039. }
  1040. /**
  1041. * Load modal content.
  1042. *
  1043. * @param {string} [url]
  1044. * @param {string} [target]
  1045. * @param {Object} [options]
  1046. * @param {Function} [callback]
  1047. * @returns
  1048. */
  1049. function loadModal(url, target, options, callback)
  1050. {
  1051. if(!url)
  1052. {
  1053. const lasShowModal = zui.Modal.query(undefined, undefined, modal => modal.shown);
  1054. if(!lasShowModal) return;
  1055. url = lasShowModal.options.url;
  1056. target = lasShowModal.id;
  1057. }
  1058. options = $.extend(typeof url === 'object' ? (url || {}) : {url: url}, options);
  1059. target = target || options.target;
  1060. callback = callback || options.callback;
  1061. if(!target || target === 'current')
  1062. {
  1063. const lastModal = zui.Modal.last();
  1064. if(!lastModal) return zui.Modal.open(options);
  1065. target = lastModal.id;
  1066. }
  1067. if(typeof target === 'string' && target[0] !== '#' && target[0] !== '.') target = `#${target}`;
  1068. const modal = zui.Modal.query(target);
  1069. if(!modal) return;
  1070. if(((modal.options.url && options.url) || (modal.options.request && options.request)) && options.loadingClass === undefined)
  1071. {
  1072. const lastUrl = $.parseLink(modal.options.url) || $.parseLink(modal.options.request?.url);
  1073. const newUrl = $.parseLink(options.url) || $.parseLink(options.request?.url);
  1074. if(lastUrl.moduleName === newUrl.moduleName && lastUrl.methodName === newUrl.methodName) options.loadingClass = '';
  1075. }
  1076. if(modal.options.request && options.url) {modal.options.request = options.request}
  1077. modal.render(options).then((result) => {if(result && callback) callback(modal.dialog);});
  1078. }
  1079. function loadTarget(url, target, options)
  1080. {
  1081. options = $.extend({}, options);
  1082. if(typeof target === 'string') options.target = target;
  1083. else if($.isPlainObject(target)) options = $.extend(options, target);
  1084. if(typeof url === 'string') options.url = url;
  1085. else if(typeof url === 'object') options = $.extend(options, url);
  1086. if(!options.target) return loadPage(options);
  1087. let remoteData;
  1088. let loadError;
  1089. target = options.target;
  1090. if(typeof target === 'string' && target[0] !== '#' && target[0] !== '.') target = `#${target}`;
  1091. const $target = $(target);
  1092. if(!$target.length) return;
  1093. if(options.cache)
  1094. {
  1095. const cache = (typeof options.cache === 'number' ? options.cache : 3600) * 1000;
  1096. const lastLoad = $target.data('zin-target-load');
  1097. if(lastLoad && (Date.now() - lastLoad) < cache) return;
  1098. }
  1099. const ajaxOptions =
  1100. {
  1101. url: options.url,
  1102. header: options.header,
  1103. type: options.method || 'GET',
  1104. data: options.data,
  1105. contentType: options.contentType,
  1106. beforeSend: () =>
  1107. {
  1108. toggleLoading(options.target, true);
  1109. if(options.beforeSend) return options.beforeSend();
  1110. },
  1111. success: (data) =>
  1112. {
  1113. remoteData = data;
  1114. if(options.beforeUpdate)
  1115. {
  1116. const result = options.beforeUpdate(data, options);
  1117. if(result === false) return;
  1118. if(typeof result === 'string') data = result;
  1119. }
  1120. if(options.success) options.success(data, options);
  1121. let $content = $(data);
  1122. if(options.selector) $content = $('<div>').append($content).find(options.selector);
  1123. if(options.replace) $target.replaceWith($content);
  1124. else $target.empty().append($content);
  1125. $target.data('zin-target-load', Date.now()).zuiInit();
  1126. },
  1127. error: (xhr, type, error) =>
  1128. {
  1129. loadError = error;
  1130. if(options.error) options.error(error, options);
  1131. if(DEBUG) console.error('[ZIN] ', 'Fetch data failed from ' + url, {xhr, type, error});
  1132. zui.Messager.show('ZIN: Fetch data failed from ' + url);
  1133. },
  1134. complete: () =>
  1135. {
  1136. toggleLoading(target, false);
  1137. if(options.complete) options.complete(remoteData, loadError, options);
  1138. }
  1139. };
  1140. return $.ajax(ajaxOptions);
  1141. }
  1142. function postAndLoadPage(url, data, selector, options)
  1143. {
  1144. if(typeof selector === 'object')
  1145. {
  1146. options = selector;
  1147. selector = null;
  1148. }
  1149. options = $.extend({url: url, selector: selector, method: 'POST', data: data, contentType: false}, options);
  1150. if(options.dataMap)
  1151. {
  1152. options.data = zui.createFormData(zui.mapFormData(options.dataMap), options.data);
  1153. delete options.dataMap;
  1154. }
  1155. if(!options.data || !(options.data instanceof FormData || Object.keys(options.data).length)) return;
  1156. if(options.app) openPage(url, options.app, options);
  1157. else loadPage(options);
  1158. }
  1159. function loadCurrentPage(options)
  1160. {
  1161. if(options instanceof Event || options instanceof HTMLElement)
  1162. {
  1163. options = {};
  1164. if(DEBUG) console.warn('[APP] ', 'loadCurrentPage() should not be called with an event or element.');
  1165. }
  1166. if(typeof options === 'string') options = {selector: options};
  1167. return loadPage($.extend({cache: false}, options));
  1168. }
  1169. function reloadPage()
  1170. {
  1171. loadPage({cache: false, url: currentAppUrl, selector: 'body>*,title>*,#configJS'});
  1172. }
  1173. /**
  1174. * Load form with zin request.
  1175. *
  1176. * @param {{target: string, url?: string, partial?: boolean, keep?: boolean|string, items?: string, updateActionUrl?: string}} options Load form options.
  1177. */
  1178. function loadForm(options)
  1179. {
  1180. options = $.extend({target: 'form', updateActionUrl: true}, options);
  1181. const $target = $(options.target);
  1182. if(!$target.length) return console.warn('[APP] ', `cannot find target with selector "${options.target}"`);
  1183. let $form = $target.closest('form');
  1184. if(!$form.length) $form = $target.find('form');
  1185. if(!$form.length) return console.warn('[APP] ', `cannot find form from target "${options.target}"`);
  1186. if(options.partial === undefined) options.partial = !!$target.closest('.modal').length;
  1187. const id = $form.attr('id');
  1188. if(!id) return console.warn('[APP] ', `form from "${options.target}" has no id`);
  1189. const formData = new FormData($form[0]);
  1190. const data = {};
  1191. formData.forEach((value, key) =>
  1192. {
  1193. if(key.includes('[')) key = key.substring(0, key.indexOf('['));
  1194. data[key] = value;
  1195. });
  1196. if(DEBUG) showLog('Load form', id, {options, data});
  1197. const $oldItems = $form.children('.form-group[data-name]');
  1198. let items = options.items || [];
  1199. if(typeof items === 'string') items = items.split(',');
  1200. const loadingTarget = items ? $oldItems.filter((_, element) => items.includes($(element).attr('data-name'))) : `#${id}`;
  1201. let url = options.url || $form.attr('data-load-url');
  1202. url = url ? zui.formatString(url, data).replace(/\{\w+\}/g, '') : currentAppUrl;
  1203. loadPage(
  1204. {
  1205. url: url,
  1206. selector: `#${id}`,
  1207. partial: options.partial,
  1208. loadingTarget: loadingTarget,
  1209. loadingClass: 'pointer-events-none',
  1210. cache: false,
  1211. success: () =>
  1212. {
  1213. let updateActionUrl = options.updateActionUrl;
  1214. if(updateActionUrl === true) updateActionUrl = url;
  1215. if(updateActionUrl) $form.attr('action', updateActionUrl);
  1216. },
  1217. onRender: (info) =>
  1218. {
  1219. if(info.name !== id) return;
  1220. if(options.items)
  1221. {
  1222. const $data = $(info.data).children('.form-group[data-name]');
  1223. items.forEach(name =>
  1224. {
  1225. if('.[#'.includes(name[0]))
  1226. {
  1227. const $items = $data.filter(name);
  1228. const $oldMatchItems = $oldItems.filter(name);
  1229. $oldMatchItems.last().after($items);
  1230. $oldMatchItems.remove();
  1231. $items.zuiInit();
  1232. }
  1233. else
  1234. {
  1235. const $items = $data.filter(`[data-name="${name}"]`);
  1236. const $oldMatchItems = $oldItems.filter(`[data-name="${name}"]`);
  1237. if($oldMatchItems.length)
  1238. {
  1239. try {$oldMatchItems.replaceWith($items);} catch (_) {}
  1240. }
  1241. else
  1242. {
  1243. $oldItems.last().after($items);
  1244. }
  1245. $items.zuiInit();
  1246. }
  1247. });
  1248. if(options.updateOrders)
  1249. {
  1250. const formGrid = $form.zui('formGrid');
  1251. if(formGrid)
  1252. {
  1253. const orders = [];
  1254. $data.each((_, element) => orders.push($(element).attr('data-name')));
  1255. formGrid.updateOrders(orders, $form.data('fullModeOrders'));
  1256. }
  1257. }
  1258. }
  1259. else
  1260. {
  1261. $form.html(info.data);
  1262. const formGrid = $form.zui('formGrid');
  1263. if(formGrid) formGrid.toggleMode(formGrid.mode, true);
  1264. }
  1265. let keep = options.keep;
  1266. if(keep === true) keep = Object.keys(data);
  1267. else if(typeof keep === 'string') keep = keep.split(',');
  1268. if(Array.isArray(keep))
  1269. {
  1270. keep.forEach((name) =>
  1271. {
  1272. const $item = $form.find(`[name="${name}"]`);
  1273. if(!$item.length) return;
  1274. const value = data[name];
  1275. if($item.is('.pick-value'))
  1276. {
  1277. const pick = $item.closest('.pick').zui();
  1278. if(pick) pick.$.setValue(value, true);
  1279. }
  1280. else if($item.is('select'))
  1281. {
  1282. $item.find(`option[value="${value}"]`).prop('selected', true);
  1283. }
  1284. else if($item.is(':checkbox'))
  1285. {
  1286. $item.prop('checked', value);
  1287. }
  1288. else
  1289. {
  1290. $item.val(value);
  1291. }
  1292. });
  1293. }
  1294. return true;
  1295. }
  1296. })
  1297. }
  1298. function applyFormData(data, formSelector)
  1299. {
  1300. let formHelper;
  1301. if(window.getPageFormHelper) formHelper = window.getPageFormHelper(formSelector, data);
  1302. if(!formHelper)
  1303. {
  1304. const $form = $(formSelector || '#mainContainer form').filter(function() {return $(this).closest('#formSettingBtn').length === 0;});
  1305. formHelper = zui.zentaoFormHelper($form);
  1306. }
  1307. formHelper.setFormData(data);
  1308. }
  1309. function openPage(url, appCode, options)
  1310. {
  1311. if(DEBUG) showLog('Open page', url, {appCode, options});
  1312. if(!window.config.zin)
  1313. {
  1314. location.href = $.createLink('index', 'app', 'url=' + btoa(url));
  1315. return;
  1316. }
  1317. if(getUrlID(url) === 'index-index') return top.location.href = url;
  1318. return $.apps.openApp(url, $.extend({code: appCode, forceReload: true}, options));
  1319. }
  1320. function autoLoad(id)
  1321. {
  1322. const parseID = (x) =>
  1323. {
  1324. const parts = x.split(':');
  1325. return {type: parts[0], ids: parts[1] ? parts[1].split(',') : []};
  1326. };
  1327. id = parseID(id);
  1328. const idSet = new Set(id.ids);
  1329. const type = id.type;
  1330. $('[data-auto-load]').each(function()
  1331. {
  1332. const $this = $(this);
  1333. const targetID = parseID($this.attr('data-auto-load'));
  1334. if(type !== targetID.type || (targetID.ids.length && !targetID.ids.some(x => idSet.has(x)))) return;
  1335. const modal = zui.Modal.query($this);
  1336. if(modal) modal.render({loadingClass: ''});
  1337. else loadCurrentPage();
  1338. });
  1339. }
  1340. /**
  1341. * Search history and go back to specified path.
  1342. *
  1343. * @param {string} target Back target, can be app name or module-method path.
  1344. * @param {string} url Fallback url.
  1345. * @returns {void}
  1346. */
  1347. function goBack(target, url)
  1348. {
  1349. if(!target || target === 'APP' || target === true) target = currentCode;
  1350. else if(target === 'GLOBAL') target = '';
  1351. $.apps.goBack(target, url, historyState);
  1352. }
  1353. /**
  1354. * Open url in app.
  1355. * @param {string} url
  1356. * @param {Object} options
  1357. * @param {string} options.url
  1358. * @param {string} options.app
  1359. * @param {string} options.load
  1360. * @param {string} options.back
  1361. * @param {Event} event
  1362. */
  1363. function openUrl(url, options, event)
  1364. {
  1365. if(typeof url === 'object')
  1366. {
  1367. options = url;
  1368. url = options.url;
  1369. }
  1370. else
  1371. {
  1372. options = options || {};
  1373. options.url = url;
  1374. }
  1375. if(typeof url === 'string' && url.includes('#'))
  1376. {
  1377. const hash = url.split('#')[1];
  1378. if(hash === 'open-modal' || hash.startsWith('open-modal?'))
  1379. {
  1380. const openModalParams = hash.includes('?') ? hash.split('?')[1] : '';
  1381. const openModalOptions = $.extend({}, {url: url, type: 'ajax'}, options);
  1382. if(openModalParams)
  1383. {
  1384. const searchParams = new URLSearchParams(openModalParams);
  1385. for(const key of searchParams.keys()) openModalOptions[key] = searchParams.getAll(key).join(',');
  1386. }
  1387. zui.Modal.open(openModalOptions);
  1388. return;
  1389. }
  1390. }
  1391. if(DEBUG) showLog('Open url', url, options);
  1392. if(options.app === 'current') options.app = currentCode;
  1393. if(options.confirm)
  1394. {
  1395. return zui.Modal.confirm(options.confirm).then(confirmed =>
  1396. {
  1397. if(confirmed) openUrl(url, $.extend({}, options, {confirm: false}), event);
  1398. else if(options.canceled) openUrl(options.canceled, $.extend({}, options, {confirm: false}), event);
  1399. });
  1400. }
  1401. const load = options.load;
  1402. if(typeof load === 'string' || load)
  1403. {
  1404. if(!options.target) options.target = options.loadId;
  1405. if(url) options.url = url;
  1406. if(load)
  1407. {
  1408. if(load === 'post') return postAndLoadPage(options.url, options.data, options);
  1409. if(load === 'table')
  1410. {
  1411. if(!options.target && event) options.target = $(event.target).closest('.dtable').attr('id');
  1412. return loadTable(options.url, options.target, options);
  1413. }
  1414. if(load === 'modal')
  1415. {
  1416. if(!options.target && event) options.target = $(event.target).closest('.modal').attr('id');
  1417. return loadModal(options);
  1418. }
  1419. if(load === 'target') return loadTarget(options);
  1420. if(load !== 'APP' && typeof load === 'string') options.selector = load;
  1421. delete options.load;
  1422. }
  1423. return loadPage(options);
  1424. }
  1425. if(typeof options.back === 'string') return goBack(options.back, url);
  1426. return openPage(url, options.app);
  1427. }
  1428. /**
  1429. * Parse wg selector.
  1430. * @param {string} selector
  1431. * @return {object|null}
  1432. */
  1433. function parseSelector(selector)
  1434. {
  1435. selector = selector.trim();
  1436. let len = selector.length;
  1437. if(len < 1) return null;
  1438. const result = {class: [], id: '', tag: '', inner: false, name: '', first: false, selector: selector};
  1439. if(selector.includes('/'))
  1440. {
  1441. const parts = selector.split('/', 2);
  1442. result.name = parts[0];
  1443. selector = parts[1];
  1444. len = selector.length;
  1445. }
  1446. selector = selector.replace('> *', '>*');
  1447. if(selector.endsWith('>*'))
  1448. {
  1449. result.inner = true;
  1450. selector = selector.substring(0, selector.length - 2);
  1451. len = selector.length;
  1452. }
  1453. let type = 'tag';
  1454. let current = '';
  1455. let updateResult = function(result, current, type)
  1456. {
  1457. if(!current.length) return;
  1458. if(type === 'class')
  1459. {
  1460. result[type].push(current);
  1461. }
  1462. else if(type === 'option')
  1463. {
  1464. current.split('&').forEach(function(option)
  1465. {
  1466. const parts = option.split('=');
  1467. result[parts[0]] = parts[1] || true;
  1468. });
  1469. }
  1470. else
  1471. {
  1472. result[type] = current;
  1473. }
  1474. };
  1475. for(let i = 0; i < len; i++)
  1476. {
  1477. let c = selector[i];
  1478. let t = '';
  1479. if(c === '#' && type !== 'option')
  1480. {
  1481. t = 'id';
  1482. }
  1483. else if(c === '.' && type !== 'option')
  1484. {
  1485. t = 'class';
  1486. }
  1487. else if(c === '(' && type !== 'option' && selector.endsWith(')'))
  1488. {
  1489. let command = selector.substring(i + 1, selector.length - 1);
  1490. if(!command) command = current;
  1491. result.command = command;
  1492. break;
  1493. }
  1494. else if(c === ':')
  1495. {
  1496. t = 'option';
  1497. }
  1498. if(!t)
  1499. {
  1500. current += c;
  1501. }
  1502. else
  1503. {
  1504. updateResult(result, current, type);
  1505. current = '';
  1506. type = t;
  1507. }
  1508. }
  1509. updateResult(result, current, type);
  1510. if(!result.name.length)
  1511. {
  1512. if(result.id.length) result.name = result.id;
  1513. else if(result.tag) result.name = result.tag;
  1514. else result.name = selector;
  1515. }
  1516. result.select = [result.tag, result.id.length ? '#' + result.id : '', result.class.length ? '.' + result.class.join('.') : ''].join('');
  1517. return result;
  1518. }
  1519. /**
  1520. * Ajax send score to server.
  1521. * @param {string} method
  1522. */
  1523. function ajaxSendScore(method)
  1524. {
  1525. $.get($.createLink('score', 'ajax', 'method=' + method));
  1526. }
  1527. /**
  1528. * Change current app language.
  1529. * @param {string} lang
  1530. */
  1531. function changeAppLang(lang)
  1532. {
  1533. if($('html').attr('lang') === lang) return;
  1534. zui.i18n.setCode(lang);
  1535. reloadPage();
  1536. $('html').attr('lang', lang);
  1537. }
  1538. /**
  1539. * Change current app theme.
  1540. * @param {string} lang
  1541. */
  1542. function changeAppTheme(theme)
  1543. {
  1544. const classList = $('html').attr('class').split(' ').filter(x => x.length && !x.startsWith('theme-'));
  1545. classList.push('theme-' + theme);
  1546. $('html').attr('class', classList.join(' '));
  1547. const $theme = $('#zuiTheme');
  1548. const oldPath = $theme.attr('href').split('/');
  1549. oldPath.pop();
  1550. oldPath.push(theme + '.css');
  1551. $theme.attr('href', oldPath.join('/'));
  1552. const userMenu = $('#userMenu-toggle').zui();
  1553. if(!userMenu) return;
  1554. const themeItem = userMenu.options.menu.items.find(x => x.key === 'theme');
  1555. themeItem.items.forEach(item => {item.active = item['data-value'] === theme;});
  1556. userMenu.render({menu: userMenu.options.menu});
  1557. }
  1558. /**
  1559. * Select UI language.
  1560. * @param {string} lang
  1561. */
  1562. function selectLang(lang)
  1563. {
  1564. $.cookie.set('lang', lang, {expires: config.cookieLife, path: config.webRoot});
  1565. ajaxSendScore('selectLang');
  1566. $.apps.changeAppsLang(lang);
  1567. }
  1568. /**
  1569. * Select UI theme.
  1570. * @param {string} lang
  1571. */
  1572. function selectTheme(theme)
  1573. {
  1574. $.cookie.set('theme', theme, {expires: config.cookieLife, path: config.webRoot});
  1575. $.ajaxSendScore('selectTheme');
  1576. $.apps.changeAppsTheme(theme);
  1577. }
  1578. /**
  1579. * Select vision.
  1580. * @param {string} vision
  1581. */
  1582. function selectVision(vision)
  1583. {
  1584. $.get($.createLink('my', 'ajaxSwitchVision', 'vision=' + vision), function(result)
  1585. {
  1586. const response = JSON.parse(result);
  1587. if(response.result == 'fail')
  1588. {
  1589. return zui.Messager.error(response.message);
  1590. }
  1591. if(response.result == 'success' && response.load)
  1592. {
  1593. window.top.location.href = response.load;
  1594. }
  1595. });
  1596. }
  1597. /**
  1598. * 获取所有界面列表。
  1599. * @param {string} vision
  1600. * @returns {Array<{id: string, label: string, active: boolean}>
  1601. */
  1602. function getVisions(vision)
  1603. {
  1604. return window.userVisions.map(key => ({id: key, label: window.config.visions[key], active: key === window.config.vision}));
  1605. }
  1606. function fetchMessage(force, fetchUrl)
  1607. {
  1608. let $this = $('#messageBar');
  1609. let $dropmenu = $("#dropdownMessageMenu");
  1610. if(typeof(force) === 'undefined' || typeof(force) === 'object') force = false;
  1611. if(typeof(fetchUrl) === 'undefined') fetchUrl = $this.attr('data-fetcher');
  1612. setTimeout(function()
  1613. {
  1614. let dropdown = $this.zui('Dropdown');
  1615. let isOpen = true;
  1616. if(dropdown) isOpen = dropdown.shown;
  1617. if(!isOpen && !force) return;
  1618. $dropmenu.load(fetchUrl);
  1619. }, 100);
  1620. }
  1621. function waitDom(selector, func, times, interval)
  1622. {
  1623. var _times = times || 100; //100次
  1624. var _interval = interval || 50; //50毫秒每次
  1625. var _self = $(selector);
  1626. var _iIntervalID;
  1627. if(_self.length)
  1628. {
  1629. func && func.call(_self);
  1630. return;
  1631. }
  1632. _iIntervalID = setInterval(function()
  1633. {
  1634. if(!_times) clearInterval(_iIntervalID);
  1635. _times <= 0 || _times--;
  1636. _self = $(selector);
  1637. if(_self.length)
  1638. {
  1639. func && func.call(_self);
  1640. clearInterval(_iIntervalID);
  1641. }
  1642. }, _interval);
  1643. }
  1644. function setImageSize(image, maxWidth, maxHeight)
  1645. {
  1646. var $image = $(image);
  1647. if($image.parent().is('a')) return;
  1648. /* If not set maxWidth, set it auto. */
  1649. if(!maxWidth)
  1650. {
  1651. bodyWidth = $('body').width();
  1652. maxWidth = bodyWidth - 470; // The side bar's width is 336, and add some margins.
  1653. }
  1654. if(!maxHeight) maxHeight = top.window.innerHeight;
  1655. setTimeout(function()
  1656. {
  1657. let maxHeightStyle = $image.height() > 0 ? 'max-height:' + maxHeight + 'px' : '';
  1658. if(!document.getElementsByClassName('xxc-embed').length && $image.width() > 0 && $image.width() > maxWidth) $image.attr('width', maxWidth);
  1659. $image.wrap('<a href="' + $image.attr('src') + '" style="display:inline-block;position:relative;overflow:hidden;' + maxHeightStyle + '" target="_blank"></a>');
  1660. if($image.height() > 0 && $image.height() > maxHeight) $image.closest('a').append("<a href='###' class='showMoreImage row items-center justify-center h-7 secondary absolute bottom-0 w-full opacity-70 hover:opacity-100' onclick='showMoreImage(this)'>" + window.config.expand + " <i class='icon-angle-down'></i></a>");
  1661. }, 50);
  1662. }
  1663. function showMoreImage(obj)
  1664. {
  1665. $(obj).parents('a').css('max-height', 'none');
  1666. $(obj).remove();
  1667. }
  1668. function handleGlobalClick(e)
  1669. {
  1670. if(e.defaultPrevented) return;
  1671. if(isInAppTab) window.parent.$('body').trigger('click');
  1672. const $target = $(e.target);
  1673. if($target.closest('.not-open-url').length) return;
  1674. const $link = $target.closest('a,.open-url');
  1675. if(!$link.length || $link.hasClass('ajax-submit') || $link.attr('download') || $link.attr('data-on') || $link.attr('zui-on') || $link.attr('zui-toggle') || $link.attr('zui-command') || $link.hasClass('show-in-app') || $link.hasClass('not-open-url') || ($link.attr('target') || '')[0] === '_') return;
  1676. const href = $link.attr('href');
  1677. if($link.hasClass('disabled') || $link.prop('disabled'))
  1678. {
  1679. e.preventDefault();
  1680. return;
  1681. }
  1682. if(href && $link.is('a'))
  1683. {
  1684. if(href.startsWith('javascript:')) return;
  1685. if(/^https?:/.test(href) && !$link.data('app'))
  1686. {
  1687. e.preventDefault();
  1688. window.open(href, '_blank', 'noopener,noreferrer');
  1689. return;
  1690. }
  1691. }
  1692. const options = $link.dataset();
  1693. if(options.toggle && options.toggle !== 'tooltip' && !$link.hasClass('open-url')) return;
  1694. const url = options.url || href;
  1695. const $modal = $link.closest('.modal');
  1696. if(options.loadId)
  1697. {
  1698. options.target = options.loadId;
  1699. delete options.loadId;
  1700. }
  1701. if(url)
  1702. {
  1703. if(/^(https?|javascript):/.test(url) && !options.app) return;
  1704. if(url[0] === '#')
  1705. {
  1706. if(/firefox/i.test(navigator.userAgent)) e.preventDefault();
  1707. return;
  1708. }
  1709. }
  1710. if(!url && $link.is('a') && !options.back && !options.load) return;
  1711. if($modal.length)
  1712. {
  1713. if(!options.load && !url) return;
  1714. if(options.load === 'modal' && !options.target) options.target = $modal.attr('id');
  1715. if(options.load === 'table')
  1716. {
  1717. options.partial = true;
  1718. if(!options.url) options.url = $modal.data('zui.Modal').options.url;
  1719. }
  1720. if(options.closeModal) zui.Modal.hide(typeof options.closeModal === 'string' ? options.closeModal : $modal);
  1721. let app = options.app;
  1722. if(!options.app) options.app = $.apps.getAppCode(url, currentCode);
  1723. if(app === currentCode && options.load !== 'modal' && options.load !== 'table')
  1724. {
  1725. options.load === 'modal';
  1726. zui.Modal.query($modal).hide();
  1727. }
  1728. }
  1729. else
  1730. {
  1731. if(options.load === 'modal' && !options.target) delete options.load;
  1732. }
  1733. openUrl(url, options, e);
  1734. e.preventDefault();
  1735. }
  1736. function createSelector(name)
  1737. {
  1738. return name.replace(/([[\]])/g, '\\$1');
  1739. };
  1740. function showValidateMessage(message)
  1741. {
  1742. let $firstControl = null;
  1743. Object.entries(message).forEach(([name, msg]) => {
  1744. if (Array.isArray(msg)) {
  1745. msg = msg.join('');
  1746. }
  1747. const nameSelector = createSelector(name);
  1748. const $element = $('.form');
  1749. let $control = $element.find(`#${nameSelector}`);
  1750. if(!$control.length) $control = $element.find(`[name="${nameSelector}"]`);
  1751. if(!$control.length && !name.includes('[')) $control = $element.find(`[name="${nameSelector}[]"]`);
  1752. if($control.hasClass('pick-value')) $control = $control.closest('.pick');
  1753. $control.addClass('has-error');
  1754. const $group = $control.closest('.form-group,.form-batch-control');
  1755. if(!$group.length) return zui.Messager.show({content: msg, type: 'danger', className: 'bg-danger text-canvas gap-2 messager-success'});
  1756. if($group.length)
  1757. {
  1758. let $tip = $group.find(`#${nameSelector}Tip`);
  1759. if(!$tip.length)
  1760. {
  1761. $tip = $(`<div class="form-tip ajax-form-tip text-danger pre-line" id="${name}Tip"></div>`).appendTo($group);
  1762. }
  1763. $tip.empty().text(msg);
  1764. }
  1765. if(!$firstControl) $firstControl = $control;
  1766. });
  1767. if($firstControl) $firstControl[0]?.focus();
  1768. }
  1769. function enterWorkspace(code, url)
  1770. {
  1771. workspaceType = code;
  1772. $.cookie.set('workspace', code || currentCode, {expires: config.cookieLife, path: config.webRoot});
  1773. if(url) openUrl(url);
  1774. else setTimeout(reloadPage, 500);
  1775. }
  1776. function exitWorkspace()
  1777. {
  1778. workspaceType = '';
  1779. $.cookie.remove('workspace', {path: config.webRoot});
  1780. setTimeout(reloadPage, 500);
  1781. }
  1782. function updateWorkspaceUI(info)
  1783. {
  1784. $('body').toggleClass('in-workspace', !!info);
  1785. if(info)
  1786. {
  1787. const $dropmenu = $('#dropmenu').first();
  1788. info.name = $dropmenu.attr('data-text');
  1789. info.dropmenu = $dropmenu.attr('zui-create-dropmenu');
  1790. info.icon = $('#heading>.toolbar>.btn>.icon').attr('class').replace('icon icon-', '');
  1791. info.menuGroup = $('#navbar>.nav').attr('data-navbar-group');
  1792. }
  1793. workspaceType = info ? info.type : null;
  1794. $.apps.updateSpaceMenu && $.apps.updateSpaceMenu(info);
  1795. }
  1796. $.extend(window, {registerRender: registerRender, fetchContent: fetchContent, loadTable: loadTable, loadPage: loadPage, postAndLoadPage: postAndLoadPage, loadCurrentPage: loadCurrentPage, parseSelector: parseSelector, toggleLoading: toggleLoading, openUrl: openUrl, openPage: openPage, goBack: goBack, registerTimer: registerTimer, loadModal: loadModal, loadTarget: loadTarget, loadComponent: loadComponent, loadPartial: loadPartial, reloadPage: reloadPage, selectLang: selectLang, selectTheme: selectTheme, selectVision: selectVision, changeAppLang, changeAppTheme: changeAppTheme, waitDom: waitDom, fetchMessage: fetchMessage, setImageSize: setImageSize, showMoreImage: showMoreImage, autoLoad: autoLoad, loadForm: loadForm, showValidateMessage: showValidateMessage, getPageInfo: getPageInfo, getPerfData: getPerfData, applyFormData: applyFormData, zinCallbacks: zinCallbacks, registerZinCallback: registerZinCallback, getVisions: getVisions, enterWorkspace: enterWorkspace, exitWorkspace: exitWorkspace, updateWorkspaceUI: updateWorkspaceUI});
  1797. $.extend($.apps, {openUrl: openUrl, getAppUrl: () => currentAppUrl});
  1798. $.extend($, {ajaxSendScore: ajaxSendScore, selectLang: selectLang});
  1799. /* Transfer click event to parent */
  1800. $(document).on('click', handleGlobalClick).on('locate.zt', (_e, data) =>
  1801. {
  1802. if(!data) return;
  1803. if(data === true) return loadCurrentPage();
  1804. if(data === 'reload') return reloadPage();
  1805. if(typeof data === 'string')
  1806. {
  1807. if(data === 'table') return loadTable();
  1808. if(data === 'modal') return loadModal();
  1809. if(data === 'login')
  1810. {
  1811. window.top.location.href = $.createLink('user', 'login', 'referer=' + btoa(currentAppUrl));
  1812. return;
  1813. }
  1814. data = {url: data};
  1815. }
  1816. if(data.autoLoad) autoLoad(data.autoLoad);
  1817. if(data.confirm)
  1818. {
  1819. return zui.Modal.confirm(data.confirm).then(confirmed =>
  1820. {
  1821. if(confirmed) $(document).trigger('locate.zt', data.confirmed);
  1822. else $(document).trigger('locate.zt', data.cancelled || data.canceled);
  1823. });
  1824. }
  1825. if(data.alert)
  1826. {
  1827. return zui.Modal.alert(data.alert).then(function()
  1828. {
  1829. if(data.modal) loadModal(data.modal);
  1830. if(data.locate) openUrl(data.locate);
  1831. });
  1832. }
  1833. if(data.modal) return loadModal(data.modal);
  1834. if(data.load) return openUrl(data);
  1835. if(data.app) return openPage(data.url + (data.selector ? (' ' + data.selector) : ''), data.app);
  1836. loadPage(data);
  1837. });
  1838. if(!isInAppTab && !isIndexPage)
  1839. {
  1840. const initialState = {url: currentAppUrl, title: document.title};
  1841. window.history.pushState(initialState, initialState.title, initialState.url);
  1842. $(window).on('popstate', function(event)
  1843. {
  1844. const state = event.state;
  1845. if(DEBUG) showLog(state ? state.code : null, 'Popstate', state ? state.url : null, state);
  1846. if(state && state.url) openPage(state.url);
  1847. });
  1848. }
  1849. $(() =>
  1850. {
  1851. if($.apps.theme) changeAppTheme($.apps.theme);
  1852. if(isIndexPage) return;
  1853. initZinbar();
  1854. if(window.defaultAppUrl) loadPage(window.defaultAppUrl);
  1855. if(isInAppTab)
  1856. {
  1857. const frameElement = window.frameElement;
  1858. if(frameElement && parent.window.$) parent.window.$(frameElement).trigger('ready.app');
  1859. }
  1860. if(DEBUG)
  1861. {
  1862. if(!isInAppTab && !zui.store.get('Zinbar:hidden') && zui.dom.isVisible($('#navbar'))) loadCurrentPage();
  1863. if(zinbar)
  1864. {
  1865. $(document).on('inited.zt', (e) =>
  1866. {
  1867. if(e.target.parentElement.closest('[z-use]')) return;
  1868. const now = performance.now();
  1869. if(zinbar.waitZUI && (now - zinbar.waitZUI.time) <= 100)
  1870. {
  1871. zinbar.waitZUI.time = now;
  1872. updatePerfInfo(zinbar.waitZUI, 'renderEnd');
  1873. }
  1874. else
  1875. {
  1876. zinbar.waitZUI = null;
  1877. }
  1878. });
  1879. }
  1880. }
  1881. });
  1882. }());