/*** * author: erwin yusrizal * ux by: tai nguyen * contributors: james west * jp adams * james warwood * craig roberts * created on: 27/02/2013 * version: 2.6.9 * features & bugs: https://github.com/warby-/jquery-pagewalkthrough/issues ***/ ; (function ($, window, document, undefined) { 'use strict'; /** * global var */ var _globalwalkthrough = {}, _elements = [], _activewalkthrough, _activeid, _hasdefault = true, _counter = 0, _iscookieload, _firsttimeload = true, _onload = true, _index = 0, _iswalkthroughactive = false, $jpwoverlay = $('
'), $jpwalkthrough = $('
'), $jpwtooltip = $('
'); /** * public method */ var methods = { // 获取私有变量值 getprivate: function (name) { return eval(name); }, // 同一元素绑定多组引导时或 _counter > 0的情况下,_activeid、_activewalkthrough、$('xx').data('jpw') 三者可能会出现不一致, // 导致同一元素触发show,renderoverlay出现的可能不是预想的那组引导,这算是一个bug // 所以这个方法用来清除组件的缓存 clearcache: function () { if (_iswalkthroughactive) { $.pagewalkthrough('close'); } for (var key in _globalwalkthrough) { _globalwalkthrough[key]._element.data('jpw', undefined); } _globalwalkthrough = {}; _elements = []; _activeid = undefined; _hasdefault = true; _counter = 0; _iscookieload = undefined; _firsttimeload = true; _onload = true; _index = 0; _iswalkthroughactive = false; }, isactive: function () { return !!_iswalkthroughactive; }, index: function (value) { if (typeof value !== 'undefined') { _index = value; } return _index; }, //init method init: function (options) { options = $.extend(true, {}, $.fn.pagewalkthrough.defaults, options); var that = this; if (!options.name) { throw new error('must provide a unique name for a tour'); } // @todo what happens with multiple walkthroughs on the same element? this.first().data('jpw', options); options._element = this; return this.each(function (i) { options = options || {}; options.elementid = options.name; _globalwalkthrough[options.name] = options; _elements.push(options.name); //check if onload and this is first time load if (options.onload) { _counter++; } //get first onload = true if (_counter === 1 && _onload) { _activeid = options.name; _activewalkthrough = _globalwalkthrough[_activeid]; _onload = false; } // set the activewalkthrough if onload is false for all walkthroughs if ((i + 1 === that.length && _counter === 0)) { _activeid = options.name; _activewalkthrough = _globalwalkthrough[_elements[0]]; _hasdefault = false; } }); }, renderoverlay: function () { // if each walkthrough has onload: true, log warning message if (_counter > 1) { debug('warning: only 1st walkthrough will be shown onload as default'); } //get cookie load _iscookieload = getcookie('_walkthrough-' + _activeid); //check if first time walkthrough if (typeof _iscookieload === 'undefined') { _iswalkthroughactive = true; if (!(onenter())) return; showstep(); showbutton('jpwclose', 'body'); settimeout(function () { //call onaftershow callback if (isfirststep() && _firsttimeload) { if (!onaftershow()) return; } }, 100); } else { oncookieload(_globalwalkthrough); } }, restart: function (e) { if (isfirststep()) return; _index = 0; if (!(onrestart(e))) return; if (!(onenter(e))) return; showstep(); }, close: function () { var options = _activewalkthrough; onleave(); if (typeof options.onclose === 'function') { options.onclose.call(this); } _index = 0; _firsttimeload = true; _iswalkthroughactive = false; //set cookie to false setcookie('_walkthrough-' + _activeid, 0, 365); _iscookieload = getcookie('_walkthrough-' + _activeid); $jpwoverlay.fadeout('slow', function () { $(this).remove(); }); $jpwalkthrough.fadeout('slow', function () { $(this).html('').remove(); }); $('#jpwclose').fadeout('slow', function () { $(this).remove(); }); }, show: function (name, e) { // if no name, then first argument is event e = name == null ? name : e; name = name || this.first().data('jpw').name; _activewalkthrough = _globalwalkthrough[this.first().data('jpw').name]; if ((name === _activeid && _iswalkthroughactive) || !(onenter(e))) return; _iswalkthroughactive = true; _firsttimeload = true; if (!(onbeforeshow())) return; showstep(); showbutton('jpwclose', 'body'); //call onaftershow callback if (isfirststep() && _firsttimeload) { if (!onaftershow()) return; } }, next: function (e) { _firsttimeload = false; if (islaststep()) return; if (!onleave(e)) return; _index = parseint(_index, 10) + 1; if (!onenter(e)) { methods.next(); } showstep('next'); }, prev: function (e) { if (isfirststep()) return; if (!onleave(e)) return; _index = parseint(_index, 10) - 1; if (!onenter(e)) { methods.prev(); } showstep('prev'); }, getoptions: function (activewalkthrough) { var _wtobj; //get only current active walkthrough if (activewalkthrough) { _wtobj = {}; _wtobj = _activewalkthrough; //get all walkthrough } else { _wtobj = []; for (var wt in _globalwalkthrough) { _wtobj.push(_globalwalkthrough[wt]); } } return _wtobj; }, refresh: function () { // stricly speaking, a skipdirection should never // be needed, but i'd rather provide one at this point // than watch it explode... showstep('next'); } }; //end public method /* pre-build walkthrough step function. handles the scrolling to the target * element. */ function showstep(skipdirection) { var options = _activewalkthrough, step = options.steps[_index], targetelement = options._element.find(step.wrapper), scrolltarget = getscrollparent(targetelement), maxscroll, scrollto; if (step.popup.type !== 'modal' && !targetelement.length) { if (step.popup.fallback === 'skip' || typeof step.popup.fallback === 'undefined') { methods[skipdirection](); return; } step.popup.type = step.popup.fallback; } // for modals, scroll to the top. for tooltips, try and center the target // (wrapper) element in the screen maxscroll = scrolltarget[0].scrollheight - scrolltarget.outerheight(); scrollto = step.popup.type === 'modal' ? 0 : math.floor( targetelement.offset().top - ($(window).height() / 2) + scrolltarget.scrolltop() ); // @todo: simplify this logic // // conditions for scrolling: // 1. new scroll value is not equal to current scroll value // and // a. new scroll value is less than the max scroll, and we are // currently at the max scroll value // or // b. new scroll value is less than or equal to 0, and the current // scroll is greater than 0 // or // c. new scroll value is greater than 0 if (scrolltarget.scrolltop() !== scrollto && ( (scrolltarget.scrolltop() === maxscroll && scrollto < maxscroll) || (scrollto <= 0 && scrolltarget.scrolltop() > 0) || (scrollto > 0) )) { // stylistic concerns - fill overlay hole and hide tooltip whilst // scrolling $jpwalkthrough.addclass('jpw-scrolling'); $jpwtooltip.fadeout('fast'); scrolltarget.animate({ scrolltop: scrollto }, options.steps[_index].scrollspeed, buildwalkthrough); } else { // not scrolling, so jump directly to building the walkthrough buildwalkthrough(); } } function buildwalkthrough() { $jpwalkthrough.removeclass('jpw-scrolling'); var options = _activewalkthrough, step = options.steps[_index], targetelement, scrollparent, maxheight; // extend step options with defaults options.steps[_index] = $.extend( true, {}, $.fn.pagewalkthrough.defaults.steps[0], step ); targetelement = options._element.find(step.wrapper); scrollparent = getscrollparent(targetelement); $jpwoverlay.show(); if (step.popup.type !== 'modal' && step.popup.type !== 'nohighlight') { $jpwalkthrough.html(''); //check if wrapper is not empty or undefined if (step.wrapper === '' || typeof step.wrapper === 'undefined') { // @todo should we skip here? debug('your walkthrough position is: "' + step.popup.type + '" but wrapper is empty or undefined. please check your "' + _activeid + '" wrapper parameter.' ); return; } maxheight = scrollparent.outerheight() - targetelement.offset().top + scrollparent.offset().top + scrollparent.scrolltop(); // if max height is negative, means we have plenty room so targetelement // height maxheight = maxheight <= 0 ? targetelement.outerheight() : maxheight; // @todo make it so we don't have to destroy and recreate this element for // each step $jpwoverlay.appendto($jpwalkthrough); // overlay hole $('
') .addclass('overlay-hole') .height(math.min(maxheight, targetelement.outerheight())) .width(targetelement.outerwidth()) .css({ // recommended to be at least twice the inset box-shadow spread padding: '20px', "box-sizing": "content-box", position: 'absolute', top: targetelement.offset().top - 20, // top/left minus padding left: targetelement.offset().left - 20, 'z-index': 999998, 'box-shadow': '0 0 1px 10000px rgba(0, 0, 0, 0.9)' }) .append( $('
') .css({ position: 'absolute', top: 0, bottom: 0, left: 0, right: 0 }) ) .appendto($jpwalkthrough); if ($('#jpwalkthrough').length) { $('#jpwalkthrough').remove(); } $jpwalkthrough.appendto('body').show(); $jpwtooltip.show(); showtooltip(); } else if (step.popup.type === 'modal') { if ($('#jpwalkthrough').length) { $('#jpwalkthrough').remove(); } showmodal(); } else { if ($('#jpwalkthrough').length) { $('#jpwalkthrough').remove(); } } showbutton('jpwprevious'); showbutton('jpwnext'); showbutton('jpwfinish'); // 每一步显示完成后,执行回调 if (typeof step.afterstepshow == 'function') { step.afterstepshow.call(this, step); } } /* * show modal */ function showmodal() { var options = _activewalkthrough, step = options.steps[_index]; $jpwoverlay.appendto('body').show().removeclass('transparent'); var textrotation = setrotation(parseint(step.popup.contentrotation, 10)); $jpwtooltip.css({ 'position': 'absolute', 'left': '50%', 'top': '25%', 'margin-left': -(parseint(step.popup.width, 10) + 60) / 2 + 'px', 'z-index': '999999' }); var tooltipslide = $('
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
'); $jpwalkthrough.html(''); $jpwtooltip.html('').append(tooltipslide) .wrapinner($('
', { id: 'tooltipwrapper', style: 'width:' + cleanvalue(parseint(step.popup.width, 10) + 30) })) .append('
') .appendto($jpwalkthrough); $jpwalkthrough.appendto('body'); $jpwtooltip.show(); $('#tooltipwrapper').css(textrotation); $('#tooltipinner').append(getcontent(step)).show(); $jpwalkthrough.show(); } /* * show tooltip */ function showtooltip() { var opt = _activewalkthrough, step = opt.steps[_index]; var top, left, arrowtop, arrowleft, overlayholewidth = $('#jpwalkthrough .overlay-hole').outerwidth(), overlayholeheight = $('#jpwalkthrough .overlay-hole').outerheight(), overlayholetop = $('#jpwalkthrough .overlay-hole').offset().top, overlayholeleft = $('#jpwalkthrough .overlay-hole').offset().left, arrow = 30; var textrotation = (typeof step.popup.contentrotation === 'undefined' || parseint(step.popup.contentrotation, 10) === 0) ? clearrotation() : setrotation(parseint(step.popup.contentrotation, 10)); // remove overlay background to prevent double-transparency if ($('#jpwoverlay').length) { $('#jpwoverlay').addclass('transparent'); } var tooltipslide = $('
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
'); $jpwtooltip.html('').css({ 'margin-left': '0', 'margin-top': '0', 'position': 'absolute', 'z-index': '999999' }) .append(tooltipslide) .wrapinner($('
', { id: 'tooltipwrapper', style: 'width:' + cleanvalue(parseint(step.popup.width, 10) + 30) })) .appendto($jpwalkthrough); $jpwalkthrough.appendto('body').show(); $('#tooltipwrapper').css(textrotation); $('#tooltipinner').append(getcontent(step)).show(); $jpwtooltip.append( ' ' ); switch (step.popup.position) { case 'top': top = overlayholetop - ($jpwtooltip.height() + (arrow / 2)) + parseint(step.popup.offsetvertical, 10) - 86; left = (overlayholeleft + (overlayholewidth / 2)) - ($jpwtooltip.width() / 2) - 5 + parseint(step.popup.offsethorizontal, 10); arrowleft = ($jpwtooltip.width() / 2) - arrow + parseint(step.popup.offsetarrowhorizontal, 10); arrowtop = (step.popup.offsetarrowvertical) ? parseint(step.popup.offsetarrowvertical, 10) : ''; break; case 'right': top = overlayholetop - (arrow / 2) + parseint(step.popup.offsetvertical, 10); left = overlayholeleft + overlayholewidth + (arrow / 2) + parseint(step.popup.offsethorizontal, 10) + 105; arrowtop = arrow + parseint(step.popup.offsetarrowvertical, 10); arrowleft = (step.popup.offsetarrowhorizontal) ? parseint(step.popup.offsetarrowhorizontal, 10) : ''; break; case 'bottom': top = overlayholetop + overlayholeheight + parseint(step.popup.offsetvertical, 10) + 86; left = (overlayholeleft + (overlayholewidth / 2)) - ($jpwtooltip.width() / 2) - 5 + parseint(step.popup.offsethorizontal, 10); arrowleft = (($jpwtooltip.width() / 2) - arrow) + parseint(step.popup.offsetarrowhorizontal, 10); arrowtop = (step.popup.offsetarrowvertical) ? parseint(step.popup.offsetarrowvertical, 10) : ''; break; case 'left': top = overlayholetop - (arrow / 2) + parseint(step.popup.offsetvertical, 10); left = overlayholeleft - $jpwtooltip.width() - (arrow) + parseint(step.popup.offsethorizontal, 10) - 105; arrowtop = arrow + parseint(step.popup.offsetarrowvertical, 10); arrowleft = (step.popup.offsetarrowvertical) ? parseint(step.popup.offsetarrowhorizontal, 10) : ''; break; } $('#jpwtooltip span.' + step.popup.position).css({ 'top': cleanvalue(arrowtop), 'left': cleanvalue(arrowleft) }); $jpwtooltip.css({ 'top': cleanvalue(top), 'left': cleanvalue(left) }); $jpwalkthrough.show(); } /* get the content for a step. first attempts to treat step.popup.content * as a selector. if this fails, or returns an empty result set, it falls * back to return the value of step.popup.content. * * this allows both selectors and literal content to be provided in the * content option. * * @param {object} step the step data to return the content for */ function getcontent(step) { var option = step.popup.content, content; try { content = $('body').find(option).html(); } catch (e) { } return content || option; } /* render a control button outside the #tooltipinner element. * * @param {string} id the button identifier within the * options.buttons hash (e.g. 'jpwnext') * @param {jquery|string} appendto (optional) the element or selector to * append the button to. defaults to * #tooltipwrapper */ function showbutton(id, appendto) { if ($('#' + id).length) return; var btn = _activewalkthrough.buttons[id], $a; // check that button is defined if (!btn) return; // check that button should be shown if ((typeof btn.show === 'function' && !btn.show()) || !btn.show) { return; } $a = $('', { id: id, html: btn.i18n }); // append button if (appendto) { $(appendto).append($a); } else { $('#tooltipwrapper').after($a); } } /** /* callback /*/ //callback for onloadhidden cookie function oncookieload(options) { /*jshint validthis: true */ for (var i = 0; i < _elements.length; i++) { if (typeof (options[_elements[i]].oncookieload) === 'function') { options[_elements[i]].oncookieload.call(this); } } return false; } function onleave(e) { /*jshint validthis: true */ var options = _activewalkthrough; if (typeof options.steps[_index].onleave === 'function') { if (!options.steps[_index].onleave.call(this, e, _index)) { return false; } } return true; } //callback for onenter step function onenter(e) { /*jshint validthis: true */ var options = _activewalkthrough; if (typeof options.steps[_index].onenter === 'function') { return options.steps[_index].onenter.call(this, e, _index); } return true; } //callback for onrestart help function onrestart(e) { /*jshint validthis: true */ var options = _activewalkthrough; //set help mode to true _iswalkthroughactive = true; methods.restart(e); if (typeof options.onrestart === 'function') { if (!options.onrestart.call(this)) { return false; } } return true; } //callback for before all first walkthrough element loaded function onbeforeshow() { /*jshint validthis: true */ var options = _activewalkthrough || {}; _index = 0; if (typeof (options.onbeforeshow) === 'function') { if (!options.onbeforeshow.call(this)) { return false; } } return true; } //callback for after all first walkthrough element loaded function onaftershow() { /*jshint validthis: true */ var options = _activewalkthrough; _index = 0; if (typeof (options.onaftershow) === 'function') { if (!options.onaftershow.call(this)) { return false; } } return true; } /** * helpers */ function debug(message) { if (window.console && window.console.log) window.console.log(message); } function clearrotation() { var rotationstyle = { '-webkit-transform': 'none', //safari '-moz-transform': 'none', //firefox '-o-transform': 'none', //opera 'filter': 'none', //ie7 '-ms-transform': 'none' //ie8+ }; return rotationstyle; } function setrotation(angle) { //for ie7 & ie8 var m11, m12, m21, m22, deg2rad, rad; //degree to radian deg2rad = math.pi * 2 / 360; rad = angle * deg2rad; m11 = math.cos(rad); m12 = math.sin(rad); m21 = math.sin(rad); m22 = math.cos(rad); var rotationstyle = { '-webkit-transform': 'rotate(' + parseint(angle, 10) + 'deg)', //safari '-moz-transform': 'rotate(' + parseint(angle, 10) + 'deg)', //firefox '-o-transform': 'rotate(' + parseint(angle, 10) + 'deg)', //opera '-ms-transform': 'rotate(' + parseint(angle, 10) + 'deg)' //ie9+ }; return rotationstyle; } function cleanvalue(value) { if (typeof value === 'string') { if (value.tolowercase().indexof('px') === -1) { return value + 'px'; } else { return value; } } else { return value + 'px'; } } function setcookie(cname, value, exdays) { var exdate = new date(); exdate.setdate(exdate.getdate() + exdays); var cvalue = encodeuricomponent(value) + ((exdays == null) ? '' : '; expires=' + exdate.toutcstring()); document.cookie = [cname, '=', cvalue].join(''); } function getcookie(cname) { var i, x, y, arrcookies = document.cookie.split(';'); for (i = 0; i < arrcookies.length; i++) { x = arrcookies[i].substr(0, arrcookies[i].indexof('=')); y = arrcookies[i].substr(arrcookies[i].indexof('=') + 1); x = x.replace(/^\s+|\s+$/g, ''); if (x === cname) { return decodeuricomponent(y); } } } /* returns true if the current step is the last step in the walkthrough. * * @return {boolean} true if user is currently viewing last step; false * otherwise */ function islaststep() { return _index === (_activewalkthrough.steps.length - 1); } /* returns true if the current step is the first step in the walkthrough. * * @return {boolean} true if user is currently viewing first step; false * otherwise */ function isfirststep() { return _index === 0; } /* get the first scrollable parent of the specified element. * adapted from jqueryui's [scrollparent](api.jqueryui.com/scrollparent/). * * @param {jquery} element the element to find the scrollable parent of * * @return {jquery} the first scrollable parent, or an empty jquery object if * either of the following is true: * 1. `element`'s position is 'fixed' * 2. `element`'s position is 'absolute', and the parent's * is 'static' */ function getscrollparent(element) { if (!(element instanceof $)) { element = $(element); } element = element.first(); var position = element.css('position'), excludestaticparent = position === 'absolute', scrollparent = element.parents().filter(function () { var parent = $(this); if (excludestaticparent && parent.css('position') === 'static') { return false; } return (/(auto|scroll)/).test( parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x') ); }).eq(0); //return position === 'fixed' ? $() : !scrollparent.length ? // $('body') : scrollparent; // wrapper为fixed时,返回$() 会出异常,改成这样 return position === 'fixed' ? $('body') : !scrollparent.length ? $('body') : scrollparent; } /** * button close click */ /* close and finish tour buttons clicks */ $(document).on('click', '#jpwclose, #jpwfinish', methods.close); /* next button clicks */ $(document).on('click', '#jpwnext', function () { $.pagewalkthrough('next'); }); /* previous button clicks */ $(document).on('click', '#jpwprevious', function () { $.pagewalkthrough('prev'); }); $(document).on( 'click', '#jpwoverlay, #jpwtooltip', function (ev) { ev.stoppropagation(); ev.stopimmediatepropagation(); } ); /** * drag & drop */ /** * main plugin */ $.pagewalkthrough = $.fn.pagewalkthrough = function (method) { if (methods[method]) { return methods[method].apply(this, [].slice.call(arguments, 1)); } else if (typeof method === 'object' || !method) { methods.init.apply(this, arguments); // render the overlay on it has a default walkthrough set to show onload if (_hasdefault && _counter < 2) { settimeout(function () { methods.renderoverlay(); }, 500); } } else { $.error('method ' + method + ' does not exist on jquery.pagewalkthrough'); } }; /* #### options * * default options for each walkthrough. * user options extend these defaults. */ $.fn.pagewalkthrough.defaults = { /* array of steps to show */ steps: [ { // jquery selector for the element to highlight for this step wrapper: '', // ##### popup options popup: { // selector for the element which contains the content, or the literal // content content: '', // popup type - either modal, tooltip or nohighlight. // see [popup types](/pages/popup-types.html) type: 'modal', // position for tooltip and nohighlight style popups - either top, // left, right or bottom position: 'top', // horizontal offset for the walkthrough offsethorizontal: 0, // vertical offset for the walkthrough offsetvertical: 0, // horizontal offset for the arrow offsetarrowhorizontal: 0, // vertical offset for the arrow offsetarrowvertical: 0, // default width for each popup width: '320', // amount in degrees to rotate the content by contentrotation: 0 }, // automatically scroll to the content for the step autoscroll: true, // speed to use when scrolling to elements scrollspeed: 1000, // prevent the user from scrolling away from the content lockscrolling: false, // callback when entering the step onenter: null, // callback when leaving the step onleave: null } ], // **(required)** walkthrough name. should be a unique name to identify the // walkthrough, as it will // be used in the cookie name name: null, // automatically show the walkthrough when the page is loaded. if multiple // walkthroughs set this to true, only the first walkthrough is shown // automatically onload: true, // callback to be executed before the walkthrough is shown onbeforeshow: null, // callback executed after the walkthrough is shown onaftershow: null, // callback executed in the event that 'restart' is triggered onrestart: null, // callback executed when the walkthrough is closed. the walkthrough can be // closed by the user clicking the close button in the top right, or // clicking the finish button on the last step onclose: null, // callback executed when cookie has been set after a walkthrough has been // closed oncookieload: null, /* ##### walkthrough controls * * hash of buttons to show. object keys are used as the button element's id */ buttons: { // id of the button jpwclose: { // translation string for the button i18n: ' x', // whether or not to show the button. can be a boolean value, or a // function which returns a boolean value show: true }, jpwnext: { i18n: '下一步', // function which resolves to a boolean show: function () { return !islaststep(); } }, jpwprevious: { i18n: '上一步', show: function () { return !isfirststep(); } }, jpwfinish: { i18n: '完成', show: function () { return islaststep(); } } } }; }(jquery, window, document));