diff --git a/README.md b/README.md index 0b9aca5..0575c30 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,21 @@ EnjoyHint **EnjoyHint** is a web-tool that provides the simplest way to create interactive tutorials and hints for your site or web-application. It can also be used to highlight and sign application elements. EnjoyHint is free software distributed under the terms of MIT license. - + +#### Improvements +Comparing to the original library this version was modified to support more advanced scenarios: + +##### Added features +* Pause tour until specified element becomes visible or event is triggered +* Prevent user interaction within highlighted element +* Ability to specify which container is being scrolled + +##### Fixed Issues +* Next button not showing with 'click' and 'custom' events +* OnSkip not called when skipping steps programmatically +* Several steps can be skipped at once if triggered event is duplicated + + #### Demo * [TODO app demo](http://xbsoftware.github.io/enjoyhint/) ([downloadable package](http://xbsoftware.github.io/enjoyhint/enjoyhint_todo_demo.zip)) * [A small guide on EnjoyHint](http://xbsoftware.github.io/enjoyhint/example1.html) @@ -164,7 +178,18 @@ var enjoyhint_script_steps = [ #### Release notes -##### v.4 +##### v.4.1.0 +Improvements: +* Disable user interaction within highlighted area (`preventEvents` step property) +* Pause and hide overlay until element becomes visible or event is triggered (`pausedUntil` step property) +* Specify which container should be scrolled (`elementToScroll` option) + +Bug Fixes: +* Fixed Next button not showing +* Fixed skippind steps when event is duplicated +* Fixed OnSkip method not called when skipping steps programmatically + +##### v.4.x * Fixed label position bugs * Fixed arrow position bugs diff --git a/bower.json b/bower.json index 1fdf8b7..5027417 100644 --- a/bower.json +++ b/bower.json @@ -4,7 +4,7 @@ "enjoyhint.js", "enjoyhint.css" ], - "version": "4.0.1", + "version": "4.1.0", "homepage": "https://github.com/xbsoftware/enjoyhint", "authors": [ "XB Software" diff --git a/enjoyhint.js b/enjoyhint.js index a7dea2d..b70f817 100644 --- a/enjoyhint.js +++ b/enjoyhint.js @@ -23,6 +23,7 @@ var SHAPE_BACKGROUND_COLOR = _options.backgroundColor || "rgba(0,0,0,0.6)"; var body = "body"; // TODO: Is it possible case when we need to define enjoyhint somewhere else? + var elementAvailableEventName = "enjoyhint-element-available"; var defaults = { onStart: function() {}, @@ -30,8 +31,10 @@ onEnd: function() {}, onSkip: function() {}, - - onNext: function() {} + + onNext: function () { }, + + elementToScroll: document.body }; var options = $.extend(defaults, _options); @@ -62,7 +65,8 @@ options.onSkip(); skipAll(); }, - fill: SHAPE_BACKGROUND_COLOR + fill: SHAPE_BACKGROUND_COLOR, + elementToScroll: options.elementToScroll }); }; @@ -97,7 +101,7 @@ $body.enjoyhint("hide_skip"); }; - var stepAction = function() { + var stepAction = function(unpause) { if (!(data && data[current_step])) { $body.enjoyhint("hide"); options.onEnd(); @@ -113,9 +117,63 @@ $enjoyhint.removeClass("enjoyhint-step-" + (current_step + 1)); $enjoyhint.removeClass("enjoyhint-step-" + (current_step + 2)); $enjoyhint.addClass("enjoyhint-step-" + (current_step + 1)); - + var step_data = data[current_step]; + $body.off(elementAvailableEventName); + + //loops waiting until specified element becomes visible + var waitUntilAvailable = function (selector, interval) { + if (interval == null) + interval = 150; + + var triggerIfAvailable = function () { + if ($(selector).is(":visible")) { + $body.trigger(elementAvailableEventName); + } + else { + setTimeout(triggerIfAvailable, interval) + } + }; + + setTimeout(triggerIfAvailable, 0); + } + + //if pausedUntil was specified, hide current overlay and wait until specified event occurs + if (!unpause && step_data.pausedUntil != null && step_data.pausedUntil.event != null) { + //hide current overlay during waiting time + $body.enjoyhint("hide"); + + //if 'available' event was chosen wait for the custom event, which is triggered when the element becomes visible + if (step_data.pausedUntil.event === 'available') { + $body.on(elementAvailableEventName, function () { + stepAction(true); //restart the step without pause + $body.off(elementAvailableEventName); + }); + + //check if element is available every 150ms + waitUntilAvailable(step_data.pausedUntil.selector); + } + else { + //delay the actual action until 'the event' happens on body or selector + if (step_data.pausedUntil.selector == null) { + on(step_data.pausedUntil.event, function () { + stepAction(true); //restart the step without pause + off(step_data.pausedUntil.event); + }); + } + else { + $(step_data.pausedUntil.selector).on(step_data.pausedUntil.event, function () { + stepAction(true); //restart the step without pause + $(step_data.pausedUntil.selector).off(step_data.pausedUntil.event) + }); + } + } + + //the rest of the logic will be executed whenever the step is restarted + return; + } + var scrollSpeed = step_data.scrollAnimationSpeed; if ( @@ -154,7 +212,7 @@ var isHintInViewport = $(step_data.selector).get(0).getBoundingClientRect(); if(isHintInViewport.top < 0 || isHintInViewport.bottom > (window.innerHeight || document.documentElement.clientHeight)){ hideCurrentHint(); - $(document.body).scrollTo(step_data.selector, step_data.scrollAnimationSpeed || 250, {offset: -200}); + $(options.elementToScroll).scrollTo(step_data.selector, step_data.scrollAnimationSpeed || 250, {offset: -200}); } else { // if previous button has been clicked and element are in viewport to prevent custom step scrollAnimationSpeed set scrollSpeed to default @@ -186,6 +244,9 @@ if (step_data.showNext !== true) { $body.enjoyhint("hide_next"); } + else { + $body.enjoyhint("show_next"); + } $body.enjoyhint("hide_prev"); @@ -255,13 +316,20 @@ break; } } else { - $event_element.on(event, function(e) { - if (step_data.keyCode && e.keyCode != step_data.keyCode) { - return; - } - - current_step++; - stepAction(); // clicked + var alreadyTriggered = false; + $event_element.one(event, function (e) { //one should ensure that event is handled only once, but that's not always enough + if (alreadyTriggered) //make sure that the step is not changed twice handling the same event + return; + + alreadyTriggered = true; + if (step_data.keyCode && e.keyCode != step_data.keyCode) { + return; + } + + $event_element.off(event); //unregister the event + + current_step++; + stepAction(); //move to the next step }); } @@ -292,7 +360,8 @@ left: step_data.left, right: step_data.right, margin: step_data.margin, - scroll: step_data.scroll + scroll: step_data.scroll, + preventEvents: step_data.preventEvents }; var customBtnProps = { @@ -403,6 +472,7 @@ case "skip": skipAll(); + options.onSkip(); break; default: $body.trigger(makeEventName(event_name, true)); @@ -767,7 +837,7 @@ doit = setTimeout(function() { if(boundingClientRect.top < 0 || boundingClientRect.bottom > (window.innerHeight || document.documentElement.clientHeight)){ - $(document.body).scrollTo(that.stepData.enjoyHintElementSelector, 150, {offset: -200, onAfter:renderAfterResize}); + $(that.options.elementToScroll).scrollTo(that.stepData.enjoyHintElementSelector, 150, {offset: -200, onAfter:renderAfterResize}); } else renderAfterResize(); }, 150); @@ -1103,16 +1173,27 @@ .appendTo(that.enjoyhint); }; - that.disableEventsNearRect = function(rect) { + that.disableEventsNearRect = function(rect, alsoDisableRect) { + var top = rect.top; + var left = rect.left; + var right = rect.right; + var bottom = rect.bottom; + + //to disable events also within highlighted rectable, simply remove the gap + if (alsoDisableRect === true) { + top = bottom; + right = left; + } + $top_dis_events - .css({ - top: "0", - left: "0" - }) - .height(rect.top); + .css({ + top: "0", + left: "0" + }) + .height(top); $bottom_dis_events.css({ - top: rect.bottom + "px", + top: bottom + "px", left: "0" }); @@ -1121,11 +1202,11 @@ top: "0", left: 0 + "px" }) - .width(rect.left); + .width(left); $right_dis_events.css({ top: "0", - left: rect.right + "px" + left: right + "px" }); }; @@ -1437,10 +1518,10 @@ else { distance = initial_distance; ver_button_position = initial_ver_position; - that.$next_btn.html(customBtnProps.nextButton && customBtnProps.nextButton.text ? - customBtnProps.nextButton.text : 'Next'); - that.$prev_btn.html(customBtnProps.prevButton && customBtnProps.prevButton.text ? - customBtnProps.prevButton.text : 'Previous'); + that.$next_btn.html(customBtnProps.nextButton && customBtnProps.nextButton.text ? + customBtnProps.nextButton.text : 'Next'); + that.$prev_btn.html(customBtnProps.prevButton && customBtnProps.prevButton.text ? + customBtnProps.prevButton.text : 'Previous'); } that.$prev_btn.css({ @@ -1481,7 +1562,7 @@ bottom: shape_data.bottom, left: shape_data.left, right: shape_data.right - }); + }, data.preventEvents); that.renderArrow({ x_from: x_from, diff --git a/enjoyhint.min.js b/enjoyhint.min.js index caf7710..98bfec9 100644 --- a/enjoyhint.min.js +++ b/enjoyhint.min.js @@ -1 +1 @@ -"use strict";!function(a){"function"==typeof define&&define.amd?define(["jquery","./jquery.enjoyhint.js","jquery.scrollto"],a):"function"==typeof require&&"object"==typeof exports?module.exports=a(require("jquery"),require("./jquery.enjoyhint.js"),require("jquery.scrollto")):window.EnjoyHint=a(jQuery)}(function(a){return function(b){function c(){o.enjoyhint("render_circle",[]),a("#enjoyhint_label").remove(),a("#enjoyhint_arrpw_line").remove(),o.enjoyhint("hide_prev"),o.enjoyhint("hide_next"),o.enjoyhint("hide_skip")}var d,e=this,f=b||{},g=f.btnNextText,h=f.btnSkipText,i=f.backgroundColor||"rgba(0,0,0,0.6)",j="body",k={onStart:function(){},onEnd:function(){},onSkip:function(){},onNext:function(){}},l=a.extend(k,f),m=[],n=0,o=a(j),p=function(){a(".enjoyhint")&&a(".enjoyhint").remove(),o.css({overflow:"hidden"}),a(document).on("touchmove",q),o.enjoyhint({onNextClick:function(){t()},onPrevClick:function(){u()},onSkipClick:function(){l.onSkip(),v()},fill:i})},q=function(a){a.preventDefault()},r=function(){a(".enjoyhint").remove(),o.css({overflow:"auto"}),a(document).off("touchmove",q)};e.clear=function(){var b=a(".enjoyhint_next_btn"),c=a(".enjoyhint_skip_btn");a(".enjoyhint_prev_btn").removeClass(e.prevUserClass),b.removeClass(e.nextUserClass),b.text(g),c.removeClass(e.skipUserClass),c.text(h)};var s=function(){if(!m||!m[n])return o.enjoyhint("hide"),l.onEnd(),void r();l.onNext();var b=a(".enjoyhint");b.removeClass("enjoyhint-step-"+n),b.removeClass("enjoyhint-step-"+(n+1)),b.removeClass("enjoyhint-step-"+(n+2)),b.addClass("enjoyhint-step-"+(n+1));var f=m[n],g=f.scrollAnimationSpeed;f.onBeforeStart&&"function"==typeof f.onBeforeStart&&f.onBeforeStart();var h=f.timeout||0;setTimeout(function(){if(!f.selector)for(var b in f)f.hasOwnProperty(b)&&b.split(" ")[1]&&(f.selector=b.split(" ")[1],f.event=b.split(" ")[0],"next"!=b.split(" ")[0]&&"auto"!=b.split(" ")[0]&&"custom"!=b.split(" ")[0]||(f.event_type=b.split(" ")[0]),f.description=f[b]);setTimeout(function(){e.clear()},250);var h=a(f.selector).get(0).getBoundingClientRect();h.top<0||h.bottom>(window.innerHeight||document.documentElement.clientHeight)?(c(),a(document.body).scrollTo(f.selector,f.scrollAnimationSpeed||250,{offset:-200})):g=250,setTimeout(function(){var b=a(f.selector),c=w(f.event);if(o.enjoyhint("show"),d=b,f.event_selector&&(d=a(f.event_selector)),d.off(c),b.off("keydown"),f.event_type||"key"!=f.event||b.keydown(function(a){a.which==f.keyCode&&(n++,s())}),!0!==f.showNext&&o.enjoyhint("hide_next"),o.enjoyhint("hide_prev"),0!==n&&o.enjoyhint("show_prev"),0==f.showPrev&&o.enjoyhint("hide_prev"),0==f.showSkip?o.enjoyhint("hide_skip"):o.enjoyhint("show_skip"),f.nextButton){var g=a(".enjoyhint_next_btn");g.addClass(f.nextButton.className||""),g.text(f.nextButton.text||"Next"),e.nextUserClass=f.nextButton.className}if(f.prevButton){var h=a(".enjoyhint_prev_btn");h.addClass(f.prevButton.className||""),h.text(f.prevButton.text||"Previous"),e.prevUserClass=f.prevButton.className}if(f.skipButton){var i=a(".enjoyhint_skip_btn");i.addClass(f.skipButton.className||""),i.text(f.skipButton.text||"Skip"),e.skipUserClass=f.skipButton.className}if(f.event_type)switch(f.event_type){case"auto":return b[f.event](),f.event,n++,void s();case"custom":x(f.event,function(){n++,y(f.event),s()});break;case"next":o.enjoyhint("show_next")}else d.on(c,function(a){f.keyCode&&a.keyCode!=f.keyCode||(n++,s())});var j=Math.max(b.outerWidth(),b.outerHeight()),k=f.radius||Math.round(j/2)+5,l=b.offset(),m=b.outerWidth(),p=b.outerHeight(),q=void 0!==f.margin?f.margin:10,t={x:l.left+Math.round(m/2),y:l.top+Math.round(p/2)-a(document).scrollTop()},u={enjoyHintElementSelector:f.selector,center_x:t.x,center_y:t.y,text:f.description,arrowColor:f.arrowColor,top:f.top,bottom:f.bottom,left:f.left,right:f.right,margin:f.margin,scroll:f.scroll},v={nextButton:f.nextButton,prevButton:f.prevButton};if(0===u.center_x&&0===u.center_y)return o.enjoyhint("hide"),r(),console.log("Error: Element position couldn't be reached");f.shape&&"circle"==f.shape?(u.shape="circle",u.radius=k):(u.radius=0,u.width=m+q,u.height=p+q),o.enjoyhint("render_label_with_shape",u,e.stop,v)},g+20||270)},h)},t=function(){n++,s()},u=function(){n--,s()},v=function(){var b=m[n],c=a(b.selector);y(b.event),c.off(w(b.event)),r()},w=function(a,b){return a+(b?"custom":"")+".enjoy_hint"},x=function(a,b){o.on(w(a,!0),b)},y=function(a){o.off(w(a,!0))};window.addEventListener("resize",function(){null!=d&&o.enjoyhint("redo_events_near_rect",d[0].getBoundingClientRect())},!1),e.stop=function(){v()},e.reRunScript=function(a){n=a,s()},e.runScript=function(){n=0,l.onStart(),s()},e.resumeScript=function(){s()},e.setCurrentStep=function(a){n=a},e.getCurrentStep=function(){return n},e.trigger=function(a){switch(a){case"next":t();break;case"skip":v();break;default:o.trigger(w(a,!0))}},e.setScript=function(a){if(!(a instanceof Array)&&a.length<1)throw new Error("Configurations list isn't correct.");m=a},e.set=function(a){e.setScript(a)},e.setSteps=function(a){e.setScript(a)},e.run=function(){e.runScript()},e.resume=function(){e.resumeScript()},p()}}),CanvasRenderingContext2D.prototype.roundRect=function(a,b,c,d,e){return c<2*e&&(e=c/2),d<2*e&&(e=d/2),this.beginPath(),this.moveTo(a+e,b),this.arcTo(a+c,b,a+c,b+d,e),this.arcTo(a+c,b+d,a,b+d,e),this.arcTo(a,b+d,a,b,e),this.arcTo(a,b,a+c,b,e),this.closePath(),this},function(a){"function"==typeof define&&define.amd?define(["jquery","kinetic"],a):"undefined"!=typeof module&&module.exports?module.exports=a(require("jquery"),require("kinetic")):a(jQuery,Kinetic)}(function(a,b){var c,d,e,f,g,h,i,j,k,l,m,n=window.innerWidth,o=window.innerHeight,p={init:function(p){return this.each(function(){function q(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)c.setAttribute(d,b[d]);return c}var r={onNextClick:function(){},onSkipClick:function(){},onPrevClick:function(){},animation_time:800};this.enjoyhint_obj={},c=this.enjoyhint_obj,c.resetComponentStuff=function(){d=null,e=null,f=null,g=null,h=null,i=null,j=null,k=null,l=null,m=null,n=window.innerWidth,o=window.innerHeight};var s=a(this);c.options=a.extend(r,p),c.gcl={chooser:"enjoyhint"},c.cl={enjoy_hint:"enjoyhint",hide:"enjoyhint_hide",disable_events_element:"enjoyhint_disable_events",btn:"enjoyhint_btn",skip_btn:"enjoyhint_skip_btn",close_btn:"enjoyhint_close_btn",next_btn:"enjoyhint_next_btn",previous_btn:"enjoyhint_prev_btn",main_canvas:"enjoyhint_canvas",main_svg:"enjoyhint_svg",svg_wrapper:"enjoyhint_svg_wrapper",svg_transparent:"enjoyhint_svg_transparent",kinetic_container:"kinetic_container"},c.canvas_size={w:1.4*a(window).width(),h:1.4*a(window).height()},c.enjoyhint=a("