|
20 | 20 | *
|
21 | 21 | * in addition to targetId, do we want to support specifying targetEl directly?
|
22 | 22 | *
|
| 23 | + * flag to see if user has already taken a tour? |
| 24 | + * |
23 | 25 | * http://daneden.me/animate/ for bounce animation
|
24 | 26 | *
|
25 | 27 | */
|
|
261 | 263 | };
|
262 | 264 |
|
263 | 265 | HopscotchBubble = function(opt) {
|
264 |
| - var prevBtnCallback, |
265 |
| - nextBtnCallback, |
266 |
| - currStep, |
| 266 | + var currStep, |
267 | 267 | isShowing = false,
|
268 | 268 |
|
269 | 269 | createButton = function(id, text) {
|
|
456 | 456 |
|
457 | 457 | // Attach click listeners
|
458 | 458 | utils.addClickListener(this.prevBtnEl, function(evt) {
|
459 |
| - if (prevBtnCallback) { |
460 |
| - prevBtnCallback(); |
461 |
| - } |
462 | 459 | window.hopscotch.prevStep();
|
463 | 460 | });
|
464 | 461 | utils.addClickListener(this.nextBtnEl, function(evt) {
|
465 |
| - if (nextBtnCallback) { |
466 |
| - nextBtnCallback(); |
467 |
| - } |
468 | 462 | window.hopscotch.nextStep();
|
469 | 463 | });
|
470 | 464 | utils.addClickListener(this.doneBtnEl, window.hopscotch.endTour);
|
|
512 | 506 | this.element.appendChild(this.arrowEl);
|
513 | 507 | };
|
514 | 508 |
|
515 |
| - this.renderStep = function(step, idx, isLast, callback) { |
| 509 | + this.renderStep = function(step, idx, subIdx, isLast, callback) { |
516 | 510 | var self = this,
|
517 | 511 | showNext = utils.valOrDefault(step.showNextButton, opt.showNextButton),
|
518 | 512 | showPrev = utils.valOrDefault(step.showPrevButton, opt.showPrevButton),
|
519 | 513 | bubbleWidth,
|
520 | 514 | bubblePadding;
|
521 | 515 |
|
522 |
| - currStep = step; |
523 |
| - |
524 | 516 | this.setTitle(step.title ? step.title : '');
|
525 | 517 | this.setContent(step.content ? step.content : '');
|
526 | 518 | this.setNum(idx);
|
527 | 519 |
|
528 |
| - this.showPrevButton(this.prevBtnEl && showPrev && idx > 0); |
| 520 | + this.showPrevButton(this.prevBtnEl && showPrev && (idx > 0 || subIdx > 0)); |
529 | 521 | this.showNextButton(this.nextBtnEl && showNext && !isLast);
|
530 | 522 | if (isLast) {
|
531 | 523 | utils.removeClass(this.doneBtnEl, 'hide');
|
|
555 | 547 | if (callback) { callback(); }
|
556 | 548 | }
|
557 | 549 |
|
558 |
| - // Set or clear new nav callbacks |
559 |
| - prevBtnCallback = step.onPrev; |
560 |
| - nextBtnCallback = step.onNext; |
561 |
| - |
562 | 550 | return this;
|
563 | 551 | };
|
564 | 552 |
|
|
680 | 668 | opt,
|
681 | 669 | currTour,
|
682 | 670 | currStepNum,
|
| 671 | + currSubstepNum, |
683 | 672 | cookieTourId,
|
684 | 673 | cookieTourStep,
|
| 674 | + cookieTourSubstep, |
685 | 675 |
|
686 | 676 | /**
|
687 | 677 | * getBubble
|
|
697 | 687 | },
|
698 | 688 |
|
699 | 689 | getCurrStep = function() {
|
700 |
| - return currTour.steps[currStepNum]; |
| 690 | + var step = currTour.steps[currStepNum]; |
| 691 | + |
| 692 | + return (step.length > 0) ? step[currSubstepNum] : step; |
| 693 | + }, |
| 694 | + |
| 695 | + /** |
| 696 | + * incrementStep |
| 697 | + * ============= |
| 698 | + * Sets current step num and substep num to the next step in the tour. |
| 699 | + * Returns true if successful, false if not. |
| 700 | + */ |
| 701 | + incrementStep = function() { |
| 702 | + var numSubsteps = currTour.steps[currStepNum].length; |
| 703 | + if (currSubstepNum < numSubsteps-1) { |
| 704 | + ++currSubstepNum; |
| 705 | + return true; |
| 706 | + } |
| 707 | + else if (currStepNum < currTour.steps.length-1) { |
| 708 | + ++currStepNum; |
| 709 | + currSubstepNum = (currStepNum.length > 0) ? 0 : undefined; |
| 710 | + return true; |
| 711 | + } |
| 712 | + return false; |
| 713 | + }, |
| 714 | + |
| 715 | + /** |
| 716 | + * decrementStep |
| 717 | + * ============= |
| 718 | + * Sets current step num and substep num to the previous step in the tour. |
| 719 | + * Returns true if successful, false if not. |
| 720 | + */ |
| 721 | + decrementStep = function() { |
| 722 | + var numPrevSubsteps; |
| 723 | + if (currSubstepNum > 0) { |
| 724 | + --currSubstepNum; |
| 725 | + return true; |
| 726 | + } |
| 727 | + else if (currStepNum > 0) { |
| 728 | + numPrevSubsteps = currTour.steps[--currStepNum].length; |
| 729 | + if (numPrevSubsteps) { |
| 730 | + currSubstepNum = numPrevSubsteps-1; |
| 731 | + } |
| 732 | + else { |
| 733 | + currSubstepNum = undefined; |
| 734 | + } |
| 735 | + return true; |
| 736 | + } |
| 737 | + return false; |
| 738 | + }, |
| 739 | + |
| 740 | + isInMultiPartStep = function() { |
| 741 | + return currTour.steps[currStepNum].length > 0; |
701 | 742 | },
|
702 | 743 |
|
703 | 744 | /**
|
|
807 | 848 | this.loadTour = function(tour) {
|
808 | 849 | var tmpOpt = {},
|
809 | 850 | bubble,
|
810 |
| - prop; |
| 851 | + prop, |
| 852 | + stepPair; |
811 | 853 | currTour = tour;
|
812 | 854 |
|
813 | 855 | // Set tour-specific configurations
|
|
823 | 865 | // Get existing tour state, if it exists.
|
824 | 866 | tourState = utils.getState(opt.cookieName);
|
825 | 867 | if (tourState) {
|
826 |
| - tourPair = tourState.split(':'); |
827 |
| - cookieTourId = tourPair[0]; // selecting tour is not supported by this framework. |
828 |
| - cookieTourStep = parseInt(tourPair[1], 10); |
| 868 | + tourPair = tourState.split(':'); |
| 869 | + cookieTourId = tourPair[0]; // selecting tour is not supported by this framework. |
| 870 | + cookieTourStep = tourPair[1]; |
| 871 | + cookieTourSubstep = undefined; |
| 872 | + stepPair = cookieTourStep.split('-'); |
| 873 | + |
| 874 | + if (stepPair.length > 1) { |
| 875 | + cookieTourStep = parseInt(stepPair[0], 10); |
| 876 | + cookieTourSubstep = parseInt(stepPair[1], 10); |
| 877 | + } |
| 878 | + else { |
| 879 | + cookieTourStep = parseInt(cookieTourStep, 10); |
| 880 | + } |
| 881 | + |
| 882 | + // Check for multipage flag |
829 | 883 | if (tourPair.length > 2 && tourPair[2] === 'mp') {
|
830 |
| - // multipage... increment tour step by 1 |
831 |
| - ++cookieTourStep; |
| 884 | + // Increment cookie step |
| 885 | + if (cookieTourSubstep && cookieTourSubstep < currTour.steps[cookieTourStep].length-1) { |
| 886 | + ++cookieTourSubstep; |
| 887 | + } |
| 888 | + else if (cookieTourStep < currTour.steps.length-1) { |
| 889 | + ++cookieTourStep; |
| 890 | + if (currTour.steps[cookieTourStep].length > 0) { |
| 891 | + cookieTourSubstep = 0; |
| 892 | + } |
| 893 | + else { |
| 894 | + cookieTourSubstep = undefined; |
| 895 | + } |
| 896 | + } |
832 | 897 | }
|
833 | 898 | }
|
834 | 899 |
|
|
840 | 905 | };
|
841 | 906 |
|
842 | 907 | this.startTour = function() {
|
843 |
| - var bubble; |
| 908 | + var bubble, |
| 909 | + step; |
844 | 910 |
|
845 | 911 | if (!currTour) {
|
846 | 912 | throw "Need to load a tour before you start it!";
|
|
853 | 919 |
|
854 | 920 | // Check if we are resuming state.
|
855 | 921 | if (currTour.id === cookieTourId && typeof cookieTourStep !== undefinedStr) {
|
856 |
| - currStepNum = cookieTourStep; |
857 |
| - if (!document.getElementById(currTour.steps[currStepNum].targetId)) { |
| 922 | + currStepNum = cookieTourStep; |
| 923 | + currSubstepNum = cookieTourSubstep; |
| 924 | + step = getCurrStep(); |
| 925 | + if (!document.getElementById(step.targetId)) { |
| 926 | + decrementStep(); |
| 927 | + step = getCurrStep(); |
858 | 928 | // May have just refreshed the page. Previous step should work. (but don't change cookie)
|
859 |
| - if (currStepNum <= 0 || !document.getElementById(currTour.steps[--currStepNum].targetId)) { |
| 929 | + if (!document.getElementById(step.targetId)) { |
860 | 930 | // Previous target doesn't exist either. The user may have just
|
861 | 931 | // clicked on a link that wasn't part of the tour. Let's just "end"
|
862 | 932 | // the tour and depend on the cookie to pick the user back up where
|
|
870 | 940 | currStepNum = 0;
|
871 | 941 | }
|
872 | 942 |
|
873 |
| - this.showStep(currStepNum); |
| 943 | + if (!currSubstepNum && isInMultiPartStep()) { |
| 944 | + // Multi-part step |
| 945 | + currSubstepNum = 0; |
| 946 | + } |
| 947 | + |
| 948 | + this.showStep(currStepNum, currSubstepNum); |
874 | 949 | bubble = getBubble().show();
|
875 | 950 |
|
876 | 951 | if (opt.animate) {
|
|
880 | 955 | return this;
|
881 | 956 | };
|
882 | 957 |
|
883 |
| - this.showStep = function(stepIdx) { |
884 |
| - var step = currTour.steps[stepIdx], |
885 |
| - numTourSteps = currTour.steps.length, |
| 958 | + this.showStep = function(stepIdx, substepIdx) { |
| 959 | + var tourSteps = currTour.steps, |
| 960 | + step = tourSteps[stepIdx], |
| 961 | + numTourSteps = tourSteps.length, |
886 | 962 | cookieVal = currTour.id + ':' + stepIdx,
|
887 |
| - bubble = getBubble(); |
| 963 | + bubble = getBubble(), |
| 964 | + isLast; |
888 | 965 |
|
889 |
| - if (!currTour) { |
890 |
| - throw "No tour currently selected!"; |
| 966 | + // Update bubble for current step |
| 967 | + currStepNum = stepIdx; |
| 968 | + currSubstepNum = substepIdx; |
| 969 | + |
| 970 | + if (typeof substepIdx !== undefinedStr) { |
| 971 | + step = step[substepIdx]; |
| 972 | + cookieVal += '-' + substepIdx; |
891 | 973 | }
|
892 | 974 |
|
893 |
| - // Update bubble for current step |
894 |
| - currStepNum = stepIdx; |
895 |
| - bubble.renderStep(step, stepIdx, (stepIdx === numTourSteps - 1), adjustWindowScroll); |
| 975 | + currStep = step; |
| 976 | + isLast = (stepIdx === numTourSteps - 1) || (substepIdx >= step.length - 1); |
| 977 | + bubble.renderStep(step, stepIdx, substepIdx, isLast, adjustWindowScroll); |
896 | 978 |
|
897 | 979 | if (step.multiPage) {
|
898 | 980 | cookieVal += ':mp';
|
899 | 981 | }
|
| 982 | + |
900 | 983 | utils.setState(opt.cookieName, cookieVal, 1);
|
901 | 984 | return this;
|
902 | 985 | };
|
903 | 986 |
|
904 | 987 | this.prevStep = function() {
|
905 |
| - if (currStepNum > 0) { |
906 |
| - this.showStep(--currStepNum); |
| 988 | + var step = getCurrStep(); |
| 989 | + |
| 990 | + if (step.onPrev) { |
| 991 | + step.onPrev(); |
| 992 | + } |
| 993 | + |
| 994 | + if (decrementStep()) { |
| 995 | + this.showStep(currStepNum, currSubstepNum); |
907 | 996 | }
|
908 | 997 | return this;
|
909 | 998 | };
|
910 | 999 |
|
911 | 1000 | this.nextStep = function() {
|
| 1001 | + var step = getCurrStep(); |
| 1002 | + |
| 1003 | + // invoke Next button callbacks |
912 | 1004 | if (opt.onNext) {
|
913 |
| - opt.onNext(getCurrStep(), currStepNum); |
| 1005 | + opt.onNext(step, currStepNum); |
914 | 1006 | }
|
915 |
| - if (currStepNum < currTour.steps.length-1) { |
916 |
| - this.showStep(++currStepNum); |
| 1007 | + if (step.onNext) { |
| 1008 | + step.onNext(); |
917 | 1009 | }
|
| 1010 | + |
| 1011 | + if (incrementStep()) { |
| 1012 | + this.showStep(currStepNum, currSubstepNum); |
| 1013 | + } |
| 1014 | + |
918 | 1015 | return this;
|
919 | 1016 | };
|
920 | 1017 |
|
|
924 | 1021 | * Cancels out of an active tour. No state is preserved.
|
925 | 1022 | */
|
926 | 1023 | this.endTour = function(clearCookie) {
|
927 |
| - var bubble = getBubble() |
928 |
| - clearCookie = utils.valOrDefault(clearCookie, true); |
| 1024 | + var bubble = getBubble(); |
| 1025 | + clearCookie = utils.valOrDefault(clearCookie, true); |
| 1026 | + currStepNum = 0; |
| 1027 | + currSubstepNum = 0; |
| 1028 | + cookieTourStep = undefined; |
| 1029 | + |
929 | 1030 | bubble.hide();
|
930 |
| - currStepNum = cookieTourStep = 0; |
931 | 1031 | if (clearCookie) {
|
932 | 1032 | utils.clearState(opt.cookieName);
|
933 | 1033 | }
|
|
940 | 1040 | * =========
|
941 | 1041 | * VALID OPTIONS INCLUDE...
|
942 | 1042 | * bubbleWidth: Number - Default bubble width. Defaults to 280.
|
943 |
| - * bubblePadding: Number - Default bubble padding. Defaults to 10. |
| 1043 | + * bubblePadding: Number - Default bubble padding. Defaults to 15. |
944 | 1044 | * bubbleBorder: Number - Default bubble border width. Defaults to 6.
|
945 | 1045 | * animate: Boolean - should the tour bubble animate between steps?
|
946 | 1046 | * Defaults to FALSE.
|
|
990 | 1090 | opt.showPrevButton = utils.valOrDefault(opt.showPrevButton, false);
|
991 | 1091 | opt.showNextButton = utils.valOrDefault(opt.showNextButton, true);
|
992 | 1092 | opt.bubbleWidth = utils.valOrDefault(opt.bubbleWidth, 280);
|
993 |
| - opt.bubblePadding = utils.valOrDefault(opt.bubblePadding, 10); |
| 1093 | + opt.bubblePadding = utils.valOrDefault(opt.bubblePadding, 15); |
994 | 1094 | opt.bubbleBorder = utils.valOrDefault(opt.bubbleBorder, 6);
|
995 | 1095 | opt.arrowWidth = utils.valOrDefault(opt.arrowWidth, 20);
|
996 | 1096 | opt.onNext = utils.valOrDefault(opt.onNext, null);
|
|
1014 | 1114 | };
|
1015 | 1115 |
|
1016 | 1116 | this.init(initOptions);
|
1017 |
| - |
1018 |
| - // DEBUG |
1019 |
| - // ===== |
1020 |
| - // REMOVE THIS LATER!!! |
1021 |
| - this.clearCookie = function() { |
1022 |
| - utils.clearState(opt.cookieName); |
1023 |
| - }; |
1024 | 1117 | };
|
1025 | 1118 |
|
1026 | 1119 | window.hopscotch = new Hopscotch();
|
|
0 commit comments