Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
/**
2
 * User tour control library.
3
 *
4
 * @module     tool_usertours/usertours
5
 * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
6
 */
7
import BootstrapTour from './tour';
8
import Templates from 'core/templates';
9
import log from 'core/log';
10
import notification from 'core/notification';
11
import * as tourRepository from './repository';
12
import Pending from 'core/pending';
13
import {eventTypes} from './events';
14
 
15
let currentTour = null;
16
let tourId = null;
17
let restartTourAndKeepProgress = false;
18
let currentStepNo = null;
19
 
20
/**
21
 * Find the first matching tour.
22
 *
23
 * @param {object[]} tourDetails
24
 * @param {object[]} filters
25
 * @returns {null|object}
26
 */
27
const findMatchingTour = (tourDetails, filters) => {
28
    return tourDetails.find(tour => filters.some(filter => {
29
        if (filter && filter.filterMatches) {
30
            return filter.filterMatches(tour);
31
        }
32
 
33
        return true;
34
    }));
35
};
36
 
37
/**
38
 * Initialise the user tour for the current page.
39
 *
40
 * @method  init
41
 * @param   {Array}    tourDetails      The matching tours for this page.
42
 * @param   {Array}    filters          The names of all client side filters.
43
 */
44
export const init = async(tourDetails, filters) => {
45
    const requirements = [];
46
    filters.forEach((filter) => {
47
        requirements.push(import(filter));
48
    });
49
 
50
    const filterPlugins = await Promise.all(requirements);
51
 
52
    const matchingTour = findMatchingTour(tourDetails, filterPlugins);
53
    if (!matchingTour) {
54
        return;
55
    }
56
 
57
    // Only one tour per page is allowed.
58
    tourId = matchingTour.tourId;
59
 
60
    let startTour = matchingTour.startTour;
61
    if (typeof startTour === 'undefined') {
62
        startTour = true;
63
    }
64
 
65
    if (startTour) {
66
        // Fetch the tour configuration.
67
        fetchTour(tourId);
68
    }
69
 
70
    addResetLink();
71
 
72
    // Watch for the reset link.
73
    document.querySelector('body').addEventListener('click', e => {
74
        const resetLink = e.target.closest('#resetpagetour');
75
        if (resetLink) {
76
            e.preventDefault();
77
            resetTourState(tourId);
78
        }
79
    });
80
 
81
    // Watch for the resize event.
82
    window.addEventListener("resize", () => {
83
        // Only listen for the running tour.
84
        if (currentTour && currentTour.tourRunning) {
85
            clearTimeout(window.resizedFinished);
86
            window.resizedFinished = setTimeout(() => {
87
                // Wait until the resize event has finished.
88
                currentStepNo = currentTour.getCurrentStepNumber();
89
                restartTourAndKeepProgress = true;
90
                resetTourState(tourId);
91
            }, 250);
92
        }
93
    });
94
};
95
 
96
/**
97
 * Fetch the configuration specified tour, and start the tour when it has been fetched.
98
 *
99
 * @method  fetchTour
100
 * @param   {Number}    tourId      The ID of the tour to start.
101
 */
102
const fetchTour = async tourId => {
103
    const pendingPromise = new Pending(`admin_usertour_fetchTour:${tourId}`);
104
 
105
    try {
106
        // If we don't have any tour config (because it doesn't need showing for the current user), return early.
107
        const response = await tourRepository.fetchTour(tourId);
108
        if (response.hasOwnProperty('tourconfig')) {
109
            const {html} = await Templates.renderForPromise('tool_usertours/tourstep', response.tourconfig);
110
            startBootstrapTour(tourId, html, response.tourconfig);
111
        }
112
        pendingPromise.resolve();
113
    } catch (error) {
114
        pendingPromise.resolve();
115
        notification.exception(error);
116
    }
117
};
118
 
119
const getPreferredResetLocation = () => {
120
    let location = document.querySelector('.tool_usertours-resettourcontainer');
121
    if (location) {
122
        return location;
123
    }
124
 
125
    location = document.querySelector('.logininfo');
126
    if (location) {
127
        return location;
128
    }
129
 
130
    location = document.querySelector('footer');
131
    if (location) {
132
        return location;
133
    }
134
 
135
    return document.body;
136
};
137
 
138
/**
139
 * Add a reset link to the page.
140
 *
141
 * @method  addResetLink
142
 */
143
const addResetLink = () => {
144
    const pendingPromise = new Pending('admin_usertour_addResetLink');
145
 
146
    Templates.render('tool_usertours/resettour', {})
147
    .then(function(html, js) {
148
        // Append the link to the most suitable place on the page with fallback to legacy selectors and finally the body if
149
        // there is no better place.
150
        Templates.appendNodeContents(getPreferredResetLocation(), html, js);
151
 
152
        return;
153
    })
154
    .catch()
155
    .then(pendingPromise.resolve)
156
    .catch();
157
};
158
 
159
/**
160
 * Start the specified tour.
161
 *
162
 * @method  startBootstrapTour
163
 * @param   {Number}    tourId      The ID of the tour to start.
164
 * @param   {String}    template    The template to use.
165
 * @param   {Object}    tourConfig  The tour configuration.
166
 * @return  {Object}
167
 */
168
const startBootstrapTour = (tourId, template, tourConfig) => {
169
    if (currentTour && currentTour.tourRunning) {
170
        // End the current tour.
171
        currentTour.endTour();
172
        currentTour = null;
173
    }
174
 
175
    document.addEventListener(eventTypes.tourEnded, markTourComplete);
176
    document.addEventListener(eventTypes.stepRenderer, markStepShown);
177
 
178
    // Sort out the tour name.
179
    tourConfig.tourName = tourConfig.name;
180
    delete tourConfig.name;
181
 
182
    // Add the template to the configuration.
183
    // This enables translations of the buttons.
184
    tourConfig.template = template;
185
 
186
    tourConfig.steps = tourConfig.steps.map(function(step) {
187
        if (typeof step.element !== 'undefined') {
188
            step.target = step.element;
189
            delete step.element;
190
        }
191
 
192
        if (typeof step.reflex !== 'undefined') {
193
            step.moveOnClick = !!step.reflex;
194
            delete step.reflex;
195
        }
196
 
197
        if (typeof step.content !== 'undefined') {
198
            step.body = step.content;
199
            delete step.content;
200
        }
201
 
202
        return step;
203
    });
204
 
205
    currentTour = new BootstrapTour(tourConfig);
206
    let startAt = 0;
207
    if (restartTourAndKeepProgress && currentStepNo) {
208
        startAt = currentStepNo;
209
        restartTourAndKeepProgress = false;
210
        currentStepNo = null;
211
    }
212
    return currentTour.startTour(startAt);
213
};
214
 
215
/**
216
 * Mark the specified step as being shownd by the user.
217
 *
218
 * @method  markStepShown
219
 * @param   {Event} e
220
 */
221
const markStepShown = e => {
222
    const tour = e.detail.tour;
223
    const stepConfig = tour.getStepConfig(tour.getCurrentStepNumber());
224
    tourRepository.markStepShown(
225
        stepConfig.stepid,
226
        tourId,
227
        tour.getCurrentStepNumber()
228
    ).catch(log.error);
229
};
230
 
231
/**
232
 * Mark the specified tour as being completed by the user.
233
 *
234
 * @method  markTourComplete
235
 * @param   {Event} e
236
 * @listens tool_usertours/stepRendered
237
 */
238
const markTourComplete = e => {
239
    document.removeEventListener(eventTypes.tourEnded, markTourComplete);
240
    document.removeEventListener(eventTypes.stepRenderer, markStepShown);
241
 
242
    const tour = e.detail.tour;
243
    const stepConfig = tour.getStepConfig(tour.getCurrentStepNumber());
244
    tourRepository.markTourComplete(
245
        stepConfig.stepid,
246
        tourId,
247
        tour.getCurrentStepNumber()
248
    ).catch(log.error);
249
};
250
 
251
/**
252
 * Reset the state, and restart the the tour on the current page.
253
 *
254
 * @method  resetTourState
255
 * @param   {Number}    tourId      The ID of the tour to start.
256
 * @returns {Promise}
257
 */
258
export const resetTourState = tourId => tourRepository.resetTourState(tourId)
259
.then(response => {
260
    if (response.startTour) {
261
        fetchTour(response.startTour);
262
    }
263
    return;
264
}).catch(notification.exception);