index.ui.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. let currentGuide = '';
  2. let currentTask = '';
  3. let currentStep = null;
  4. let popover = null;
  5. let inited = false;
  6. function showLog(name, step, moreTitles, moreInfos)
  7. {
  8. if(!config.debug) return;
  9. const titles = ['%c TUTORIAL '];
  10. const titleColors = ['color:#fff;font-weight:bold;background:#9c27b0'];
  11. if(step)
  12. {
  13. if(step.guide)
  14. {
  15. titles.push(`%c ${step.guide.title || step.guide.name} `);
  16. titleColors.push('background:rgba(156, 39, 176, 0.2);color:#9c27b0;');
  17. }
  18. if(step.task)
  19. {
  20. titles.push(`%c> ${step.task.title || step.task.name} `);
  21. titleColors.push('background:rgba(156, 39, 176, 0.2);color:#9c27b0;');
  22. }
  23. titles.push(`%c> ${step.index + 1}: ${step.title || step.name} `);
  24. titleColors.push('background:rgba(156, 39, 176, 0.2);color:#9c27b0;');
  25. }
  26. if(name)
  27. {
  28. titles.push(`%c ${name} `);
  29. titleColors.push('color:#9c27b0;font-weight:bold;');
  30. }
  31. if(!Array.isArray(moreTitles) && moreTitles) moreTitles = [moreTitles];
  32. if(Array.isArray(moreTitles) && typeof moreTitles[0] === 'string' && (moreTitles[0].startsWith('success:') || moreTitles[0].startsWith('error:')))
  33. {
  34. const message = moreTitles.shift();
  35. const [type, content] = message.split(':', 2);
  36. titles.push(`%c ${content} `);
  37. titleColors.push(`color:${type === 'error' ? '#f56c6c' : '#67c23a'};`);
  38. }
  39. console.groupCollapsed(titles.join(''), ...titleColors, ...(moreTitles || []));
  40. if(step) console.trace('step', step);
  41. if(moreInfos)
  42. {
  43. if($.isPlainObject(moreInfos)) Object.keys(moreInfos).forEach((infoName) => console.log(infoName, moreInfos[infoName]));
  44. else console.log(moreInfos);
  45. }
  46. console.groupEnd();
  47. }
  48. if(config.debug) showLog('Guides data', null, null, {guides});
  49. const stepPresenters =
  50. {
  51. openApp: function(step)
  52. {
  53. const scope = getStepScope(step);
  54. const $menuNav = scope.$('#menuNav');
  55. if(!$menuNav.data('tutorial.checkOpenAppStep'))
  56. {
  57. const checkOpenAppStep = (event, info) =>
  58. {
  59. if(!currentStep || currentStep.type !== 'openApp') return;
  60. if((event.type === 'shown' || event.type === 'hidden') && info[0].options.target !== '#menuMoreList') return;
  61. stepPresenters.openApp(currentStep);
  62. };
  63. scope.$(scope).on('resize', checkOpenAppStep);
  64. scope.$('#menuMoreBtn').on('shown hidden', checkOpenAppStep);
  65. $menuNav.append([
  66. '<style>',
  67. '.tutorial-not-open-app #menuNav .nav > li > a {pointer-events:none!important;}',
  68. '.tutorial-not-open-app #menuNav .nav > li > a.active, .tutorial-not-open-app #menuNav .nav > li > a#menuMoreBtn {pointer-events:auto!important;}',
  69. '</style>'
  70. ].join('\n'));
  71. $menuNav.data('tutorial.checkOpenAppStep', true);
  72. }
  73. const $menuMainNav = $menuNav.find('#menuMainNav');
  74. const $appNav = $menuMainNav.children(`li[data-app="${step.app}"]`);
  75. let $targetNav = $appNav;
  76. if($appNav.hasClass('hidden'))
  77. {
  78. const $menuMoreList = $menuNav.find('#menuMoreList.in');
  79. if($menuMoreList.length) $targetNav = $menuMoreList.children(`li[data-app="${step.app}"]`);
  80. else $targetNav = $menuNav.find('#menuMoreBtn');
  81. }
  82. const popoverOptions = {placement: 'right'};
  83. if($targetNav.is('#menuMoreBtn'))
  84. {
  85. $.extend(popoverOptions, {title: null, footer: undefined, content: lang.clickAndOpenIt.replace('%s', $targetNav.text().trim()).replace('%s', $appNav.text().trim()), notFinalTarget: true});
  86. }
  87. else
  88. {
  89. $targetNav = $targetNav.find('a');
  90. }
  91. if (!step.closedApp)
  92. {
  93. scope.$.apps.closeApp(step.app);
  94. step.closedApp = true;
  95. }
  96. return highlightStepTarget($targetNav, step, popoverOptions);
  97. },
  98. click: function(step)
  99. {
  100. return highlightStepTarget((scope) => scope.$(step.target), step, {placement: 'bottom'});
  101. },
  102. clickNavbar: function(step)
  103. {
  104. return highlightStepTarget((scope) => scope.$('.#'.includes(step.target) ? step.target : `#navbar>.nav>.item>a[data-id="${step.target}"]`), step);
  105. },
  106. clickMainNavbar: function(step)
  107. {
  108. return highlightStepTarget((scope) => scope.$('.#'.includes(step.target) ? step.target : `#mainNavbar .nav>.item>a[data-id="${step.target}"]`), step);
  109. },
  110. form: function(step)
  111. {
  112. return highlightStepTarget((scope) => {
  113. const $target = scope.$(step.target || 'form');
  114. /* Check form. */
  115. const $form = getStepForm(scope, $target);
  116. if(!$form || !$form.length) console.error(`[TUTORIAL] Cannot find form for step "${step.guide.name} > ${step.task.name} > ${step.title}"`, step);
  117. $form.attr('zui-tutorial-step', step.id);
  118. $form.find('.open-url,[data-back],[data-load],.ajax-submit').addClass('pointer-events-none');
  119. step.$form = $form;
  120. const $panel = $target.closest('.panel');
  121. return $panel.length ? $panel : $target;
  122. }, step, (!step.popover || !step.popover.placement) ? {placement: 'right'} : null);
  123. },
  124. saveForm: function(step)
  125. {
  126. return highlightStepTarget((scope) => {
  127. const $target = scope.$(step.target);
  128. /* Check form. */
  129. const $form = getStepForm(scope, $target);
  130. if(!$form || !$form.length) console.error(`[TUTORIAL] Cannot find form for step "${step.guide.name} > ${step.task.name} > ${step.title}"`, step);
  131. $form.attr('zui-tutorial-step', step.id);
  132. step.$form = $form;
  133. const $saveBtn = $form.find('[type="submit"]');
  134. return $saveBtn.length ? $saveBtn : $target;
  135. }, step);
  136. },
  137. selectRow: function(step)
  138. {
  139. return highlightStepTarget((scope) => {
  140. const $target = scope.$(step.target || '.dtable');
  141. let $table = $target.closest('[z-use-dtable]');
  142. if(!$table.length) $table = $target.find('[z-use-dtable]');
  143. if(!$table.length) $table = scope.$('[z-use-dtable]');
  144. if(!$table.length) console.error(`[TUTORIAL] Cannot find table for step "${step.guide.name} > ${step.task.name} > ${step.title}"`, step);
  145. step.$table = $table;
  146. if(!$table.data('tutorial.selectRowBinding'))
  147. {
  148. $table.data('tutorial.selectRowBinding', true);
  149. const dtable = $table.zui('dtable');
  150. dtable.setOptions({onCheckChange: function()
  151. {
  152. if(!this.getChecks().length) return;
  153. activeNextStep(step);
  154. }});
  155. }
  156. return $target;
  157. }, step);
  158. }
  159. };
  160. function toggleIframeMask(toggle)
  161. {
  162. $('body').toggleClass('tutorial-has-mask', toggle);
  163. }
  164. function getStepForm(scope, $target)
  165. {
  166. let $form = $target.closest('form');
  167. if(!$form.length) $form = $target.find('form');
  168. if(!$form.length) $form = scope.$('form');
  169. if(!$form.data('tutorial.formBinding'))
  170. {
  171. $form.data('tutorial.formBinding', true);
  172. const ajaxForm = $form.zui('ajaxForm');
  173. if(!ajaxForm) return console.error(`[TUTORIAL] Cannot find ajaxForm for step "${currentStep.guide.name} > ${currentStep.task.name} > ${currentStep.title}"`, currentStep);
  174. ajaxForm.setOptions({beforeSubmit: () =>
  175. {
  176. if(currentStep.type === 'form')
  177. {
  178. const nextStep = currentStep.task.steps[currentStep.index + 1];
  179. activeNextStep((nextStep && nextStep.type === 'saveForm') ? nextStep : currentStep);
  180. }
  181. return false;
  182. }});
  183. }
  184. return $form;
  185. }
  186. function destroyPopover(callback, scope)
  187. {
  188. if(config.debug) showLog('Destroy popover', currentStep, popover ? `${popover.gid}/${popover.options.key}` : 'no popover', {popover, callback, scope});
  189. if(!popover)
  190. {
  191. if(scope && scope.$)
  192. {
  193. scope.$('.tutorial-hl').removeClass('tutorial-hl');
  194. scope.$('.tutorial-light-box').addClass('opacity-0');
  195. }
  196. if(callback) callback();
  197. return;
  198. };
  199. const $trigger = $(popover.trigger);
  200. if($trigger.length)
  201. {
  202. $trigger.removeClass('tutorial-hl');
  203. $trigger.closest('body').find('.tutorial-light-box').addClass('opacity-0');
  204. }
  205. if(callback) setTimeout(callback, popover.shown ? 300 : 200);
  206. popover.hide(true);
  207. popover = null;
  208. }
  209. function highlightStepTarget($target, step, popoverOptions)
  210. {
  211. if(config.debug) showLog('Highlight', step, popover ? `${popover.gid}/${popover.options.key}` : 'no popover', {$target, popover, popoverOptions});
  212. if(popover)
  213. {
  214. if(popover.options.key === step.id)
  215. {
  216. const target = (typeof $target === 'function' ? $target(getStepScope(step)) : $target)[0];
  217. if(target === popover.trigger) return;
  218. }
  219. return destroyPopover(() => highlightStepTarget($target, step, popoverOptions));
  220. }
  221. ensureStepScope(step, (scope) =>
  222. {
  223. if(step !== currentStep) return;
  224. const $originTarget = $target;
  225. if(typeof $target === 'function') $target = $target(scope);
  226. $target = $target.first();
  227. if(!$target.length)
  228. {
  229. if(!step.waitedTarget || step.waitedTarget < 10)
  230. {
  231. step.waitedTarget = (step.waitedTarget || 0) + 1;
  232. return setTimeout(() => highlightStepTarget($originTarget, step, popoverOptions), 500);
  233. }
  234. return console.error(`[TUTORIAL] Cannot find target for step "${step.guide.title || step.guide.name} > ${step.task.title || step.task.name} > ${step.title}"`, step);
  235. }
  236. if($target.is('[data-toggle="dropdown"]'))
  237. {
  238. if($target.is('[data-trigger="hover"]')) $target.attr('data-trigger', 'click');
  239. const dropdown = $target.zui('dropdown');
  240. if(dropdown && dropdown.options.trigger === 'hover') dropdown.destroy();
  241. }
  242. const isLastStep = step.index === step.task.steps.length - 1;
  243. popoverOptions = $.extend(
  244. {
  245. $optionsFromDataset: false,
  246. key : step.id,
  247. title : step.title,
  248. strategy : 'fixed',
  249. show : true,
  250. limitInScreen : true,
  251. mask : false,
  252. trigger : 'manual',
  253. destroyOnHide : true,
  254. closeBtn : false,
  255. content : step.desc,
  256. contentClass : 'popover-content px-4 whitespace-pre-line',
  257. className : 'tutorial-popover rounded-md',
  258. titleClass : 'popover-title text-lg pl-1',
  259. minWidth : 280,
  260. maxWidth : 400,
  261. headingClass : 'popover-heading bg-transparent',
  262. elementShowClass: 'with-popover-show tutorial-hl',
  263. destroyOnHide : true,
  264. shift : true,
  265. hideOthers : false,
  266. hideNewOnHide : false,
  267. offset : 32,
  268. footer:
  269. {
  270. component: 'toolbar',
  271. props:
  272. {
  273. className: 'py-3 px-4 gap-3',
  274. items:
  275. [
  276. {component: 'span', children: `${step.index + 1}/${step.task.steps.length}`, className: 'flex-auto'},
  277. isLastStep ? null : {type: 'default', text: lang.exitStep, onClick: () => unactiveTask()},
  278. {type: 'primary', text: isLastStep ? lang.finish : lang.nextStep, onClick: () => goToNextStep(step)},
  279. ].filter(Boolean)
  280. }
  281. },
  282. onHide: function()
  283. {
  284. if(this.options.key !== step.id) return;
  285. $(this.trigger).closest('body').find('.tutorial-light-box').addClass('opacity-0');
  286. },
  287. onLayout: function(info)
  288. {
  289. const $trigger = $(info.trigger);
  290. const $body = $trigger.closest('body');
  291. const rect = info.trigger.getBoundingClientRect();
  292. let $lightElement = $body.find('.tutorial-light-box');
  293. if(!$lightElement.length) $lightElement = $('<div class="tutorial-light-box fixed pointer-events-none rounded" style="box-shadow: 0 0 10px rgba(0, 0, 0, 0.4), 0 0 0 9999px rgba(0, 0, 0, 0.4); z-index: 1690; transition: opacity .5s;"><div class="is-left pointer-events-auto absolute top-0 bottom-0"></div><div class="is-top pointer-events-auto absolute"></div><div class="is-right pointer-events-auto absolute top-0 bottom-0 left-full"></div><div class="is-bottom pointer-events-auto absolute top-full"></div><style>.tutorial-light-box.opacity-0>div{pointer-events:none!important}</style></div>').appendTo($body);
  294. $lightElement.removeClass('opacity-0').css(
  295. {
  296. top : rect.top,
  297. left : rect.left,
  298. width : rect.width,
  299. height : rect.height,
  300. borderRadius: $trigger.css('borderRadius'),
  301. zIndex : this._zIndex - 1
  302. }).appendTo($body);
  303. $lightElement.find('.is-left').css({left: -rect.left, width: Math.round(rect.left)});
  304. $lightElement.find('.is-right').css({width: Math.round(window.innerWidth - rect.right)});
  305. $lightElement.find('.is-top').css({top: -rect.top, height: Math.round(rect.top), left: -rect.left, right: rect.right - window.innerWidth});
  306. $lightElement.find('.is-bottom').css({height: Math.round(window.innerHeight - rect.bottom), left: -rect.left, right: rect.right - window.innerWidth});
  307. }
  308. }, step.popover, popoverOptions);
  309. if(popoverOptions.title === null)
  310. {
  311. const text = $target.text().trim();
  312. if(['openApp'].includes(step.type)) popoverOptions.title = lang.clickTipFormat.replace('%s', text);
  313. }
  314. step.$target = $target;
  315. popover = new scope.zui.Popover($target, popoverOptions);
  316. if(!popoverOptions.notFinalTarget) $target.attr('zui-tutorial-step', step.id);
  317. $target.scrollIntoView();
  318. if(config.debug) showLog('Show popover', step, `${popover.gid}/${popover.options.key}`, {$target, popover, popoverOptions});
  319. const $doc = scope.$(scope.document);
  320. if($doc.data('tutorial.checkBinding')) return;
  321. $doc.data('tutorial.checkBinding', true).on('click', '[zui-tutorial-step]', function(event, info)
  322. {
  323. if(!currentStep || currentStep.checkType !== event.type) return;
  324. const stepID = this.getAttribute('zui-tutorial-step');
  325. if(stepID !== currentStep.id) return;
  326. if(config.debug) showLog(`Step target ${event.type}`, currentStep, null, {stepID, event});
  327. activeNextStep();
  328. });
  329. $doc.on('pageload.app', function()
  330. {
  331. presentStep();
  332. });
  333. if(scope.zui.Ajax.globalBeforeSends)
  334. {
  335. scope.zui.Ajax.globalBeforeSends.push(function(options)
  336. {
  337. options.headers['X-ZIN-Tutorial'] = getCurrentStepID();
  338. });
  339. }
  340. });
  341. }
  342. function updateTaskUI(task, change)
  343. {
  344. if(change) $.extend(task, change);
  345. const guideName = task.guide.name;
  346. const taskName = task.name;
  347. const $guide = $('#tutorialTabs').find(`.tutorial-guide[data-name="${guideName}"]`);
  348. if(!$guide.length) return;
  349. const $task = $guide.find(`.tutorial-task[data-name="${taskName}"]`);
  350. if(!$task.length) return;
  351. $task.attr('data-status', task.status).toggleClass('active', !!task.active);
  352. const $steps = $task.find('.tutorial-step').removeClass('active');
  353. if(task.currentStepIndex !== undefined) $steps.filter(`[data-step="${task.currentStepIndex}"]`).addClass('active');
  354. if(task.active)
  355. {
  356. const $tabToggle = $(`#tutorialTabs .tabs-nav>.nav-item[data-key="${task.guide.type}"]>a`);
  357. if($tabToggle.length && !$tabToggle.hasClass('active')) setTimeout(() => $tabToggle[0].click(), 200);
  358. $task.scrollIntoView();
  359. }
  360. const url = $.createLink('tutorial', 'index', `referer=&guide=${guideName}&task=${taskName}`);
  361. const prevState = window.history.state;
  362. if(prevState.url !== url) window.history.replaceState({url}, '', url);
  363. return true;
  364. }
  365. function getNextTask(task)
  366. {
  367. const guideTaskNames = Object.keys(task.guide.tasks);
  368. if(guideTaskNames.length === task.index + 1) return;
  369. for(let i = 0; i < guideTaskNames.length; ++i)
  370. {
  371. const name = guideTaskNames[i];
  372. const thisTask = task.guide.tasks[name];
  373. if(thisTask.index === task.index + 1) return thisTask;
  374. }
  375. }
  376. function getNextGuide(guide)
  377. {
  378. const guideNames = Object.keys(guides);
  379. if(guideNames.length === guide.index + 1) return;
  380. for(let i = 0; i < guideNames.length; ++i)
  381. {
  382. const name = guideNames[i];
  383. const thisGuide = guides[name];
  384. if(thisGuide.index === guide.index + 1) return thisGuide;
  385. }
  386. }
  387. function activeNextStep(step)
  388. {
  389. step = step || currentStep;
  390. if(!step) return;
  391. formatStep(step);
  392. if(config.debug) showLog('Active next step', step);
  393. if(step.type === 'saveForm' && step.$form) step.$form.find('.open-url,[data-back],[data-load],.ajax-submit').addClass('pointer-events-none');
  394. if(step.endUrl) openApp(step.endUrl, step.app);
  395. destroyPopover(() =>
  396. {
  397. if(step.task.steps.length - 1 === step.index)
  398. {
  399. updateTaskUI(step.task, {status: 'done', active: false});
  400. const nextTask = getNextTask(step.task);
  401. const nextGuide = nextTask ? null : getNextGuide(step.guide);
  402. const nextGuideTask = (nextGuide && nextGuide.taskList.length) ? nextGuide.tasks[nextGuide.taskList[0]] : null;
  403. const actions = [];
  404. if(nextTask) actions.push({type: 'confirm', btnType: 'primary', text: `${lang.nextTask}${lang.colon}${nextTask.title}`, onClick: () => activeTask(step.guide.name, nextTask.name)}, {key: 'cancel', onClick: () => unactiveTask()});
  405. else if(nextGuideTask) actions.push({type: 'confirm', btnType: 'primary', text: `${lang.nextGuide}${lang.colon}${nextGuide.title}`, onClick: () => activeTask(nextGuide.name, nextGuideTask.name)}, {key: 'cancel', onClick: () => unactiveTask()});
  406. else actions.push({type: 'confirm', btnType: 'primary', text: lang.finish, onClick: () => unactiveTask()});
  407. zui.Modal.alert(
  408. {
  409. content: lang.congratulateTask.replace('<span class="task-name-current"></span>', step.task.title),
  410. actions: actions
  411. });
  412. return;
  413. }
  414. activeTaskStep(step.guide.name, step.task.name, step.index + 1);
  415. });
  416. }
  417. function goToNextStep(step)
  418. {
  419. step = step || currentStep;
  420. if(!step) return;
  421. if(step.checkType === 'click')
  422. {
  423. const $target = getStepScope(step).$(`[zui-tutorial-step="${step.id}"]`);
  424. if($target.length) return $target.trigger('click', 'skipCheckStep');
  425. }
  426. else if(step.type === 'saveForm' && step.$form)
  427. {
  428. step.$form[0].submit();
  429. } else if(step.type === 'openApp')
  430. {
  431. step.closedApp = false;
  432. }
  433. setTimeout(() => activeNextStep(step), 200);
  434. }
  435. function toggleActiveTarget(type, name, toggle)
  436. {
  437. if(!name) return false;
  438. const $target = $('#tutorialTabs').find(`.tutorial-${type}[data-name="${name}"]`);
  439. if(!$target.length) return;
  440. $target.toggleClass('active', toggle);
  441. $target.closest(`.tutorial-${type}-list`).toggleClass(`has-active-${type}`, toggle);
  442. return true;
  443. }
  444. function unactiveTask()
  445. {
  446. if(currentStep) destroyPopover(null, getStepScope(currentStep));
  447. toggleActiveTarget('task', currentTask, false);
  448. currentTask = '';
  449. currentStep = null;
  450. toggleIframeMask(true);
  451. }
  452. function getHomeScope()
  453. {
  454. return $('#iframePage')[0].contentWindow;
  455. }
  456. function getStepScope(step)
  457. {
  458. if(step.scope && step.scope.name) return step.scope;
  459. step.scope = null;
  460. const homeScope = getHomeScope();
  461. if(step.type === 'openApp')
  462. {
  463. step.scope = homeScope;
  464. }
  465. else if(homeScope.$ && homeScope.$.apps)
  466. {
  467. const openedApp = step.app ? homeScope.$.apps.openedApps[step.app] : homeScope.$.apps.getLastApp();
  468. if(openedApp) step.scope = openedApp.iframe.contentWindow;
  469. }
  470. return step.scope;
  471. }
  472. function ensureStepScope(step, callback)
  473. {
  474. const scope = getStepScope(step);
  475. const waitStepScope = (reason) =>
  476. {
  477. if(config.debug) showLog(`Wait step scope: ${reason}`, step, null, {scope});
  478. if(step.waitScopeTimer) clearTimeout(step.waitScopeTimer);
  479. step.waitScopeTimer = setTimeout(() =>
  480. {
  481. delete step.waitScopeTimer;
  482. ensureStepScope(step, callback);
  483. }, 500);
  484. };
  485. if(!scope || !scope.$) return waitStepScope('no scope');
  486. if(scope.name !== 'iframePage')
  487. {
  488. const $body = scope.$('body');
  489. if($body.hasClass('loading-page')) return waitStepScope('page is loading');
  490. const scopePage = ($body.attr('data-page') || '').toLowerCase();
  491. if(!scopePage) return waitStepScope('page is not ready');
  492. const scopePageRaw = ($body.attr('data-page-raw') || '').toLowerCase();
  493. const stepPage = (step.page || '').toLowerCase();
  494. if(stepPage && stepPage !== scopePage && stepPage !== scopePageRaw)
  495. {
  496. destroyPopover(null, scope);
  497. if(config.debug) showLog(`Page scope not match: ${stepPage}, current page is ${scopePage}`, step, null, {scope});
  498. return;
  499. }
  500. }
  501. if(step.waitScopeTimer) clearTimeout(step.waitScopeTimer);
  502. step.waitScopeTimer = setTimeout(() =>
  503. {
  504. delete step.waitScopeTimer;
  505. callback(scope);
  506. }, 200);
  507. }
  508. function openApp(url, app)
  509. {
  510. if(url.includes('#app='))
  511. {
  512. const [urlPart, appPart] = url.split('#app=', 2);
  513. url = urlPart;
  514. app = appPart;
  515. }
  516. const homeScope = getHomeScope();
  517. if(!homeScope.$ || !homeScope.$.apps)
  518. {
  519. setTimeout(() => openApp(url, app), 200);
  520. return;
  521. }
  522. if(Array.isArray(url)) url = $.createLink.apply(null, url);
  523. const openedApp = homeScope.$.apps.open(url, app, {forceReload: true});
  524. if(openedApp && openedApp.iframe.contentWindow.$)
  525. {
  526. openedApp.iframe.contentWindow.$('body').addClass('loading-page');
  527. }
  528. }
  529. function formatStep(step, guideName, taskName, stepIndex)
  530. {
  531. if(step.id) return step;
  532. guideName = guideName || step.guideName;
  533. taskName = taskName || step.taskName;
  534. stepIndex = typeof stepIndex !== 'number' ? step.index : stepIndex;
  535. const guide = guides[guideName];
  536. const task = guide.tasks[taskName];
  537. step.id = `${guideName}-${taskName}-${stepIndex}`;
  538. step.guide = guide;
  539. step.task = task;
  540. step.index = stepIndex;
  541. step.isLast = stepIndex === task.steps.length - 1;
  542. step.checkType = step.type === 'form' ? 'change' : 'click';
  543. if(step.type === 'openApp' && !task.app) task.app = step.app;
  544. if(!step.app && stepIndex) step.app = task.steps[stepIndex - 1].app;
  545. if(!step.app) step.app = task.app || guide.app;
  546. if(!step.app)
  547. {
  548. const taskNames = Object.keys(guide.tasks);
  549. for(let i = 0; i < taskNames.length; i++)
  550. {
  551. const thisTask = guide.tasks[taskNames[i]];
  552. if(thisTask.app && thisTask.index < task.index)
  553. {
  554. step.app = thisTask.app;
  555. break;
  556. }
  557. }
  558. }
  559. return step;
  560. }
  561. function presentStep(step)
  562. {
  563. step = step || currentStep;
  564. if(!step) return;
  565. const presenter = stepPresenters[step.type];
  566. if(config.debug) showLog('Present step', step, null, {presenter});
  567. step.waitedTarget = false;
  568. if(presenter) presenter(step);
  569. const homeScope = getHomeScope();
  570. homeScope.$('body').toggleClass('tutorial-not-open-app', step.type !== 'openApp');
  571. if(inited) return;
  572. homeScope.$('#appsBar').css('pointer-events', 'none');
  573. inited = true;
  574. }
  575. function activeTaskStep(guideName, taskName, stepIndex)
  576. {
  577. const guide = guides[guideName];
  578. const task = guide.tasks[taskName];
  579. const step = task.steps[stepIndex];
  580. if(currentStep === step) return;
  581. task.guide = guide;
  582. if(!updateTaskUI(task, {active: true, status: 'doing', currentStepIndex: stepIndex})) return;
  583. formatStep(step, guideName, taskName);
  584. if(step.url) openApp(step.url, step.app);
  585. else if(stepIndex === 0 && task.startUrl) openApp(step.task.startUrl, step.task.app);
  586. currentStep = step;
  587. if(config.debug) showLog('Active step', step);
  588. ensureStepScope(step, () => presentStep(step));
  589. }
  590. function activeTask(guideName, taskName, stepIndex)
  591. {
  592. if(stepIndex === undefined) stepIndex = 0;
  593. if(currentGuide !== guideName) activeGuide(guideName);
  594. if(currentTask && currentTask !== taskName) unactiveTask();
  595. if(currentTask !== taskName)
  596. {
  597. currentTask = taskName;
  598. toggleActiveTarget('task', currentTask, true);
  599. }
  600. toggleIframeMask(false);
  601. activeTaskStep(guideName, taskName, stepIndex);
  602. }
  603. function unactiveGuide()
  604. {
  605. if(!toggleActiveTarget('guide', currentGuide, false)) return;
  606. if(currentTask)
  607. {
  608. const guide = guides[currentGuide];
  609. if(guide && guide.tasks[currentTask]) unactiveTask();
  610. }
  611. currentGuide = '';
  612. }
  613. function activeGuide(guideName)
  614. {
  615. if(currentGuide && currentGuide !== guideName) unactiveGuide();
  616. if(currentGuide !== guideName)
  617. {
  618. currentGuide = guideName;
  619. toggleActiveTarget('guide', currentGuide, true);
  620. }
  621. }
  622. window.activeTask = activeTask;
  623. window.getCurrentStepID = function()
  624. {
  625. return currentStep ? currentStep.id : '';
  626. };
  627. window.handleClickGuide = function(event)
  628. {
  629. const guideName = $(event.target).closest('.tutorial-guide').data('name');
  630. if(guideName === currentGuide) unactiveGuide();
  631. else activeGuide(guideName);
  632. };
  633. window.handleClickTask = function(event)
  634. {
  635. const $task = $(event.target).closest('.tutorial-task');
  636. const taskName = $task.data('name');
  637. const guideName = $task.closest('.tutorial-guide').data('name');
  638. activeTask(guideName, taskName);
  639. };
  640. toggleIframeMask(true);