Issue #1809352 by nick_schuch, larowlan, jthorson, Wim Leers, Devin Carlson, effulgentsia, tim.plunkett, jessebeach | cweagans: Added Write tour.module and add it to core.

8.0.x
Dries 2013-02-18 17:06:10 -05:00
parent 395d3a6bf1
commit 9e49307da3
25 changed files with 2462 additions and 0 deletions

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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' : '<a href="#close" class="joyride-close-tip">X</a>',
'timer' : '<div class="joyride-timer-indicator-wrap"><span class="joyride-timer-indicator"></span></div>',
'tip' : '<div class="joyride-tip-guide"><span class="joyride-nub"></span></div>',
'wrapper' : '<div class="joyride-content-wrapper" role="dialog"></div>',
'button' : '<a href="#" class="joyride-next-tip"></a>'
}
},
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('<div class="joyride-modal-bg">').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));

View File

@ -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 <ol> 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 <ol> 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);

View File

@ -0,0 +1,136 @@
<?php
/**
* @file
* Contains \Drupal\tour\Plugin\Core\Entity\Tour.
*/
namespace Drupal\tour\Plugin\Core\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
use Drupal\tour\TipsBag;
/**
* Defines the configured text tour entity.
*
* @Plugin(
* id = "tour",
* label = @Translation("Tour"),
* module = "tour",
* controller_class = "Drupal\Core\Config\Entity\ConfigStorageController",
* config_prefix = "tour.tour",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* "uuid" = "uuid",
* }
* )
*/
class Tour extends ConfigEntityBase {
/**
* The name (plugin ID) of the tour.
*
* @var string
* Unique identifier for this tour.
*/
public $id;
/**
* The label of the tour.
*
* @var string
* A human readable name for this tour.
*/
public $label;
/**
* The paths in which this tip can be displayed.
*
* @var array
* An array of paths.
*/
protected $paths;
/**
* Holds the collection of tips that are attached to this tour.
*
* @var \Drupal\tour\TipsBag
*/
protected $tipsBag;
/**
* The array of plugin config, only used for export and to populate the $tipsBag.
*
* @var array
*/
protected $tips;
/**
* Overrides \Drupal\Core\Config\Entity\ConfigEntityBase::__construct();
*/
public function __construct(array $values, $entity_type) {
parent::__construct($values, $entity_type);
$this->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;
}
}

View File

@ -0,0 +1,95 @@
<?php
/**
* @file
* Contains \Drupal\tour\TipPluginText.
*/
namespace Drupal\tour\Plugin\tour\tip;
use Drupal\Core\Annotation\Plugin;
use Drupal\tour\TipPluginBase;
/**
* Displays some text as a tip.
*
* @Plugin(
* id = "text",
* module = "tour"
* )
*/
class TipPluginText extends TipPluginBase {
/**
* The body text which is used for render of this Text Tip.
*
* @var string
* A string of text used as the body.
*/
protected $body;
/**
* The forced position of where the tip will be located.
*
* @var string
* A string of left|right|top|bottom.
*/
protected $location;
/**
* Returns a ID that is guaranteed uniqueness.
*
* @return string
* A unique string.
*/
public function getAriaId() {
static $id;
if (!isset($id)) {
$id = drupal_html_id($this->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' => '<h2 class="tour-tip-label" id="tour-tip-' . $this->getAriaId() . '-label">' . check_plain($this->getLabel()) . '</h2>
<p class="tour-tip-body" id="tour-tip-' . $this->getAriaId() . '-contents">' . filter_xss_admin($this->getBody()) . '</p>'
);
}
}

View File

@ -0,0 +1,59 @@
<?php
/**
* @file
* Contains \Drupal\tour\Tests\TourPluginTest.
*/
namespace Drupal\tour\Tests;
use Drupal\simpletest\DrupalUnitTestBase;
/**
* Tests tour plugin functionality.
*/
class TourPluginTest extends DrupalUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('tour');
/**
* Stores the tour plugin manager.
*
* @var \Drupal\tour\TourManager
*/
protected $pluginManager;
/**
* Defines test info.
*/
public static function getInfo() {
return array(
'name' => '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.');
}
}

View File

@ -0,0 +1,149 @@
<?php
/**
* @file
* Contains \Drupal\tour\Tests\TourTest.
*/
namespace Drupal\tour\Tests;
use Drupal\Core\Language\Language;
use Drupal\simpletest\WebTestBase;
/**
* Tests tour functionality.
*/
class TourTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('tour', 'locale', 'language', 'tour_test');
public static function getInfo() {
return array(
'name' => '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 ./div[contains(., :text)]]', array(
':data_id' => 'tour-code-test-1',
':text' => '3 of 3'
));
$this->assertEqual(count($elements), 1, 'Found code tip was weighted last and had "3 of 3".');
// Test hook_tour_alter().
$this->assertText('Altered by hook_tour_tips_alter');
}
}

View File

@ -0,0 +1,88 @@
<?php
/**
* @file
* Contains \Drupal\tour\TipPluginBase.
*/
namespace Drupal\tour;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Plugin\Discovery\CacheDecorator;
use Drupal\tour\TipPluginInterface;
/**
* Defines a base tour implementation.
*/
abstract class TipPluginBase extends PluginBase implements TipPluginInterface {
/**
* The label which is used for render of this tip.
*
* @var string
* The label of this tip.
*/
protected $label;
/**
* Allows tips to take more priority that others.
*
* @var string
* A number which pertains to ordering.
*/
protected $weight;
/**
* The attributes that will be applied to the markup of this tip.
*
* @var array
* An array of attributes.
*/
protected $attributes;
/**
* Overrides \Drupal\Component\Plugin\PluginBase::__construct().
*/
public function __construct(array $configuration, $plugin_id, CacheDecorator $discovery) {
parent::__construct($configuration, $plugin_id, $discovery);
$this->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;
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* @file
* Contains \Drupal\tour\TipPluginInterface.
*/
namespace Drupal\tour;
/**
* Defines an interface for tour items.
*/
interface TipPluginInterface {
/**
* Returns label of the tip.
*
* @return string
* The label of the tip.
*/
public function getLabel();
/**
* Returns weight of the tip.
*
* @return string
* The weight of the tip.
*/
public function getWeight();
/**
* Returns an array of attributes for the tip wrapper.
*
* @return array
* An array of classes and values.
*/
public function getAttributes();
/**
* Used for returning values by key.
*
* @var string
* Key of the value.
*
* @return string
* Value of the key.
*/
public function get($key);
/**
* Used for returning values by key.
*
* @var string
* Key of the value.
*
* @var string
* Value of the key.
*/
public function set($key, $value);
/**
* Returns a renderable array.
*
* @return array
* A renderable array.
*/
public function getOutput();
}

View File

@ -0,0 +1,79 @@
<?php
/**
* @file
* Contains \Drupal\tour\TipsBag.
*/
namespace Drupal\tour;
use Drupal\Component\Plugin\PluginBag;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Utility\NestedArray;
/**
* A collection of tips.
*/
class TipsBag extends PluginBag {
/**
* The initial configuration for each tip in the bag.
*
* @var array
* An associative array containing the initial configuration for each tip
* in the bag, keyed by plugin instance ID.
*/
protected $configurations = array();
/**
* The manager used to instantiate the plugins.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $manager;
/**
* Constructs a TipsBag object.
*
* @param \Drupal\Component\Plugin\PluginManagerInterface $manager
* The manager to be used for instantiating plugins.
* @param array $configurations
* (optional) An associative array containing the initial configuration for
* each tour in the bag, keyed by plugin instance ID.
*/
public function __construct(PluginManagerInterface $manager, array $configurations = array()) {
$this->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)));
}
}
}

View File

@ -0,0 +1,26 @@
<?php
/**
* @file
* Contains \Drupal\tour\TourBundle.
*/
namespace Drupal\tour;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* Tour dependency injection container.
*/
class TourBundle extends Bundle {
/**
* Overrides \Symfony\Component\HttpKernel\Bundle\Bundle::build().
*/
public function build(ContainerBuilder $container) {
// Register the plugin manager for our plugin type with the dependency
// injection container.
$container->register('plugin.manager.tour', 'Drupal\tour\TourManager');
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* @file
* Contains \Drupal\tour\TourManager.
*/
namespace Drupal\tour;
use Drupal\Component\Plugin\PluginManagerBase;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
use Drupal\Core\Plugin\Discovery\CacheDecorator;
use Drupal\Component\Plugin\Discovery\ProcessDecorator;
/**
* Configurable text tour manager.
*/
class TourManager extends PluginManagerBase {
/**
* Overrides \Drupal\Component\Plugin\PluginManagerBase::__construct().
*/
public function __construct() {
$this->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;
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,48 @@
<?php
/**
* @file
* Contains \Drupal\tour_test\Plugin\tour\tour\TipPluginImage.
*/
namespace Drupal\tour_test\Plugin\tour\tip;
use Drupal\Core\Annotation\Plugin;
use Drupal\tour\TipPluginBase;
/**
* Displays an image as a tip.
*
* @Plugin(
* id = "image",
* module = "tour_test"
* )
*/
class TipPluginImage extends TipPluginBase {
/**
* The url which is used for the image in this Tip.
*
* @var string
* A url used for the image.
*/
protected $url;
/**
* The alt text which is used for the image in this Tip.
*
* @var string
* A alt text used for the image.
*/
protected $alt;
/**
* Overrides \Drupal\tour\Plugin\tour\tour\TipPluginInterface::getOutput().
*/
public function getOutput() {
return array(
'#markup' => '<h2 class="tour-tip-label" id="tour-tip-' . $this->get('ariaId') . '-label">' . check_plain($this->get('label')) . '</h2>
<p class="tour-tip-image" id="tour-tip-' . $this->get('ariaId') . '-contents">' . theme('image', array('uri' => $this->get('url'), 'alt' => $this->get('alt'))) . '</p>'
);
}
}

View File

@ -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

View File

@ -0,0 +1,87 @@
<?php
/**
* @file
* Provides tests for tour module
*/
/**
* Implements hook_menu().
*/
function tour_test_menu() {
$items['tour-test-1'] = array(
'page callback' => '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');
}
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* @file
* Describes API functions for tour module.
*/
/**
* Allow modules to alter tour items before render.
*
* @param array $tour_items
* Array of \Drupal\tour\TipPluginInterface items.
* @param string $path
* The path for which the tour is valid.
*/
function hook_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');
}
}
}
/**
* 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'));
}

View File

@ -0,0 +1,5 @@
name = Tour
description = Provides guided tours.
package = Core
version = VERSION
core = 8.x

View File

@ -0,0 +1,205 @@
<?php
/**
* @file
* Main functions of the module.
*/
use Drupal\Core\Cache\CacheBackendInterface;
/**
* Implements hook_permission().
*/
function tour_permission() {
return array(
'access tour' => 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;
$tour_items = array();
// 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);
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++;
}
$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();
}
/**
* Implements hook_tour_tour_update().
*/
function tour_tour_update($entity) {
drupal_container()->get('plugin.manager.tour')->clearCachedDefinitions();
}

View File

@ -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 <em>published</em>, you would add a filter for <em>Published</em> and select <em>Yes</em>.
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 <em>Content</em> to output each item completely, using your configured display settings. Other options include <em>Fields</em> which allows you to output only specific fields on each matching result. Additional formats can be added by installing additional module to <em>extend</em> 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

View File

@ -31,3 +31,4 @@ dependencies[] = file
dependencies[] = rdf
dependencies[] = views
dependencies[] = views_ui
dependencies[] = tour