welcome tour (#527)

refs https://github.com/TryGhost/Ghost/issues/5168
- adds a `tour` service that handles syncing and management of tour throbbers & content
- adds a `gh-tour-item` component that handles the display of a throbber and it's associated popover when clicked
- uses settings API endpoint to populate viewed tour items on app boot/signin
- adds `liquid-tether@2.0.3` dependency for attaching throbbers and popups
- adds initial tour contents
This commit is contained in:
Kevin Ansfield 2017-06-08 16:00:10 +01:00 committed by Aileen Nowak
parent 3d0730ab03
commit d8e1375af4
21 changed files with 922 additions and 9 deletions

View File

@ -10,9 +10,12 @@ import moment from 'moment';
import {guidFor} from 'ember-metal/utils';
import {htmlSafe} from 'ember-string';
import {invokeAction} from 'ember-invoke-action';
import {task, timeout} from 'ember-concurrency';
const {Handlebars} = Ember;
const PSM_ANIMATION_LENGTH = 400;
export default Component.extend(SettingsMenuMixin, {
selectedAuthor: null,
authors: [],
@ -31,6 +34,7 @@ export default Component.extend(SettingsMenuMixin, {
metaDescriptionScratch: alias('model.metaDescriptionScratch'),
_showSettingsMenu: false,
_showThrobbers: false,
didReceiveAttrs() {
this._super(...arguments);
@ -43,20 +47,37 @@ export default Component.extend(SettingsMenuMixin, {
this.set('selectedAuthor', author);
});
// reset the publish date on close if it has an error
// HACK: ugly method of working around the CSS animations so that we
// can add throbbers only when the animation has finished
// TODO: use liquid-fire to handle PSM slide-in and replace tabs manager
// with something more ember-like
if (this.get('showSettingsMenu') && !this._showSettingsMenu) {
this.get('showThrobbers').perform();
}
// fired when menu is closed
if (!this.get('showSettingsMenu') && this._showSettingsMenu) {
let post = this.get('model');
let errors = post.get('errors');
// reset the publish date if it has an error
if (errors.has('publishedAtBlogDate') || errors.has('publishedAtBlogTime')) {
post.set('publishedAtBlogTZ', post.get('publishedAtUTC'));
post.validate({attribute: 'publishedAtBlog'});
}
// remove throbbers
this.set('_showThrobbers', false);
}
this._showSettingsMenu = this.get('showSettingsMenu');
},
showThrobbers: task(function* () {
yield timeout(PSM_ANIMATION_LENGTH);
this.set('_showThrobbers', true);
}).restartable(),
seoTitle: computed('model.titleScratch', 'metaTitleScratch', function () {
let metaTitle = this.get('metaTitleScratch') || '';
@ -133,6 +154,16 @@ export default Component.extend(SettingsMenuMixin, {
},
actions: {
showSubview() {
this._super(...arguments);
this.set('_showThrobbers', false);
},
closeSubview() {
this._super(...arguments);
this.get('showThrobbers').perform();
},
discardEnter() {
return false;
},

View File

@ -0,0 +1,171 @@
import Component from 'ember-component';
import computed, {reads} from 'ember-computed';
import injectService from 'ember-service/inject';
import run from 'ember-runloop';
import {isBlank} from 'ember-utils';
let instancesCounter = 0;
let triangleClassPositions = {
'top-left': {
attachment: 'top left',
targetAttachment: 'bottom center',
offset: '0 28px'
},
'top': {
attachment: 'top center',
targetAttachment: 'bottom center'
},
'top-right': {
attachment: 'top right',
targetAttachment: 'bottom center',
offset: '0 -28px'
},
'right-top': {
attachment: 'top right',
targetAttachment: 'middle left',
offset: '28px 0'
},
'right': {
attachment: 'middle right',
targetAttachment: 'middle left'
},
'right-bottom': {
attachment: 'bottom right',
targetAttachment: 'middle left',
offset: '-28px 0'
},
'bottom-right': {
attachment: 'bottom right',
targetAttachment: 'top center',
offset: '0 -28px'
},
'bottom': {
attachment: 'bottom center',
targetAttachment: 'top center'
},
'bottom-left': {
attachment: 'bottom left',
targetAttachment: 'top center',
offset: '0 28px'
},
'left-bottom': {
attachment: 'bottom left',
targetAttachment: 'middle right',
offset: '-28px 0'
},
'left': {
attachment: 'middle left',
targetAttachment: 'middle right'
},
'left-top': {
attachment: 'top left',
targetAttachment: 'middle right',
offset: '28px 0'
}
};
const GhTourItemComponent = Component.extend({
mediaQueries: injectService(),
tour: injectService(),
tagName: '',
throbberId: null,
target: null,
throbberAttachment: 'middle center',
popoverTriangleClass: 'top',
isOpen: false,
_elementId: null,
_throbber: null,
_throbberElementId: null,
_throbberElementSelector: null,
_popoverAttachment: null,
_popoverTargetAttachment: null,
_popoverOffset: null,
isMobile: reads('mediaQueries.isMobile'),
isVisible: computed('isMobile', '_throbber', function () {
let isMobile = this.get('isMobile');
let hasThrobber = !isBlank(this.get('_throbber'));
return !isMobile && hasThrobber;
}),
init() {
this._super(...arguments);
// this is a tagless component so we need to generate our own elementId
this._elementId = instancesCounter++;
this._throbberElementId = `throbber-${this._elementId}`;
this._throbberElementSelector = `#throbber-${this._elementId}`;
this._handleOptOut = run.bind(this, this._remove);
this._handleViewed = run.bind(this, this._removeIfViewed);
this.get('tour').on('optOut', this._handleOptOut);
this.get('tour').on('viewed', this._handleViewed);
},
didReceiveAttrs() {
let throbberId = this.get('throbberId');
let throbber = this.get('tour').activeThrobber(throbberId);
let triangleClass = this.get('popoverTriangleClass');
let popoverPositions = triangleClassPositions[triangleClass];
this._throbber = throbber;
this._popoverAttachment = popoverPositions.attachment;
this._popoverTargetAttachment = popoverPositions.targetAttachment;
this._popoverOffset = popoverPositions.offset;
},
willDestroyElement() {
this._super(...arguments);
this.get('tour').off('optOut', this._handleOptOut);
this.get('tour').off('viewed', this._handleOptOut);
},
_removeIfViewed(id) {
if (id === this.get('throbberId')) {
this._remove();
}
},
_remove() {
this.set('_throbber', null);
},
_close() {
this.set('isOpen', false);
},
actions: {
open() {
this.set('isOpen', true);
},
close() {
this._close();
},
markAsViewed() {
let throbberId = this.get('throbberId');
this.get('tour').markThrobberAsViewed(throbberId);
this.set('_throbber', null);
this._close();
},
optOut() {
this.get('tour').optOut();
this.set('_throbber', null);
this._close();
}
}
});
GhTourItemComponent.reopenClass({
positionalParams: ['throbberId']
});
export default GhTourItemComponent;

View File

@ -35,6 +35,7 @@ export default Model.extend(ValidationEngine, {
count: attr('raw'),
facebook: attr('facebook-url-user'),
twitter: attr('twitter-url-user'),
tour: attr('json-string'),
ghostPaths: injectService(),
ajax: injectService(),

View File

@ -34,6 +34,7 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
notifications: injectService(),
settings: injectService(),
upgradeNotification: injectService(),
tour: injectService(),
beforeModel() {
return this.get('config').fetch();
@ -64,12 +65,14 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
});
let settingsPromise = this.get('settings').fetch();
let tourPromise = this.get('tour').fetchViewed();
// return the feature/settings load promises so that we block until
// they are loaded to enable synchronous access everywhere
return RSVP.all([
featurePromise,
settingsPromise
settingsPromise,
tourPromise
]);
}
},

View File

@ -1,10 +1,12 @@
import RSVP from 'rsvp';
import SessionService from 'ember-simple-auth/services/session';
import computed from 'ember-computed';
import injectService from 'ember-service/inject';
export default SessionService.extend({
store: injectService(),
feature: injectService(),
store: injectService(),
tour: injectService(),
user: computed(function () {
return this.get('store').queryRecord('user', {id: 'me'});
@ -12,7 +14,13 @@ export default SessionService.extend({
authenticate() {
return this._super(...arguments).then((authResult) => {
return this.get('feature').fetch().then(() => {
// TODO: remove duplication with application.afterModel
let preloadPromises = [
this.get('feature').fetch(),
this.get('tour').fetchViewed()
];
return RSVP.all(preloadPromises).then(() => {
return authResult;
});
});

143
app/services/tour.js Normal file
View File

@ -0,0 +1,143 @@
import Evented from 'ember-evented';
import RSVP from 'rsvp';
import Service from 'ember-service';
import computed from 'ember-computed';
import injectService from 'ember-service/inject';
export default Service.extend(Evented, {
ghostPaths: injectService(),
session: injectService(),
// this service is responsible for managing tour item visibility and syncing
// the viewed state with the server
//
// tour items need to be centrally defined here so that we have a single
// source of truth for marking all tour items as viewed
//
// a {{gh-tour-item "unique-id"}} component can be inserted in any template,
// this will use the tour service to grab content and determine visibility
// with the component in control of rendering the throbber/controlling the
// modal - this allows the component lifecycle hooks to perform automatic
// display/cleanup when the relevant UI is visible.
viewed: [],
// IDs should **NOT** be changed if they have been part of a release unless
// the re-display of the throbber should be forced. In that case it may be
// useful to add a version number eg. `my-feature` -> `my-feature-v2`.
// Format is as follows:
//
// {
// id: 'test',
// title: 'This is a test',
// message: 'This is a test of our <strong>feature tour</strong> feature'
// }
//
// TODO: it may be better to keep this configuration elsewhere to keep the
// service clean. Eventually we'll want apps to be able to register their
// own throbbers and tour content
throbbers: [],
init() {
let adminUrl = `${window.location.origin}${this.get('ghostPaths.url').admin()}`;
let adminDisplayUrl = adminUrl.replace(`${window.location.protocol}//`, '');
this.throbbers = [{
id: 'getting-started',
title: 'Getting started with Ghost',
message: `This is your admin area! You'll find all of your content, users and settings right here. You can come back any time by visiting <strong><a href="${adminUrl}" target="_blank">${adminDisplayUrl}</a></strong>`
}, {
id: 'using-the-editor',
title: 'Using the Ghost editor',
message: 'Ghost uses Markdown to allow you to write and format content quickly and easily. This toolbar also helps! Hit the <strong>?</strong> icon for more editor shortcuts.'
}, {
id: 'static-post',
title: 'Turning posts into pages',
message: 'Static pages are permanent pieces of content which live outside of your usual stream of posts, for example and \'about\' or \'contact\' page.'
}, {
id: 'featured-post',
title: 'Setting a featured post',
message: 'Depending on your theme, featured posts receive special styling to make them stand out as a particularly important or emphasised story.'
}, {
id: 'upload-a-theme',
title: 'Customising your publication',
message: 'Using custom themes, you can completely control the look and feel of your site to suit your branch. Here\'s a full guide to help: <strong><a href="https://themes.ghost.org" target="_blank">https://themes.ghost.org</a></strong>'
}];
},
_activeThrobbers: computed('viewed.[]', 'throbbers.[]', function () {
// return throbbers that haven't been viewed
let viewed = this.get('viewed');
let throbbers = this.get('throbbers');
return throbbers.reject((throbber) => {
return viewed.includes(throbber.id);
});
}),
// retrieve the IDs of the viewed throbbers from the server, always returns
// a promise
fetchViewed() {
return this.get('session.user').then((user) => {
let viewed = user.get('tour') || [];
this.set('viewed', viewed);
return viewed;
});
},
// save the list of viewed throbbers to the server overwriting the
// entire list
syncViewed() {
let viewed = this.get('viewed');
return this.get('session.user').then((user) => {
user.set('tour', viewed);
return user.save();
});
},
// returns throbber content for a given ID only if that throbber hasn't been
// viewed. Used by the {{gh-tour-item}} component to determine visibility
activeThrobber(id) {
let activeThrobbers = this.get('_activeThrobbers');
return activeThrobbers.findBy('id', id);
},
// when a throbber is opened the component will call this method to mark
// it as viewed and sync with the server. Always returns a promise
markThrobberAsViewed(id) {
let viewed = this.get('viewed');
if (!viewed.includes(id)) {
this.get('viewed').push(id);
this.trigger('viewed', id);
return this.syncViewed();
} else {
return RSVP.resolve(true);
}
},
// opting-out will use the list of IDs defined in this file making it the
// single-source-of-truth and allowing future client updates to control when
// new UI should be surfaced through tour items
optOut() {
let allThrobberIds = this.get('throbbers').mapBy('id');
this.set('viewed', allThrobberIds);
this.trigger('optOut');
return this.syncViewed();
},
// this is not used anywhere at the moment but it's useful to use via ember
// inspector as a reset mechanism
reEnable() {
this.set('viewed', []);
return this.syncViewed();
}
});

View File

@ -14,6 +14,7 @@
@import "patterns/tables.css";
@import "patterns/navlist.css";
@import "patterns/boxes.css";
@import "patterns/throbber.css";
/* Components: Groups of Patterns
@ -31,6 +32,8 @@
@import "components/power-select.css";
@import "components/power-calendar.css";
@import "components/publishmenu.css";
@import "components/popovers.css";
@import "components/tour.css";
/* Layouts: Groups of Components

View File

@ -14,6 +14,7 @@
@import "patterns/tables.css";
@import "patterns/navlist.css";
@import "patterns/boxes.css";
@import "patterns/throbber.css";
/* Components: Groups of Patterns
@ -31,6 +32,8 @@
@import "components/power-select.css";
@import "components/power-calendar.css";
@import "components/publishmenu.css";
@import "components/popovers.css";
@import "components/tour.css";
/* Layouts: Groups of Components

View File

@ -0,0 +1,226 @@
/* Popovers
/* ---------------------------------------------------------- */
.popover-item {
position: relative;
display: inline-block;
padding: 11px 26px 13px 16px;
min-width: 300px;
max-width: 400px;
background: var(--darkgrey);
border-radius: 6px;
color: var(--midgrey);
font-size: 1.2rem;
}
.popover-title {
color: #fff;
font-size: 1.4rem;
font-weight: 300;
}
.popover-desc {
margin-top: -4px;
}
.popover-body {
margin-top: 11px;
line-height: 1.7;
}
.popover-body b {
color: #fff;
}
.popover-body > *:last-child {
margin: 0;
}
/* Open / Close
/* ---------------------------------------------------------- */
.popover {
position: relative;
display: inline-block;
}
.popover .popover-item {
position: absolute;
z-index: 20;
}
.popover .popover-item.open {
display: block;
}
.popover .popover-item.closed {
display: none;
}
.popover-triangle-top {
transform-origin: top center;
}
.popover-triangle-top-left {
transform-origin: top left;
}
.popover-triangle-top-right {
transform-origin: top right;
}
.popover-triangle-bottom {
transform-origin: bottom center;
}
.popover-triangle-bottom-left {
transform-origin: bottom left;
}
.popover-triangle-bottom-right {
transform-origin: bottom right;
}
.popover-triangle-left {
transform-origin: left center;
}
.popover-triangle-left-top {
transform-origin: left top;
}
.popover-triangle-left-bottom {
transform-origin: left bottom;
}
.popover-triangle-right {
transform-origin: right center;
}
.popover-triangle-right-top {
transform-origin: right top;
}
.popover-triangle-right-bottom {
transform-origin: right bottom;
}
.popover-triangle-top:before,
.popover-triangle-top:after,
.popover-triangle-top-left:before,
.popover-triangle-top-left:after,
.popover-triangle-top-right:before,
.popover-triangle-top-right:after,
.popover-triangle-bottom:before,
.popover-triangle-bottom:after,
.popover-triangle-bottom-left:before,
.popover-triangle-bottom-left:after,
.popover-triangle-bottom-right:before,
.popover-triangle-bottom-right:after,
.popover-triangle-left:before,
.popover-triangle-left:after,
.popover-triangle-left-top:before,
.popover-triangle-left-top:after,
.popover-triangle-left-bottom:before,
.popover-triangle-left-bottom:after,
.popover-triangle-right:before,
.popover-triangle-right:after,
.popover-triangle-right-top:before,
.popover-triangle-right-top:after,
.popover-triangle-right-bottom:before,
.popover-triangle-right-bottom:after {
content: "";
position: absolute;
display: block;
}
.popover-triangle-top:before,
.popover-triangle-top:after,
.popover-triangle-bottom:before,
.popover-triangle-bottom:after {
left: 50%;
margin-left: -14px;
}
.popover-triangle-top-left:before,
.popover-triangle-top-left:after,
.popover-triangle-bottom-left:before,
.popover-triangle-bottom-left:after {
left: 14px;
}
.popover-triangle-top-right:before,
.popover-triangle-top-right:after,
.popover-triangle-bottom-right:before,
.popover-triangle-bottom-right:after {
right: 14px;
left: auto;
}
.popover-triangle-top:before,
.popover-triangle-top-left:before,
.popover-triangle-top-right:before {
top: calc(-14px * 0.8);
width: 0;
height: 0;
border-right: 14px solid transparent;
border-bottom: calc(14px * 0.8) solid #242628;
border-left: 14px solid transparent;
}
.popover-triangle-bottom:before,
.popover-triangle-bottom-left:before,
.popover-triangle-bottom-right:before {
bottom: calc(-14px * 0.8);
width: 0;
height: 0;
border-top: calc(14px * 0.8) solid #242628;
border-right: 14px solid transparent;
border-left: 14px solid transparent;
}
.popover-triangle-left:before,
.popover-triangle-left:after,
.popover-triangle-right:before,
.popover-triangle-right:after {
top: 50%;
margin-top: -14px;
}
.popover-triangle-left-top:before,
.popover-triangle-left-top:after,
.popover-triangle-right-top:before,
.popover-triangle-right-top:after {
top: 14px;
}
.popover-triangle-left-bottom:before,
.popover-triangle-left-bottom:after,
.popover-triangle-right-bottom:before,
.popover-triangle-right-bottom:after {
top: auto;
bottom: 14px;
}
.popover-triangle-left:before,
.popover-triangle-left-top:before,
.popover-triangle-left-bottom:before {
left: calc(-14px * 0.8);
width: 0;
height: 0;
border-top: 14px solid transparent;
border-right: calc(14px * 0.8) solid #242628;
border-bottom: 14px solid transparent;
}
.popover-triangle-right:before,
.popover-triangle-right-top:before,
.popover-triangle-right-bottom:before {
right: calc(-14px * 0.8);
width: 0;
height: 0;
border-top: 14px solid transparent;
border-bottom: 14px solid transparent;
border-left: calc(14px * 0.8) solid #242628;
}

View File

@ -0,0 +1,134 @@
/* ------------------------------------------------------------
Popovers
Styles for the popover component
* Popovers
* Open/Close
* Positioning
* Triangles classes
------------------------------------------------------------ */
.throbber-container {
z-index: 998;
}
.tour-background {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 999;
background: rgba(54,83,109,0.08);
}
.tour.liquid-wormhole-element {
z-index: 999;
}
.tour .popover-item {
padding: 25px;
max-width: none;
width: 480px;
border: #bfbfbf 1px solid;
background: #fff;
box-shadow: rgba(0,0,0,0.15) 0 1px 8px;
color: var(--midgrey);
}
.tour .popover-title {
margin-bottom: 0.4em;
color: var(--darkgrey);
font-size: 1.8rem;
font-weight: bold;
}
.tour .popover-desc {
margin: 0;
}
.tour .popover-body {
margin: 0;
font-size: 1.5rem;
line-height: 1.55em;
}
.tour .popover-foot {
display: flex;
justify-content: space-between;
margin-top: 25px;
}
.tour-optout {
align-self: flex-end;
}
/* 'dem Triangles */
.tour .popover-triangle-top:before,
.tour .popover-triangle-top-left:before,
.tour .popover-triangle-top-right:before {
border-right: 14px solid transparent;
border-bottom: calc(14px * 0.8) solid #bfbfbf;
border-left: 14px solid transparent;
}
.tour .popover-triangle-top:after,
.tour .popover-triangle-top-left:after,
.tour .popover-triangle-top-right:after {
top: -10px;
border-right: 14px solid transparent;
border-bottom: calc(14px * 0.8) solid #fff;
border-left: 14px solid transparent;
}
.tour .popover-triangle-bottom:before,
.tour .popover-triangle-bottom-left:before,
.tour .popover-triangle-bottom-right:before {
border-top: calc(14px * 0.8) solid #bfbfbf;
border-right: 14px solid transparent;
border-left: 14px solid transparent;
}
.tour .popover-triangle-bottom:after,
.tour .popover-triangle-bottom-left:after,
.tour .popover-triangle-bottom-right:after {
bottom: -10px;
border-top: calc(14px * 0.8) solid #fff;
border-right: 14px solid transparent;
border-left: 14px solid transparent;
}
.tour .popover-triangle-left:before,
.tour .popover-triangle-left-top:before,
.tour .popover-triangle-left-bottom:before {
border-top: 14px solid transparent;
border-right: calc(14px * 0.8) solid #bfbfbf;
border-bottom: 14px solid transparent;
}
.tour .popover-triangle-left:after,
.tour .popover-triangle-left-top:after,
.tour .popover-triangle-left-bottom:after {
left: -10px;
border-top: 14px solid transparent;
border-right: calc(14px * 0.8) solid #fff;
border-bottom: 14px solid transparent;
}
.tour .popover-triangle-right:before,
.tour .popover-triangle-right-top:before,
.tour .popover-triangle-right-bottom:before {
border-top: 14px solid transparent;
border-bottom: 14px solid transparent;
border-left: calc(14px * 0.8) solid #bfbfbf;
}
.tour .popover-triangle-right:after,
.tour .popover-triangle-right-top:after,
.tour .popover-triangle-right-bottom:after {
right: -10px;
border-top: 14px solid transparent;
border-bottom: 14px solid transparent;
border-left: calc(14px * 0.8) solid #fff;
}

View File

@ -0,0 +1,75 @@
/* ------------------------------------------------------------
Throbber
Pulsing little circle which indicates a tour is available
------------------------------------------------------------ */
/* click-area for triggering the popover */
.throbber-trigger {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
}
.throbber {
position: relative;
display: block;
width: 14px;
height: 14px;
border: rgba(255,255,255,0.3) 2px solid;
background: rgba(255,255,255,0.6);
border-radius: 100%;
box-shadow: rgba(0,0,0,0.25) 0 0 0 2px;
animation-name: throbber-fade;
animation-duration: 1.2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
animation-fill-mode: forwards;
}
.throbber:before {
content: "";
position: absolute;
top: 50%;
left: 50%;
display: block;
margin: -13px 0 0 -13px;
width: 26px;
height: 26px;
border: rgba(255,255,255,0.4) 2px solid;
background: rgba(255,255,255,0.2);
border-radius: 100%;
box-shadow: rgba(0,0,0,0.15) 0 0 0 2px;
animation-name: throbber-pulse, throbber-fade;
animation-duration: 1.2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
animation-fill-mode: forwards;
pointer-events: none;
}
@keyframes throbber-fade {
0%,
100% {
opacity: 0;
}
40%,
60% {
opacity: 0.8;
}
}
@keyframes throbber-pulse {
0% {
transform: scale3d(0.5, 0.5, 1);
}
50% {
transform: scale3d(1.4, 1.4, 1);
}
100% {
transform: scale3d(0.5, 0.5, 1);
}
}

View File

@ -60,3 +60,9 @@
<a class="gh-nav-foot-sitelink" href="{{config.blogUrl}}/" target="_blank">View site {{inline-svg "external"}}</a>
</footer>
<div class="gh-autonav-toggle" {{action "openAutoNav" on="mouseEnter"}}></div>
{{gh-tour-item "getting-started"
target=".gh-menu-toggle"
throbberAttachment="bottom middle"
popoverTriangleClass="left-top"
}}

View File

@ -163,3 +163,24 @@
</div>
</div>
{{/gh-tabs-manager}}
{{!--
_showThrobbers is on a timer so that throbbers don't get positioned until
the slide-in animation has finished and it gets toggled when the meta
pane is shown
--}}
{{#if _showThrobbers}}
{{gh-tour-item "static-post"
target="label[for='static-page'] p"
throbberAttachment="middle middle"
throbberOffset="0px 75px"
popoverTriangleClass="bottom-right"
}}
{{gh-tour-item "featured-post"
target="label[for='featured'] p"
throbberAttachment="middle middle"
throbberOffset="0px -20px"
popoverTriangleClass="bottom-right"
}}
{{/if}}

View File

@ -0,0 +1,43 @@
{{#if isVisible}}
{{!-- tether the throbber --}}
{{#liquid-tether
class="throbber-container"
target=target
attachment="middle center"
targetAttachment=throbberAttachment
targetOffset=throbberOffset
}}
<a class="throbber-trigger" href="#" {{action "open"}} id={{_throbberElementId}}>
<span class="throbber"></span>
</a>
{{/liquid-tether}}
{{#if isOpen}}
{{!-- wormhole the background click overlay --}}
{{#liquid-wormhole class="tour-container"}}
<div class="tour-background" {{action "close" on="click"}}></div>
{{/liquid-wormhole}}
{{!-- tether the popover --}}
{{#liquid-tether
class="tour"
target=_throbberElementSelector
attachment=_popoverAttachment
targetAttachment=_popoverTargetAttachment
offset=_popoverOffset
}}
<div class="popover-item popover-triangle-{{popoverTriangleClass}}">
<h3 class="popover-title">{{_throbber.title}}</h3>
<div class="popover-body">
{{{_throbber.message}}}
</div>
<footer class="popover-foot">
<small class="tour-optout">
Not your first time? <a href="#" {{action 'optOut'}}>Skip these tips</a>
</small>
<a class="tour-dismiss btn btn-blue" href="#" {{action 'markAsViewed'}}>Ok, got it</a>
</footer>
</div>
{{/liquid-tether}}
{{/if}}
{{/if}}

View File

@ -73,6 +73,13 @@
<div class="gh-markdown-editor-preview-content"></div>
</div>
{{/if}}
{{gh-tour-item "using-the-editor"
target=".gh-editor-footer"
throbberAttachment="top left"
throbberOffset="0 20%"
popoverTriangleClass="bottom"
}}
{{/gh-markdown-editor}}
{{!-- TODO: put tool/status bar in here so that scroll area can be fixed --}}

View File

@ -68,3 +68,9 @@
</section>
{{outlet}}
{{gh-tour-item "upload-a-theme"
target=".gh-themes-uploadbtn"
throbberAttachment="top middle"
popoverTriangleClass="bottom"
}}

View File

@ -10,4 +10,18 @@ export default function () {
this.hasClass('fade-transition'),
this.use('crossFade', {duration: 100})
);
this.transition(
this.hasClass('tour-container'),
this.toValue(true),
this.use('fade', {duration: 150}),
this.reverse('fade', {duration: 150})
);
this.transition(
this.hasClass('tour'),
this.toValue(true),
this.use('fade', {duration: 300}),
this.reverse('fade', {duration: 300})
);
}

View File

@ -98,6 +98,7 @@
"grunt-shell": "2.1.0",
"jquery-deparam": "0.5.3",
"liquid-fire": "0.27.3",
"liquid-tether": "2.0.4",
"liquid-wormhole": "2.0.5",
"loader.js": "4.4.0",
"markdown-it": "8.3.1",

View File

@ -14,7 +14,8 @@ describe('Unit: Model: invite', function() {
'service:ghost-paths',
'service:ajax',
'service:session',
'service:feature'
'service:feature',
'service:tour'
]
});

View File

@ -13,6 +13,7 @@ describe('Unit: Serializer: user', function() {
'service:notifications',
'service:session',
'transform:facebook-url-user',
'transform:json-string',
'transform:moment-utc',
'transform:raw',
'transform:twitter-url-user'

View File

@ -1448,7 +1448,7 @@ broccoli-merge-trees@2.0.0, broccoli-merge-trees@^2.0.0:
broccoli-plugin "^1.3.0"
merge-trees "^1.0.1"
broccoli-merge-trees@^1.0.0, broccoli-merge-trees@^1.1.0, broccoli-merge-trees@^1.1.1, broccoli-merge-trees@^1.1.2, broccoli-merge-trees@^1.1.4, broccoli-merge-trees@^1.1.5, broccoli-merge-trees@^1.2.1:
broccoli-merge-trees@^1.0.0, broccoli-merge-trees@^1.1.0, broccoli-merge-trees@^1.1.1, broccoli-merge-trees@^1.1.2, broccoli-merge-trees@^1.1.4, broccoli-merge-trees@^1.1.5, broccoli-merge-trees@^1.2.1, broccoli-merge-trees@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/broccoli-merge-trees/-/broccoli-merge-trees-1.2.4.tgz#a001519bb5067f06589d91afa2942445a2d0fdb5"
dependencies:
@ -5203,14 +5203,14 @@ js-tokens@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"
js-yaml@3.6.1, js-yaml@3.x, js-yaml@^3.2.5, js-yaml@^3.6.1, js-yaml@~3.6.0:
js-yaml@3.6.1, js-yaml@3.x, js-yaml@^3.2.5, js-yaml@^3.5.1, js-yaml@^3.6.1, js-yaml@~3.6.0:
version "3.6.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.6.1.tgz#6e5fe67d8b205ce4d22fad05b7781e8dadcc4b30"
dependencies:
argparse "^1.0.7"
esprima "^2.6.0"
js-yaml@^3.2.7, js-yaml@^3.5.1, js-yaml@~3.7.0:
js-yaml@^3.2.7, js-yaml@~3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80"
dependencies:
@ -5391,7 +5391,18 @@ liquid-fire@0.27.3:
match-media "^0.2.0"
velocity-animate ">= 0.11.8"
liquid-wormhole@2.0.5:
liquid-tether@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/liquid-tether/-/liquid-tether-2.0.4.tgz#6abcb53c3b3750d2b714a875f83de362c06e528a"
dependencies:
broccoli-funnel "^1.1.0"
broccoli-merge-trees "^1.2.4"
ember-cli-babel "^5.1.7"
ember-cli-htmlbars "^1.0.10"
liquid-wormhole "^2.0.2"
tether "^1.4.0"
liquid-wormhole@2.0.5, liquid-wormhole@^2.0.2:
version "2.0.5"
resolved "https://registry.yarnpkg.com/liquid-wormhole/-/liquid-wormhole-2.0.5.tgz#70d346892aff649945645848962209fffb331740"
dependencies:
@ -7879,6 +7890,10 @@ testem@^1.15.0:
tap-parser "^5.1.0"
xmldom "^0.1.19"
tether@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/tether/-/tether-1.4.0.tgz#0f9fa171f75bf58485d8149e94799d7ae74d1c1a"
text-encoding@0.6.4:
version "0.6.4"
resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"