diff --git a/core/modules/tour/css/joyride-2.0.3.css b/core/modules/tour/css/joyride-2.0.3.css new file mode 100644 index 0000000..ba103f0 --- /dev/null +++ b/core/modules/tour/css/joyride-2.0.3.css @@ -0,0 +1,222 @@ +/* Artfully masterminded by ZURB */ +#joyRideTipContent { display: none; } + +/* Default styles for the container */ +.joyride-tip-guide { + position: absolute; + background: #000; + background: rgba(0,0,0,0.8); + display: none; + color: #fff; + width: 300px; + z-index: 101; + top: 0; /* keeps the page from scrolling when calculating position */ + left: 0; + font-family: "HelveticaNeue", "Helvetica Neue", "Helvetica", Helvetica, Arial, Lucida, sans-serif; + font-weight: normal; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; +} + +.joyride-content-wrapper { + padding: 10px 10px 15px 15px; +} + +/* Mobile */ +@media only screen and (max-width: 767px) { + .joyride-tip-guide { + width: 95% !important; + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; + left: 2.5% !important; + } + .joyride-tip-guide-wrapper { + width: 100%; + } +} + + +/* Add a little css triangle pip, older browser just miss out on the fanciness of it */ +.joyride-tip-guide span.joyride-nub { + display: block; + position: absolute; + left: 22px; + width: 0; + height: 0; + border: solid 14px; + border: solid 14px; +} + +.joyride-tip-guide span.joyride-nub.top { + /* + IE7/IE8 Don't support rgba so we set the fallback + border color here. However, IE7/IE8 are also buggy + in that the fallback color doesn't work for + border-bottom-color so here we set the border-color + and override the top,left,right colors below. + */ + border-color: #000; + border-color: rgba(0,0,0,0.8); + border-top-color: transparent !important; + border-left-color: transparent !important; + border-right-color: transparent !important; + top: -28px; + bottom: none; +} + +.joyride-tip-guide span.joyride-nub.bottom { + /* + IE7/IE8 Don't support rgba so we set the fallback + border color here. However, IE7/IE8 are also buggy + in that the fallback color doesn't work for + border-top-color so here we set the border-color + and override the bottom,left,right colors below. + */ + border-color: #000; + border-color: rgba(0,0,0,0.8) !important; + border-bottom-color: transparent !important; + border-left-color: transparent !important; + border-right-color: transparent !important; + bottom: -28px; + bottom: none; +} + +.joyride-tip-guide span.joyride-nub.right { + border-color: #000; + border-color: rgba(0,0,0,0.8) !important; + border-top-color: transparent !important; + border-right-color: transparent !important; + border-bottom-color: transparent !important; + top: 22px; + bottom: none; + left: auto; + right: -28px; +} + +.joyride-tip-guide span.joyride-nub.left { + border-color: #000; + border-color: rgba(0,0,0,0.8) !important; + border-top-color: transparent !important; + border-left-color: transparent !important; + border-bottom-color: transparent !important; + top: 22px; + left: -28px; + right: auto; + bottom: none; +} + +/* Typography */ +.joyride-tip-guide h1,.joyride-tip-guide h2,.joyride-tip-guide h3,.joyride-tip-guide h4,.joyride-tip-guide h5,.joyride-tip-guide h6 { + line-height: 1.25; + margin: 0; + font-weight: bold; + color: #fff; +} +.joyride-tip-guide h1 { font-size: 30px; } +.joyride-tip-guide h2 { font-size: 26px; } +.joyride-tip-guide h3 { font-size: 22px; } +.joyride-tip-guide h4 { font-size: 18px; } +.joyride-tip-guide h5 { font-size: 16px; } +.joyride-tip-guide h6 { font-size: 14px; } +.joyride-tip-guide p { + margin: 0 0 18px 0; + font-size: 14px; + line-height: 18px; +} +.joyride-tip-guide a { + color: rgb(255,255,255); + text-decoration: none; + border-bottom: dotted 1px rgba(255,255,255,0.6); +} +.joyride-tip-guide a:hover { + color: rgba(255,255,255,0.8); + border-bottom: none; +} + +/* Button Style */ +.joyride-tip-guide .joyride-next-tip { + width: auto; + padding: 6px 18px 4px; + font-size: 13px; + text-decoration: none; + color: rgb(255,255,255); + border: solid 1px rgb(0,60,180); + background: rgb(0,99,255); + background: -moz-linear-gradient(top, rgb(0,99,255) 0%, rgb(0,85,214) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgb(0,99,255)), color-stop(100%,rgb(0,85,214))); + background: -webkit-linear-gradient(top, rgb(0,99,255) 0%,rgb(0,85,214) 100%); + background: -o-linear-gradient(top, rgb(0,99,255) 0%,rgb(0,85,214) 100%); + background: -ms-linear-gradient(top, rgb(0,99,255) 0%,rgb(0,85,214) 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#0063ff', endColorstr='#0055d6',GradientType=0 ); + background: linear-gradient(top, rgb(0,99,255) 0%,rgb(0,85,214) 100%); + text-shadow: 0 -1px 0 rgba(0,0,0,0.5); + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + -webkit-box-shadow: 0px 1px 0px rgba(255,255,255,0.3) inset; + -moz-box-shadow: 0px 1px 0px rgba(255,255,255,0.3) inset; + box-shadow: 0px 1px 0px rgba(255,255,255,0.3) inset; +} + +.joyride-next-tip:hover { + color: rgb(255,255,255) !important; + border: solid 1px rgb(0,60,180) !important; + background: rgb(43,128,255); + background: -moz-linear-gradient(top, rgb(43,128,255) 0%, rgb(29,102,211) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgb(43,128,255)), color-stop(100%,rgb(29,102,211))); + background: -webkit-linear-gradient(top, rgb(43,128,255) 0%,rgb(29,102,211) 100%); + background: -o-linear-gradient(top, rgb(43,128,255) 0%,rgb(29,102,211) 100%); + background: -ms-linear-gradient(top, rgb(43,128,255) 0%,rgb(29,102,211) 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#2b80ff', endColorstr='#1d66d3',GradientType=0 ); + background: linear-gradient(top, rgb(43,128,255) 0%,rgb(29,102,211) 100%); +} + +.joyride-timer-indicator-wrap { + width: 50px; + height: 3px; + border: solid 1px rgba(255,255,255,0.1); + position: absolute; + right: 17px; + bottom: 16px; +} +.joyride-timer-indicator { + display: block; + width: 0; + height: inherit; + background: rgba(255,255,255,0.25); +} + +.joyride-close-tip { + position: absolute; + right: 10px; + top: 10px; + color: rgba(255,255,255,0.4) !important; + text-decoration: none; + font-family: Verdana, sans-serif; + font-size: 10px; + font-weight: bold; + border-bottom: none !important; +} + +.joyride-close-tip:hover { + color: rgba(255,255,255,0.9) !important; +} + +.joyride-modal-bg { + position: fixed; + height: 100%; + width: 100%; + background: rgb(0,0,0); + background: transparent; + background: rgba(0,0,0, 0.5); + -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + filter: alpha(opacity=50); + opacity: 0.5; + z-index: 100; + display: none; + top: 0; + left: 0; + cursor: pointer; +} diff --git a/core/modules/tour/css/tour-rtl.css b/core/modules/tour/css/tour-rtl.css new file mode 100644 index 0000000..5e19f7b --- /dev/null +++ b/core/modules/tour/css/tour-rtl.css @@ -0,0 +1,13 @@ +/** + * @file + * RTL styling for tour module. + */ + +.js .toolbar .bar .tour-toolbar-tab.tab { + float: left; +} + +.tour-progress { + right: 0; + left: 15px; +} diff --git a/core/modules/tour/css/tour.css b/core/modules/tour/css/tour.css new file mode 100644 index 0000000..9f71ccb --- /dev/null +++ b/core/modules/tour/css/tour.css @@ -0,0 +1,36 @@ +/** + * @file + * Styling for tour module. + */ + +/* Tab appearance. */ +.js .toolbar .bar .tour-toolbar-tab.tab { + float: right; /* LTR */ +} +.js .toolbar .bar .tour-toolbar-tab button { + padding-bottom: 1em; + padding-top: 1em; + color: #fff; + font-weight: bold; +} +.js .toolbar .bar .tour-toolbar-tab button.active { + background-image: -webkit-linear-gradient(rgba(255, 255, 255, 0.25) 20%, transparent 200%); + background-image: linear-gradient(rgba(255, 255, 255, 0.25) 20%, transparent 200%); +} + +/* Joyride tips should always be on top of everything else. */ +.joyride-tip-guide { + z-index: 999; +} + +/* Override placement of the tour progress indicator. */ +.tour-progress { + position: absolute; + bottom: 10px; + right: 15px; /* LTR */ +} + +/* @todo Remove once http://drupal.org/node/1916690 is resolved. */ +.js .toolbar .bar .tour-toolbar-tab.tab.element-hidden { + display: none; +} diff --git a/core/modules/tour/js/jquery.joyride-2.0.3.js b/core/modules/tour/js/jquery.joyride-2.0.3.js new file mode 100644 index 0000000..b144fb9 --- /dev/null +++ b/core/modules/tour/js/jquery.joyride-2.0.3.js @@ -0,0 +1,675 @@ +/* + * jQuery Foundation Joyride Plugin 2.0.3 + * http://foundation.zurb.com + * Copyright 2012, ZURB + * Free to use under the MIT license. + * http://www.opensource.org/licenses/mit-license.php +*/ + +/*jslint unparam: true, browser: true, indent: 2 */ + +;(function ($, window, undefined) { + 'use strict'; + + var defaults = { + 'version' : '2.0.3', + 'tipLocation' : 'bottom', // 'top' or 'bottom' in relation to parent + 'nubPosition' : 'auto', // override on a per tooltip bases + 'scrollSpeed' : 300, // Page scrolling speed in milliseconds + 'timer' : 0, // 0 = no timer , all other numbers = timer in milliseconds + 'startTimerOnClick' : true, // true or false - true requires clicking the first button start the timer + 'startOffset' : 0, // the index of the tooltip you want to start on (index of the li) + 'nextButton' : true, // true or false to control whether a next button is used + 'tipAnimation' : 'fade', // 'pop' or 'fade' in each tip + 'pauseAfter' : [], // array of indexes where to pause the tour after + 'tipAnimationFadeSpeed': 300, // when tipAnimation = 'fade' this is speed in milliseconds for the transition + 'cookieMonster' : false, // true or false to control whether cookies are used + 'cookieName' : 'joyride', // Name the cookie you'll use + 'cookieDomain' : false, // Will this cookie be attached to a domain, ie. '.notableapp.com' + 'tipContainer' : 'body', // Where will the tip be attached + 'postRideCallback' : $.noop, // A method to call once the tour closes (canceled or complete) + 'postStepCallback' : $.noop, // A method to call after each step + 'template' : { // HTML segments for tip layout + 'link' : 'X', + 'timer' : '
', + 'tip' : '
', + 'wrapper' : '', + 'button' : '' + } + }, + + Modernizr = Modernizr || false, + + settings = {}, + + methods = { + + init : function (opts) { + return this.each(function () { + + if ($.isEmptyObject(settings)) { + settings = $.extend(true, defaults, opts); + + // non configurable settings + settings.document = window.document; + settings.$document = $(settings.document); + settings.$window = $(window); + settings.$content_el = $(this); + settings.body_offset = $(settings.tipContainer).position(); + settings.$tip_content = $('> li', settings.$content_el); + settings.paused = false; + settings.attempts = 0; + + settings.tipLocationPatterns = { + top: ['bottom'], + bottom: [], // bottom should not need to be repositioned + left: ['right', 'top', 'bottom'], + right: ['left', 'top', 'bottom'] + }; + + // are we using jQuery 1.7+ + methods.jquery_check(); + + // can we create cookies? + if (!$.isFunction($.cookie)) { + settings.cookieMonster = false; + } + + // generate the tips and insert into dom. + if (!settings.cookieMonster || !$.cookie(settings.cookieName)) { + + settings.$tip_content.each(function (index) { + methods.create({$li : $(this), index : index}); + }); + + // show first tip + if (!settings.startTimerOnClick && settings.timer > 0) { + methods.show('init'); + methods.startTimer(); + } else { + methods.show('init'); + } + + } + + settings.$document.on('click.joyride', '.joyride-next-tip, .joyride-modal-bg', function (e) { + e.preventDefault(); + + if (settings.$li.next().length < 1) { + methods.end(); + } else if (settings.timer > 0) { + clearTimeout(settings.automate); + methods.hide(); + methods.show(); + methods.startTimer(); + } else { + methods.hide(); + methods.show(); + } + + }); + + settings.$document.on('click.joyride', '.joyride-close-tip', function (e) { + e.preventDefault(); + methods.end(); + }); + + settings.$window.bind('resize.joyride', function (e) { + if (methods.is_phone()) { + methods.pos_phone(); + } else { + methods.pos_default(); + } + }); + } else { + methods.restart(); + } + + }); + }, + + // call this method when you want to resume the tour + resume : function () { + methods.set_li(); + methods.show(); + }, + + tip_template : function (opts) { + var $blank, content, $wrapper; + + opts.tip_class = opts.tip_class || ''; + + $blank = $(settings.template.tip).addClass(opts.tip_class); + content = $.trim($(opts.li).html()) + + methods.button_text(opts.button_text) + + settings.template.link + + methods.timer_instance(opts.index); + + $wrapper = $(settings.template.wrapper); + if (opts.li.attr('data-aria-labelledby')) { + $wrapper.attr('aria-labelledby', opts.li.attr('data-aria-labelledby')) + } + if (opts.li.attr('data-aria-describedby')) { + $wrapper.attr('aria-describedby', opts.li.attr('data-aria-describedby')) + } + $blank.append($wrapper); + $blank.first().attr('data-index', opts.index); + $('.joyride-content-wrapper', $blank).append(content); + + return $blank[0]; + }, + + timer_instance : function (index) { + var txt; + + if ((index === 0 && settings.startTimerOnClick && settings.timer > 0) || settings.timer === 0) { + txt = ''; + } else { + txt = methods.outerHTML($(settings.template.timer)[0]); + } + return txt; + }, + + button_text : function (txt) { + if (settings.nextButton) { + txt = $.trim(txt) || 'Next'; + txt = methods.outerHTML($(settings.template.button).append(txt)[0]); + } else { + txt = ''; + } + return txt; + }, + + create : function (opts) { + // backwards compatibility with data-text attribute + var buttonText = opts.$li.attr('data-button') || opts.$li.attr('data-text'), + tipClass = opts.$li.attr('class'), + $tip_content = $(methods.tip_template({ + tip_class : tipClass, + index : opts.index, + button_text : buttonText, + li : opts.$li + })); + + $(settings.tipContainer).append($tip_content); + }, + + show : function (init) { + var opts = {}, ii, opts_arr = [], opts_len = 0, p, + $timer = null; + + // are we paused? + if (settings.$li === undefined || ($.inArray(settings.$li.index(), settings.pauseAfter) === -1)) { + + // don't go to the next li if the tour was paused + if (settings.paused) { + settings.paused = false; + } else { + methods.set_li(init); + } + + settings.attempts = 0; + + if (settings.$li.length && settings.$target.length > 0) { + opts_arr = (settings.$li.data('options') || ':').split(';'); + opts_len = opts_arr.length; + + // parse options + for (ii = opts_len - 1; ii >= 0; ii--) { + p = opts_arr[ii].split(':'); + + if (p.length === 2) { + opts[$.trim(p[0])] = $.trim(p[1]); + } + } + + settings.tipSettings = $.extend({}, settings, opts); + + settings.tipSettings.tipLocationPattern = settings.tipLocationPatterns[settings.tipSettings.tipLocation]; + + // scroll if not modal + if (!/body/i.test(settings.$target.selector)) { + methods.scroll_to(); + } + + if (methods.is_phone()) { + methods.pos_phone(true); + } else { + methods.pos_default(true); + } + + $timer = $('.joyride-timer-indicator', settings.$next_tip); + + if (/pop/i.test(settings.tipAnimation)) { + + $timer.outerWidth(0); + + if (settings.timer > 0) { + + settings.$next_tip.show(); + $timer.animate({ + width: $('.joyride-timer-indicator-wrap', settings.$next_tip).outerWidth() + }, settings.timer); + + } else { + + settings.$next_tip.show(); + + } + + + } else if (/fade/i.test(settings.tipAnimation)) { + + $timer.outerWidth(0); + + if (settings.timer > 0) { + + settings.$next_tip.fadeIn(settings.tipAnimationFadeSpeed); + + settings.$next_tip.show(); + $timer.animate({ + width: $('.joyride-timer-indicator-wrap', settings.$next_tip).outerWidth() + }, settings.timer); + + } else { + + settings.$next_tip.fadeIn(settings.tipAnimationFadeSpeed); + + } + } + + settings.$current_tip = settings.$next_tip; + $('.joyride-next-tip', settings.$current_tip).focus(); + methods.tabbable(settings.$current_tip); + + // skip non-existent targets + } else if (settings.$li && settings.$target.length < 1) { + + methods.show(); + + } else { + + methods.end(); + + } + } else { + + settings.paused = true; + + } + + }, + + // detect phones with media queries if supported. + is_phone : function () { + if (Modernizr) { + return Modernizr.mq('only screen and (max-width: 767px)'); + } + + return (settings.$window.width() < 767) ? true : false; + }, + + hide : function () { + settings.postStepCallback(settings.$li.index(), settings.$current_tip); + $('.joyride-modal-bg').hide(); + settings.$current_tip.hide(); + }, + + set_li : function (init) { + if (init) { + settings.$li = settings.$tip_content.eq(settings.startOffset); + methods.set_next_tip(); + settings.$current_tip = settings.$next_tip; + } else { + settings.$li = settings.$li.next(); + methods.set_next_tip(); + } + + methods.set_target(); + }, + + set_next_tip : function () { + settings.$next_tip = $('.joyride-tip-guide[data-index=' + settings.$li.index() + ']'); + }, + + set_target : function () { + var cl = settings.$li.attr('data-class'), + id = settings.$li.attr('data-id'), + $sel = function () { + if (id) { + return $(settings.document.getElementById(id)); + } else if (cl) { + return $('.' + cl).first(); + } else { + return $('body'); + } + }; + + settings.$target = $sel(); + }, + + scroll_to : function () { + var window_half, tipOffset; + + window_half = settings.$window.height() / 2; + tipOffset = Math.ceil(settings.$target.offset().top - window_half + settings.$next_tip.outerHeight()); + + $("html, body").stop().animate({ + scrollTop: tipOffset + }, settings.scrollSpeed); + }, + + paused : function () { + if (($.inArray((settings.$li.index() + 1), settings.pauseAfter) === -1)) { + return true; + } + + return false; + }, + + destroy : function () { + settings.$document.off('.joyride'); + $(window).off('.joyride'); + $('.joyride-close-tip, .joyride-next-tip, .joyride-modal-bg').off('.joyride'); + $('.joyride-tip-guide, .joyride-modal-bg').remove(); + clearTimeout(settings.automate); + settings = {}; + }, + + restart : function () { + methods.hide(); + settings.$li = undefined; + methods.show('init'); + }, + + pos_default : function (init) { + var half_fold = Math.ceil(settings.$window.height() / 2), + tip_position = settings.$next_tip.offset(), + $nub = $('.joyride-nub', settings.$next_tip), + nub_height = Math.ceil($nub.outerHeight() / 2), + toggle = init || false; + + // tip must not be "display: none" to calculate position + if (toggle) { + settings.$next_tip.css('visibility', 'hidden'); + settings.$next_tip.show(); + } + + if (!/body/i.test(settings.$target.selector)) { + + if (methods.bottom()) { + settings.$next_tip.css({ + top: (settings.$target.offset().top + nub_height + settings.$target.outerHeight()), + left: settings.$target.offset().left}); + + methods.nub_position($nub, settings.tipSettings.nubPosition, 'top'); + + } else if (methods.top()) { + + settings.$next_tip.css({ + top: (settings.$target.offset().top - settings.$next_tip.outerHeight() - nub_height), + left: settings.$target.offset().left}); + + methods.nub_position($nub, settings.tipSettings.nubPosition, 'bottom'); + + } else if (methods.right()) { + + settings.$next_tip.css({ + top: settings.$target.offset().top, + left: (settings.$target.outerWidth() + settings.$target.offset().left)}); + + methods.nub_position($nub, settings.tipSettings.nubPosition, 'left'); + + } else if (methods.left()) { + + settings.$next_tip.css({ + top: settings.$target.offset().top, + left: (settings.$target.offset().left - settings.$next_tip.outerWidth() - nub_height)}); + + methods.nub_position($nub, settings.tipSettings.nubPosition, 'right'); + + } + + if (!methods.visible(methods.corners(settings.$next_tip)) && settings.attempts < settings.tipSettings.tipLocationPattern.length) { + + $nub.removeClass('bottom') + .removeClass('top') + .removeClass('right') + .removeClass('left'); + + settings.tipSettings.tipLocation = settings.tipSettings.tipLocationPattern[settings.attempts]; + + settings.attempts++; + + methods.pos_default(true); + + } + + } else if (settings.$li.length) { + + methods.pos_modal($nub); + + } + + if (toggle) { + settings.$next_tip.hide(); + settings.$next_tip.css('visibility', 'visible'); + } + + }, + + pos_phone : function (init) { + var tip_height = settings.$next_tip.outerHeight(), + tip_offset = settings.$next_tip.offset(), + target_height = settings.$target.outerHeight(), + $nub = $('.joyride-nub', settings.$next_tip), + nub_height = Math.ceil($nub.outerHeight() / 2), + toggle = init || false; + + $nub.removeClass('bottom') + .removeClass('top') + .removeClass('right') + .removeClass('left'); + + if (toggle) { + settings.$next_tip.css('visibility', 'hidden'); + settings.$next_tip.show(); + } + + if (!/body/i.test(settings.$target.selector)) { + + if (methods.top()) { + + settings.$next_tip.offset({top: settings.$target.offset().top - tip_height - nub_height}); + $nub.addClass('bottom'); + + } else { + + settings.$next_tip.offset({top: settings.$target.offset().top + target_height + nub_height}); + $nub.addClass('top'); + + } + + } else if (settings.$li.length) { + + methods.pos_modal($nub); + + } + + if (toggle) { + settings.$next_tip.hide(); + settings.$next_tip.css('visibility', 'visible'); + } + }, + + pos_modal : function ($nub) { + methods.center(); + $nub.hide(); + + if ($('.joyride-modal-bg').length < 1) { + $('body').append('
').show(); + } + + if (/pop/i.test(settings.tipAnimation)) { + $('.joyride-modal-bg').show(); + } else { + $('.joyride-modal-bg').fadeIn(settings.tipAnimationFadeSpeed); + } + }, + + center : function () { + var $w = settings.$window; + + settings.$next_tip.css({ + top : ((($w.height() - settings.$next_tip.outerHeight()) / 2) + $w.scrollTop()), + left : ((($w.width() - settings.$next_tip.outerWidth()) / 2) + $w.scrollLeft()) + }); + + return true; + }, + + bottom : function () { + return /bottom/i.test(settings.tipSettings.tipLocation); + }, + + top : function () { + return /top/i.test(settings.tipSettings.tipLocation); + }, + + right : function () { + return /right/i.test(settings.tipSettings.tipLocation); + }, + + left : function () { + return /left/i.test(settings.tipSettings.tipLocation); + }, + + corners : function (el) { + var w = settings.$window, + right = w.width() + w.scrollLeft(), + bottom = w.width() + w.scrollTop(); + + return [ + el.offset().top <= w.scrollTop(), + right <= el.offset().left + el.outerWidth(), + bottom <= el.offset().top + el.outerHeight(), + w.scrollLeft() >= el.offset().left + ]; + }, + + visible : function (hidden_corners) { + var i = hidden_corners.length; + + while (i--) { + if (hidden_corners[i]) return false; + } + + return true; + }, + + nub_position : function (nub, pos, def) { + if (pos === 'auto') { + nub.addClass(def); + } else { + nub.addClass(pos); + } + }, + + startTimer : function () { + if (settings.$li.length) { + settings.automate = setTimeout(function () { + methods.hide(); + methods.show(); + methods.startTimer(); + }, settings.timer); + } else { + clearTimeout(settings.automate); + } + }, + + end : function () { + if (settings.cookieMonster) { + $.cookie(settings.cookieName, 'ridden', { expires: 365, domain: settings.cookieDomain }); + } + + if (settings.timer > 0) { + clearTimeout(settings.automate); + } + + $('.joyride-modal-bg').hide(); + settings.$current_tip.hide(); + settings.postStepCallback(settings.$li.index(), settings.$current_tip); + settings.postRideCallback(settings.$li.index(), settings.$current_tip); + }, + + jquery_check : function () { + // define on() and off() for older jQuery + if (!$.isFunction($.fn.on)) { + + $.fn.on = function (types, sel, fn) { + + return this.delegate(sel, types, fn); + + }; + + $.fn.off = function (types, sel, fn) { + + return this.undelegate(sel, types, fn); + + }; + + return false; + } + + return true; + }, + + outerHTML : function (el) { + // support FireFox < 11 + return el.outerHTML || new XMLSerializer().serializeToString(el); + }, + + version : function () { + return settings.version; + }, + + tabbable : function (el) { + $(el).on('keydown', function( event ) { + if (!event.isDefaultPrevented() && event.keyCode && + // Escape key. + event.keyCode === 27 ) { + event.preventDefault(); + methods.end(); + return; + } + + // Prevent tabbing out of tour items. + if ( event.keyCode !== 9 ) { + return; + } + var tabbables = $(el).find(":tabbable"), + first = tabbables.filter(":first"), + last = tabbables.filter(":last"); + if ( event.target === last[0] && !event.shiftKey ) { + first.focus( 1 ); + event.preventDefault(); + } else if ( event.target === first[0] && event.shiftKey ) { + last.focus( 1 ); + event.preventDefault(); + } + }); + } + + }; + + $.fn.joyride = function (method) { + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return methods.init.apply(this, arguments); + } else { + $.error('Method ' + method + ' does not exist on jQuery.joyride'); + } + }; + +}(jQuery, this)); diff --git a/core/modules/tour/js/tour.js b/core/modules/tour/js/tour.js new file mode 100644 index 0000000..975a258 --- /dev/null +++ b/core/modules/tour/js/tour.js @@ -0,0 +1,202 @@ +/** + * @file + * Attaches behaviors for the Tour module's toolbar tab. + */ + +(function ($, Backbone, Drupal, document) { + +"use strict"; + +/** + * Attaches the tour's toolbar tab behavior. + */ +Drupal.behaviors.tour = { + attach: function (context) { + var model = new Drupal.tour.models.StateModel(); + var view = new Drupal.tour.views.ToggleTourView({ + el: $(context).find('#toolbar-tab-tour'), + model: model + }); + + // Update the model based on Overlay events. + $(document) + // Overlay is opening: cancel tour if active and mark overlay as open. + .on('drupalOverlayOpen.tour', function () { + model.set({ isActive: false, overlayIsOpen: true }); + }) + // Overlay is loading a new URL: clear tour & cancel if active. + .on('drupalOverlayBeforeLoad.tour', function () { + model.set({ isActive: false, overlayTour: [] }); + }) + // Overlay is closing: clear tour & cancel if active, mark overlay closed. + .on('drupalOverlayClose.tour', function () { + model.set({ isActive: false, overlayIsOpen: false, overlayTour: [] }); + }) + // Overlay has loaded DOM: check whether a tour is available. + .on('drupalOverlayReady.tour', function () { + // We must select the tour in the Overlay's window using the Overlay's + // jQuery, because the joyride plugin only works for the window in which + // it was loaded. + // @todo Make upstream contribution so this can be simplified, which + // should also allow us to *not* load jquery.joyride.js in the Overlay, + // resulting in better front-end performance. + var overlay = Drupal.overlay.iframeWindow; + var $overlayContext = overlay.jQuery(overlay.document); + model.set('overlayTour', $overlayContext.find('#tour')); + }); + + model + // Allow other scripts to respond to tour events. + .on('change:isActive', function (model, isActive) { + $(document).trigger((isActive) ? 'drupalTourStarted' : 'drupalTourStopped'); + }) + // Initialization: check whether a tour is available on the current page. + .set('tour', $(context).find('#tour')); + } +}; + +Drupal.tour = Drupal.tour || { models: {}, views: {}}; + +/** + * Backbone Model for tours. + */ +Drupal.tour.models.StateModel = Backbone.Model.extend({ + defaults: { + // Indicates whether the Drupal root window has a tour. + tour: [], + // Indicates whether the Overlay is open. + overlayIsOpen: false, + // Indicates whether the Overlay window has a tour. + overlayTour: [], + // Indicates whether the tour is currently running. + isActive: false, + // Indicates which tour is the active one (necessary to cleanly stop). + activeTour: [] + } +}); + +/** + * Handles edit mode toggle interactions. + */ +Drupal.tour.views.ToggleTourView = Backbone.View.extend({ + + events: { 'click': 'onClick' }, + + /** + * Implements Backbone Views' initialize(). + */ + initialize: function () { + this.model.on('change:tour change:overlayTour change:overlayIsOpen change:isActive', this.render, this); + this.model.on('change:isActive', this.toggleTour, this); + }, + + /** + * Implements Backbone Views' render(). + */ + render: function () { + // Render the visibility. + this.$el.toggleClass('element-hidden', this._getTour().length === 0); + // Render the state. + var isActive = this.model.get('isActive'); + this.$el.find('button') + .toggleClass('active', isActive) + .attr('aria-pressed', isActive); + return this; + }, + + /** + * Model change handler; starts or stops the tour. + */ + toggleTour: function() { + if (this.model.get('isActive')) { + var $tour = this._getTour(); + this._removeIrrelevantTourItems($tour, this._getDocument()); + var that = this; + $tour.joyride({ + postRideCallback: function () { that.model.set('isActive', false); } + }); + this.model.set({ isActive: true, activeTour: $tour }); + } + else { + this.model.get('activeTour').joyride('destroy'); + this.model.set({ isActive: false, activeTour: [] }); + } + }, + + /** + * Toolbar tab click event handler; toggles isActive. + */ + onClick: function (event) { + this.model.set('isActive', !this.model.get('isActive')); + event.preventDefault(); + event.stopPropagation(); + }, + + /** + * Gets the tour. + * + * @return jQuery + * A jQuery element pointing to a
    containing tour items. + */ + _getTour: function () { + var whichTour = (this.model.get('overlayIsOpen')) ? 'overlayTour' : 'tour'; + return this.model.get(whichTour); + }, + + /** + * Gets the relevant document as a jQuery element. + * + * @return jQuery + * A jQuery element pointing to the document within which a tour would be + * started given the current state. I.e. when the Overlay is open, this will + * point to the HTML document inside the Overlay's iframe, otherwise it will + * point to the Drupal root window. + */ + _getDocument: function () { + return (this.model.get('overlayIsOpen')) ? $(Drupal.overlay.iframeWindow.document) : $(document); + }, + + /** + * Removes tour items for elements that don't exist. + * + * @param jQuery $tour + * A jQuery element pointing to a
      containing tour items. + * @param jQuery $document + * A jQuery element pointing to the document within which the elements + * should be sought. + * + * @see _getDocument() + */ + _removeIrrelevantTourItems: function ($tour, $document) { + var removals = false; + $tour + .find('li') + .each(function () { + var $this = $(this); + var itemId = $this.attr('data-id'); + var itemClass = $this.attr('data-class'); + if ($document.find('#' + itemId + ', .' + itemClass).length === 0) { + removals = true; + $this.remove(); + } + }); + + // If there were removals, we'll have to do some clean-up. + if (removals) { + var total = $tour.find('li').length; + $tour + .find('li') + // Rebuild the progress data. + .each(function (index) { + var progress = Drupal.t('!tour_item of !total', { '!tour_item': index + 1, '!total': total }); + $(this).find('.tour-progress').text(progress); + }) + // Update the last item to have "End tour" as the button. + .last() + .attr('data-text', Drupal.t('End tour')); + } + } + +}); + +})(jQuery, Backbone, Drupal, document); diff --git a/core/modules/tour/lib/Drupal/tour/Plugin/Core/Entity/Tour.php b/core/modules/tour/lib/Drupal/tour/Plugin/Core/Entity/Tour.php new file mode 100644 index 0000000..71ce552 --- /dev/null +++ b/core/modules/tour/lib/Drupal/tour/Plugin/Core/Entity/Tour.php @@ -0,0 +1,136 @@ +tipsBag = new TipsBag(drupal_container()->get('plugin.manager.tour'), $this->tips); + } + + /** + * Returns label of tour. + * + * @return string + * The label of the tour. + */ + public function getLabel() { + return $this->label; + } + + /** + * The paths that this tour will appear on. + * + * @return array + * Returns array of paths for the tour. + */ + public function getPaths() { + return $this->paths; + } + + /** + * Returns tip plugin. + * + * @return string + * The identifier of the tip. + */ + public function getTip($id) { + return $this->tipsBag->get($id); + } + + /** + * Returns a list of tips. + * + * @return array + * A list of tips. + */ + public function getTipList() { + return array_keys($this->tips); + } + + /** + * Overrides \Drupal\Core\Config\Entity\ConfigEntityBase::getExportProperties(); + */ + public function getExportProperties() { + $properties = parent::getExportProperties(); + $names = array( + 'id', + 'label', + 'paths', + 'tips', + ); + foreach ($names as $name) { + $properties[$name] = $this->get($name); + } + return $properties; + } +} diff --git a/core/modules/tour/lib/Drupal/tour/Plugin/tour/tip/TipPluginText.php b/core/modules/tour/lib/Drupal/tour/Plugin/tour/tip/TipPluginText.php new file mode 100644 index 0000000..c492d8e --- /dev/null +++ b/core/modules/tour/lib/Drupal/tour/Plugin/tour/tip/TipPluginText.php @@ -0,0 +1,95 @@ +get('id')); + } + return $id; + } + + /** + * Returns body of the text tip. + * + * @return string + * The body of the text tip. + */ + public function getBody() { + return $this->get('body'); + } + + /** + * Returns location of the text tip. + * + * @return string + * The location (left|right|top|bottom) of the text tip. + */ + public function getLocation() { + return $this->get('location'); + } + + /** + * Overrides \Drupal\tour\Plugin\tour\tour\TipPluginInterface::getAttributes(); + */ + public function getAttributes() { + $attributes = parent::getAttributes(); + $attributes['data-aria-describedby'] = 'tour-tip-' . $this->getAriaId() . '-contents'; + $attributes['data-aria-labelledby'] = 'tour-tip-' . $this->getAriaId() . '-label'; + if ($location = $this->get('location')) { + $attributes['data-options'] = 'tipLocation:' . $location; + } + return $attributes; + } + + /** + * Overrides \Drupal\tour\Plugin\tour\tour\TipPluginInterface::getOutput(); + */ + public function getOutput() { + return array( + '#markup' => '

      ' . check_plain($this->getLabel()) . '

      +

      ' . filter_xss_admin($this->getBody()) . '

      ' + ); + } +} diff --git a/core/modules/tour/lib/Drupal/tour/Tests/TourPluginTest.php b/core/modules/tour/lib/Drupal/tour/Tests/TourPluginTest.php new file mode 100644 index 0000000..188e83c --- /dev/null +++ b/core/modules/tour/lib/Drupal/tour/Tests/TourPluginTest.php @@ -0,0 +1,59 @@ + 'Tour plugin tests', + 'description' => 'Test the functionality of tour plugins.', + 'group' => 'Tour', + ); + } + + /** + * Sets up the test. + */ + protected function setUp() { + parent::setUp(); + + config_install_default_config('module', 'tour'); + $this->pluginManager = $this->container->get('plugin.manager.tour'); + } + + /** + * Test tour plugins. + */ + public function testTourPlugins() { + $this->assertIdentical(count($this->pluginManager->getDefinitions()), 1, 'Only tour plugins for the enabled modules were returned.'); + } + +} diff --git a/core/modules/tour/lib/Drupal/tour/Tests/TourTest.php b/core/modules/tour/lib/Drupal/tour/Tests/TourTest.php new file mode 100644 index 0000000..58e50f9 --- /dev/null +++ b/core/modules/tour/lib/Drupal/tour/Tests/TourTest.php @@ -0,0 +1,149 @@ + 'Tour tests', + 'description' => 'Test the functionality of tour tips.', + 'group' => 'Tour', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->drupalLogin($this->drupalCreateUser(array('access tour', 'administer languages'))); + } + + /** + * Test tour functionality. + */ + public function testTourFunctionality() { + // Navigate to tour-test-1 and verify the tour_test_1 tip is found. + $this->drupalGet('tour-test-1'); + $elements = $this->xpath('//li[@data-id=:data_id and ./h2[contains(., :text)]]', array( + ':data_id' => 'tour-test-1', + ':text' => 'The first tip' + )); + $this->assertEqual(count($elements), 1, 'Found English variant of tip 1.'); + + $elements = $this->xpath('//li[@data-id=:data_id and ./h2[contains(., :text)]]', array( + ':data_id' => 'tour-test-2', + ':text' => 'The quick brown fox' + )); + $this->assertNotEqual(count($elements), 1, 'Did not find English variant of tip 2.'); + + $elements = $this->xpath('//li[@data-id=:data_id and ./h2[contains(., :text)]]', array( + ':data_id' => 'tour-test-1', + ':text' => 'La pioggia cade in spagna' + )); + $this->assertNotEqual(count($elements), 1, 'Did not find Italian variant of tip 1.'); + + // Ensure that plugin's work. + $this->assertRaw('img src="http://local/image.png"', 'Image plugin tip found.'); + + // Navigate to tour-test-2/subpath and verify the tour_test_2 tip is found. + $this->drupalGet('tour-test-2/subpath'); + $elements = $this->xpath('//li[@data-id=:data_id and ./h2[contains(., :text)]]', array( + ':data_id' => 'tour-test-2', + ':text' => 'The quick brown fox' + )); + $this->assertEqual(count($elements), 1, 'Found English variant of tip 2.'); + + $elements = $this->xpath('//li[@data-id=:data_id and ./h2[contains(., :text)]]', array( + ':data_id' => 'tour-test-1', + ':text' => 'The first tip' + )); + $this->assertNotEqual(count($elements), 1, 'Did not find English variant of tip 1.'); + + // Enable Italian language and navigate to it/tour-test1 and verify italian + // version of tip is found. + language_save(new Language(array('langcode' => 'it'))); + $this->drupalGet('it/tour-test-1'); + + $elements = $this->xpath('//li[@data-id=:data_id and ./h2[contains(., :text)]]', array( + ':data_id' => 'tour-test-1', + ':text' => 'La pioggia cade in spagna' + )); + $this->assertEqual(count($elements), 1, 'Found Italian variant of tip 1.'); + + $elements = $this->xpath('//li[@data-id=:data_id and ./h2[contains(., :text)]]', array( + ':data_id' => 'tour-test-1', + ':text' => 'The first tip' + )); + $this->assertNotEqual(count($elements), 1, 'Did not find English variant of tip 1.'); + + language_save(new Language(array('langcode' => 'en'))); + + // Programmatically create a tour for use through the remainder of the test. + entity_create('tour', array( + 'id' => 'tour-entity-create-test-en', + 'label' => 'Tour test english', + 'langcode' => 'en', + 'paths' => array( + 'tour-test-1', + ), + 'tips' => array( + 'tour-test-1' => array( + 'id' => 'tour-code-test-1', + 'plugin' => 'text', + 'label' => 'The rain in spain', + 'body' => 'Falls mostly on the plain.', + 'weight' => '100', + 'attributes' => array( + 'data-id' => 'tour-code-test-1', + ), + ), + ), + ))->save(); + + $this->drupalGet('tour-test-1'); + + // Load it back from the database and verify storage worked. + $entity_save_tip = entity_load('tour', 'tour-entity-create-test-en'); + // Verify that hook_ENTITY_TYPE_load() integration worked. + $this->assertEqual($entity_save_tip->loaded, 'Load hooks work'); + // Verify that hook_ENTITY_TYPE_presave() integration worked. + $this->assertEqual($entity_save_tip->label(), 'Tour test english alter'); + + // Navigate to tour-test-1 and verify the new tip is found. + $this->drupalGet('tour-test-1'); + $elements = $this->xpath('//li[@data-id=:data_id and ./h2[contains(., :text)]]', array( + ':data_id' => 'tour-code-test-1', + ':text' => 'The rain in spain' + )); + $this->assertEqual(count($elements), 1, 'Found the required tip markup for tip 4'); + + // Verify that the weight sorting works by ensuring the lower weight item + // (tip 4) has the close button. + $elements = $this->xpath('//li[@data-id=:data_id and @data-text=:text]', array( + ':data_id' => 'tour-code-test-1', + ':text' => 'End tour' + )); + $this->assertEqual(count($elements), 1, 'Found code tip was weighted last and had "End tour".'); + + // Test hook_tour_alter(). + $this->assertText('Altered by hook_tour_tips_alter'); + } +} diff --git a/core/modules/tour/lib/Drupal/tour/TipPluginBase.php b/core/modules/tour/lib/Drupal/tour/TipPluginBase.php new file mode 100644 index 0000000..4779061 --- /dev/null +++ b/core/modules/tour/lib/Drupal/tour/TipPluginBase.php @@ -0,0 +1,88 @@ +definition = $this->discovery->getDefinition($plugin_id); + $this->module = $this->definition['module']; + } + + /** + * Implements \Drupal\tour\Plugin\tour\tour\TourInterface::getLabel(). + */ + public function getLabel() { + return $this->get('label'); + } + + /** + * Implements \Drupal\tour\Plugin\tour\tour\TourInterface::getWeight(). + */ + public function getWeight() { + return $this->get('weight'); + } + + /** + * Implements \Drupal\tour\Plugin\tour\tour\TourInterface::getAttributes(). + */ + public function getAttributes() { + return $this->get('attributes'); + } + + /** + * Implements \Drupal\tour\Plugin\tour\tour\TourInterface::get(). + */ + public function get($key) { + if (!empty($this->configuration[$key])) { + return $this->configuration[$key]; + } + } + + /** + * Implements \Drupal\tour\Plugin\tour\tour\TourInterface::set(). + */ + public function set($key, $value) { + $this->configuration[$key] = $value; + } +} diff --git a/core/modules/tour/lib/Drupal/tour/TipPluginInterface.php b/core/modules/tour/lib/Drupal/tour/TipPluginInterface.php new file mode 100644 index 0000000..3f376ef --- /dev/null +++ b/core/modules/tour/lib/Drupal/tour/TipPluginInterface.php @@ -0,0 +1,68 @@ +manager = $manager; + $this->configurations = $configurations; + + if (!empty($configurations)) { + $this->instanceIDs = array_combine(array_keys($configurations), array_keys($configurations)); + } + } + + /** + * Overrides \Drupal\Component\Plugin\PluginBag::initializePlugin(). + */ + protected function initializePlugin($instance_id) { + // If the tip was initialized before, just return. + if (isset($this->pluginInstances[$instance_id])) { + return; + } + + $type = $this->configurations[$instance_id]['plugin']; + $definition = $this->manager->getDefinition($type); + + if (isset($definition)) { + $this->addInstanceID($instance_id); + $configuration = $definition; + + // Merge the actual configuration into the default configuration. + if (isset($this->configurations[$instance_id])) { + $configuration = NestedArray::mergeDeep($configuration, $this->configurations[$instance_id]); + } + $this->pluginInstances[$instance_id] = $this->manager->createInstance($type, $configuration, $this); + } + else { + throw new PluginException(format_string("Unknown tip plugin ID '@tip'.", array('@tip' => $instance_id))); + } + } +} diff --git a/core/modules/tour/lib/Drupal/tour/TourBundle.php b/core/modules/tour/lib/Drupal/tour/TourBundle.php new file mode 100644 index 0000000..337a672 --- /dev/null +++ b/core/modules/tour/lib/Drupal/tour/TourBundle.php @@ -0,0 +1,26 @@ +register('plugin.manager.tour', 'Drupal\tour\TourManager'); + } +} diff --git a/core/modules/tour/lib/Drupal/tour/TourManager.php b/core/modules/tour/lib/Drupal/tour/TourManager.php new file mode 100644 index 0000000..633006d --- /dev/null +++ b/core/modules/tour/lib/Drupal/tour/TourManager.php @@ -0,0 +1,51 @@ +discovery = new AnnotatedClassDiscovery('tour', 'tip'); + $this->discovery = new ProcessDecorator($this->discovery, array($this, 'processDefinition')); + $this->discovery = new CacheDecorator($this->discovery, 'tour'); + $this->factory = new DefaultFactory($this->discovery); + } + + /** + * Overrides \Drupal\Component\Plugin\PluginManagerBase::createInstance(). + */ + public function createInstance($plugin_id, array $configuration = array(), TipsBag $bag = NULL) { + $plugin_class = DefaultFactory::getPluginClass($plugin_id, $this->discovery); + return new $plugin_class($configuration, $plugin_id, $this->discovery, $bag); + } + + /** + * Overrides \Drupal\Component\Plugin\PluginManagerBase::processDefinition(). + */ + public function processDefinition(&$definition, $plugin_id) { + parent::processDefinition($definition, $plugin_id); + + // @todo Remove this check once http://drupal.org/node/1780396 is resolved. + if (!module_exists($definition['module'])) { + $definition = NULL; + return; + } + } +} diff --git a/core/modules/tour/tests/tour_test/config/tour.tour.tour-test-2-en.yml b/core/modules/tour/tests/tour_test/config/tour.tour.tour-test-2-en.yml new file mode 100644 index 0000000..6816aa7 --- /dev/null +++ b/core/modules/tour/tests/tour_test/config/tour.tour.tour-test-2-en.yml @@ -0,0 +1,14 @@ +id: tour-test-2-en +label: Tour test english +langcode: en +paths: + - tour-test-2/* +tips: + tour-test-2: + id: tour-test-2-en + plugin: text + label: The quick brown fox + body: Per lo più in pianura. + weight: "2" + attributes: + data-id: tour-test-2 diff --git a/core/modules/tour/tests/tour_test/config/tour.tour.tour-test-en.yml b/core/modules/tour/tests/tour_test/config/tour.tour.tour-test-en.yml new file mode 100644 index 0000000..5271213 --- /dev/null +++ b/core/modules/tour/tests/tour_test/config/tour.tour.tour-test-en.yml @@ -0,0 +1,22 @@ +id: tour-test-en +label: Tour test english +langcode: en +paths: + - tour-test-1 +tips: + tour-test-1: + id: tour-test-1-en + plugin: text + label: The first tip + body: Is always the best dressed. + weight: "1" + attributes: + data-id: tour-test-1 + tour-test-3: + id: tour-test-3-en + plugin: image + label: The awesome image + url: http://local/image.png + weight: "1" + attributes: + data-id: tour-test-3 diff --git a/core/modules/tour/tests/tour_test/config/tour.tour.tour-test-it.yml b/core/modules/tour/tests/tour_test/config/tour.tour.tour-test-it.yml new file mode 100644 index 0000000..86f0a25 --- /dev/null +++ b/core/modules/tour/tests/tour_test/config/tour.tour.tour-test-it.yml @@ -0,0 +1,14 @@ +id: tour-test-it +label: Tour test italian +langcode: it +paths: + - tour-test-1 +tips: + tour-test-1-it: + id: tour-test-1-it + plugin: text + label: La pioggia cade in spagna + body: Per lo più in pianura. + weight: "1" + attributes: + data-id: tour-test-1 diff --git a/core/modules/tour/tests/tour_test/lib/Drupal/tour_test/Plugin/tour/tip/TipPluginImage.php b/core/modules/tour/tests/tour_test/lib/Drupal/tour_test/Plugin/tour/tip/TipPluginImage.php new file mode 100644 index 0000000..af280a3 --- /dev/null +++ b/core/modules/tour/tests/tour_test/lib/Drupal/tour_test/Plugin/tour/tip/TipPluginImage.php @@ -0,0 +1,48 @@ + '

      ' . check_plain($this->get('label')) . '

      +

      ' . theme('image', array('uri' => $this->get('url'), 'alt' => $this->get('alt'))) . '

      ' + ); + } +} diff --git a/core/modules/tour/tests/tour_test/tour_test.info b/core/modules/tour/tests/tour_test/tour_test.info new file mode 100644 index 0000000..afc405d --- /dev/null +++ b/core/modules/tour/tests/tour_test/tour_test.info @@ -0,0 +1,7 @@ +name = Tour module tests +description = Tests module for tour module. +package = Core +version = VERSION +core = 8.x +hidden = TRUE +dependencies[] = tour diff --git a/core/modules/tour/tests/tour_test/tour_test.module b/core/modules/tour/tests/tour_test/tour_test.module new file mode 100644 index 0000000..a2b7bda --- /dev/null +++ b/core/modules/tour/tests/tour_test/tour_test.module @@ -0,0 +1,87 @@ + 'tour_test_1', + 'access callback' => TRUE, + 'title' => 'Tour test 1' + ); + $items['tour-test-2/subpath'] = array( + 'page callback' => 'tour_test_2', + 'access callback' => TRUE, + 'title' => 'Tour test 2' + ); + return $items; +} + +/** + * Implements hook_ENTITY_TYPE_load() for tour. + */ +function tour_test_tour_load($entities) { + if (isset($entities['tour-entity-create-test-en'])) { + $entities['tour-entity-create-test-en']->loaded = 'Load hooks work'; + } +} + +/** + * Implements hook_ENTITY_TYPE_presave() for tour. + */ +function tour_test_tour_presave($entity) { + if ($entity->id() == 'tour-entity-create-test-en') { + $entity->set('label', $entity->label() . ' alter'); + } +} + +/** + * Page callback: output some content for testing tours. + */ +function tour_test_1() { + return array( + 'tip-1' => array( + '#type' => 'container', + '#attributes' => array( + 'id' => 'tour-test-1', + ), + '#children' => t('Where does the rain in Spain fail?'), + ), + 'tip-4' => array( + '#type' => 'container', + '#attributes' => array( + 'id' => 'tour-test-4', + ), + '#children' => t('Tip created later?'), + ), + ); +} + +/** + * Page callback: output some content for testing tours. + */ +function tour_test_2() { + return array( + '#type' => 'container', + '#attributes' => array( + 'id' => 'tour-test-2', + ), + '#children' => t('Pangram example'), + ); +} + +/** + * Implements hook_tour_alter(). + */ +function tour_test_tour_tips_alter(array &$tour_tips, $path) { + foreach ($tour_tips as $tour_tip) { + if ($tour_tip->get('id') == 'tour-code-test-1') { + $tour_tip->set('body', 'Altered by hook_tour_tips_alter'); + } + } +} diff --git a/core/modules/tour/tour.api.php b/core/modules/tour/tour.api.php new file mode 100644 index 0000000..a3672b3 --- /dev/null +++ b/core/modules/tour/tour.api.php @@ -0,0 +1,73 @@ +get('id') == 'tour-code-test-1') { + $tour_tip->set('body', 'Altered by hook_tour_tips_alter'); + } + } +} + +/** + * Act on tour objects when loaded. + * + * @param array $entities + * An array of \Drupal\tour\Plugin\Core\Entity\Tour objects, indexed by id. + */ +function hook_tour_load($entities) { + if (isset($entities['tour-entity-create-test-en'])) { + $entities['tour-entity-create-test-en']->loaded = 'Load hooks work'; + } +} + +/** + * Act on a tour being inserted or updated. + * + * This hook is invoked before the tour object is saved to configuration. + * + * @param \Drupal\tour\Plugin\Core\Entity\Tour $entity + * The tour object. + * + * @see hook_tour_insert() + * @see hook_tour_update() + */ +function hook_tour_presave($entity) { + if ($entity->id() == 'tour-entity-create-test-en') { + $entity->set('label', $entity->label() . ' alter'); + } +} + +/** + * Respond to creation of a new tour. + * + * @param \Drupal\tour\Plugin\Core\Entity\Tour $entity + * The tour object being inserted. + */ +function hook_tour_insert($entity) { + drupal_container()->get('plugin.manager.tour')->clearCachedDefinitions(); + cache('cache_tour')->deleteTags(array('tour_items')); +} + +/** + * Respond to updates to a tour object. + * + * @param \Drupal\tour\Plugin\Core\Entity\Tour $entity + * The tour object being updated. + */ +function hook_tour_update($entity) { + drupal_container()->get('plugin.manager.tour')->clearCachedDefinitions(); + cache('cache_tour')->deleteTags(array('tour_items')); +} diff --git a/core/modules/tour/tour.info b/core/modules/tour/tour.info new file mode 100644 index 0000000..341cb0d --- /dev/null +++ b/core/modules/tour/tour.info @@ -0,0 +1,5 @@ +name = Tour +description = Provides guided tours. +package = Core +version = VERSION +core = 8.x diff --git a/core/modules/tour/tour.install b/core/modules/tour/tour.install new file mode 100644 index 0000000..e22be72 --- /dev/null +++ b/core/modules/tour/tour.install @@ -0,0 +1,16 @@ + array( + 'title' => t('Access tour'), + 'description' => t('View tour tips.'), + ), + ); +} + +/** + * Implements hook_library_info(). + */ +function tour_library_info() { + $path = drupal_get_path('module', 'tour'); + + $libraries['tour'] = array( + 'title' => 'Tour', + 'version' => VERSION, + 'js' => array( + // Add the JavaScript, with a group and weight such that it will run + // before modules/overlay/overlay-parent.js. + $path . '/js/tour.js' => array('group' => JS_LIBRARY, 'weight' => -1), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('system', 'backbone'), + array('tour', 'jquery.joyride'), + array('tour', 'tour-styling'), + ), + ); + + $libraries['tour-styling'] = array( + 'title' => 'Tour', + 'version' => VERSION, + 'css' => array( + $path . '/css/tour.css' => array('media' => 'screen'), + ) + ); + + $libraries['jquery.joyride'] = array( + 'title' => 'Joyride', + 'website' => 'https://github.com/zurb/joyride', + 'version' => '2.0.3', + 'js' => array( + $path . '/js/jquery.joyride-2.0.3.js' => array(), + ), + 'css' => array( + $path . '/css/joyride-2.0.3.css' => array('media' => 'screen'), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'jquery.cookie'), + ), + ); + + return $libraries; +} + +/** + * Implements hook_toolbar(). + */ +function tour_toolbar() { + if (!user_access('access tour')) { + return; + } + + $tab['tour'] = array( + '#type' => 'toolbar_item', + 'tab' => array( + '#type' => 'html_tag', + '#tag' => 'button', + '#value' => t('Tour'), + '#attributes' => array( + 'class' => array('icon', 'icon-help'), + 'role' => 'button', + 'aria-pressed' => 'false', + ), + ), + '#wrapper_attributes' => array( + 'class' => array('tour-toolbar-tab', 'element-hidden'), + 'id' => 'toolbar-tab-tour', + ), + '#attached' => array( + 'library' => array( + array('tour', 'tour'), + ), + ), + ); + + return $tab; +} + +/** + * Implements hook_preprocess_HOOK() for page.tpl.php. + */ +function tour_preprocess_page(&$variables) { + $path = current_path(); + $langcode = language(LANGUAGE_TYPE_CONTENT)->langcode; + $cid = $path . ':' . $langcode; + $cache = cache('cache_tour')->get($cid); + $tour_items = array(); + if ($cache) { + // Cache hit. + $tour_items = $cache->data; + } + else { + // Load all of the items and match on path. + $tour_ids = entity_query('tour') + ->condition('langcode', $langcode) + ->execute(); + $tours = entity_load_multiple('tour', $tour_ids); + + $path_alias = drupal_strtolower(drupal_container()->get('path.alias_manager')->getPathAlias($path)); + foreach ($tours as $tour) { + // @todo replace this with an entity_query() that does path matching when + // http://drupal.org/node/1918768 lands. + $pages = implode("\n", $tour->getPaths()); + if (drupal_match_path($path_alias, $pages) || (($path != $path_alias) && drupal_match_path($path, $pages))) { + foreach ($tour->getTipList() as $id) { + $tour_items[] = $tour->getTip($id); + } + } + } + + // Allow other modules to alter. + drupal_container()->get('module_handler')->alter('tour_tips', $tour_items, $path); + + // Cache for future requests. + cache('cache_tour')->set($cid, $tour_items, CacheBackendInterface::CACHE_PERMANENT, array('tour_items')); + } + + if (empty($tour_items)) { + return; + } + + // Sort by weight. + uasort($tour_items, function ($a, $b) { + if ($a->getWeight() == $b->getWeight()) { + return 0; + } + return ($a->getWeight() < $b->getWeight()) ? -1 : 1; + }); + + $index = 1; + $count = count($tour_items); + foreach ($tour_items as $tour_item) { + $list_items[] = array( + 'output' => $tour_item->getOutput(), + 'counter' => array( + '#type' => 'container', + '#attributes' => array( + 'class' => array( + 'tour-progress', + ), + ), + '#children' => t('!tour_item of !total', array('!tour_item' => $index, '!total' => $count)), + ), + '#wrapper_attributes' => $tour_item->getAttributes(), + ); + $index++; + } + + // Give the last tip the "End tour" button. + end($list_items); + $key = key($list_items); + $list_items[$key]['#wrapper_attributes']['data-text'] = t('End tour'); + + $variables['page']['help']['tour'] = array( + '#theme' => 'item_list', + '#items' => $list_items, + '#type' => 'ol', + '#attributes' => array( + 'id' => 'tour', + 'class' => array( + 'element-hidden', + ), + ), + '#attached' => array( + 'library' => array( + // We must also attach the jquery.joyride library here, because it only + // works within the window within which it is loaded. This means that if + // we want the Tour module to work inside the Overlay, we must ensure + // that jquery.joyride also is loaded there. (And since the Toolbar does + // not get loaded inside the Overlay, we cannot rely on it being loaded + // that way.) + // If this a non-overlay page, then Drupal's dependency checking will + // ensure this gets loaded only once. + array('tour', 'jquery.joyride'), + // Similarly, we must load tour's CSS, in order to style the tour tips + // in the desired way inside the Overlay. + array('tour', 'tour-styling'), + ), + ), + ); +} + +/** + * Implements hook_tour_tour_insert(). + */ +function tour_tour_insert($entity) { + drupal_container()->get('plugin.manager.tour')->clearCachedDefinitions(); + cache('cache_tour')->deleteTags(array('tour_items')); +} + +/** + * Implements hook_tour_tour_update(). + */ +function tour_tour_update($entity) { + drupal_container()->get('plugin.manager.tour')->clearCachedDefinitions(); + cache('cache_tour')->deleteTags(array('tour_items')); +} diff --git a/core/modules/views/views_ui/config/tour.tour.views-ui-en.yml b/core/modules/views/views_ui/config/tour.tour.views-ui-en.yml new file mode 100644 index 0000000..0e80903 --- /dev/null +++ b/core/modules/views/views_ui/config/tour.tour.views-ui-en.yml @@ -0,0 +1,87 @@ +id: views-ui-en +label: Views ui +langcode: en +paths: + - admin/structure/views/view/*/edit +tips: + views-ui-active-display-en: + id: views-ui-active-display-en + plugin: text + label: Active display + body: This is the active display in the view. When there are multiple displays, one link for each display is shown and you can switch displays simply by clicking on the display link. + weight: "2" + attributes: + data-class: views-display-top li.active + views-ui-displays-en: + id: views-ui-displays-en + plugin: text + label: Displays in this view + body: A view can consist of multiple displays. A display is a way of outputting the results E.g. as a page or in a block. The available displays in your view are show here. + weight: "1" + attributes: + data-id: views-display-top + views-ui-fields-en: + id: views-ui-fields-en + plugin: text + label: Fields + body: This section shows the fields output for each result. Depending on the format selected for the view, you may not see anything listed here. If the format of your view uses fields, you can click on each field displayed to configure it. + weight: "5" + attributes: + data-class: views-ui-display-tab-bucket.fields + views-ui-filter-en: + id: views-ui-filter-en + plugin: text + label: Filter your view + body: This section displays the filters you have active in your view. A filter is used to limit the results available in the output. E.g. to only show content that was published, you would add a filter for Published and select Yes. + weight: "6" + attributes: + data-class: views-ui-display-tab-bucket.filter-criteria + views-ui-filter-operations-en: + id: views-ui-filter-operations-en + plugin: text + label: Filter actions + body: Use this drop-button to add and re-arrange filters + weight: "7" + attributes: + data-class: views-ui-display-tab-bucket.filter-criteria .dropbutton-widget + views-ui-format-en: + id: views-ui-format-en + plugin: text + label: Output format + body: Use this section to manage the format of the output results. You can choose different ways in which the matching results are output. E.g. Choose Content to output each item completely, using your configured display settings. Other options include Fields which allows you to output only specific fields on each matching result. Additional formats can be added by installing additional module to extend Drupal's base functionality. + weight: "4" + attributes: + data-class: views-ui-display-tab-bucket.format + views-ui-preview-en: + id: views-ui-preview-en + plugin: text + label: Preview + body: Use this button to show a preview of the view output + weight: "10" + attributes: + data-id: preview-submit + views-ui-sorts-en: + id: views-ui-sorts-en + plugin: text + label: Sort Criteria + body: This section shows the enabled sorting criteria for the view. Sorting criteria are used to control the order in which the results are output. Clicking on any of the active sorting criteria shown in this section enables you to configure it. + weight: "8" + attributes: + data-class: views-ui-display-tab-bucket.sort-criteria + views-ui-sorts-operations-en: + id: views-ui-sorts-operations-en + plugin: text + label: Sort actions + body: Use this drop-button to add and re-arrange the sorting criteria. + weight: "9" + attributes: + data-class: views-ui-display-tab-bucket.sort-criteria .dropbutton-widget + views-ui-view-admin-en: + id: views-ui-view-admin-en + plugin: text + label: View administration + body: Use this drop-button to perform administrative tasks on the view, including adding a description and creating a clone. Click the drop button to view the available options. + weight: "3" + location: left + attributes: + data-id: views-display-extra-actions diff --git a/core/profiles/standard/standard.info b/core/profiles/standard/standard.info index eb98d8a..fed2c99 100644 --- a/core/profiles/standard/standard.info +++ b/core/profiles/standard/standard.info @@ -31,3 +31,4 @@ dependencies[] = file dependencies[] = rdf dependencies[] = views dependencies[] = views_ui +dependencies[] = tour