MDL-40988 quiz: ability to break quizzes into sections
[moodle.git] / mod / quiz / yui / build / moodle-mod_quiz-util-slot / moodle-mod_quiz-util-slot.js
blob9394866ec3950abdb629d4137fcdd40c939b6a8a
1 YUI.add('moodle-mod_quiz-util-slot', function (Y, NAME) {
3 /**
4  * A collection of utility classes for use with slots.
5  *
6  * @module moodle-mod_quiz-util
7  * @submodule moodle-mod_quiz-util-slot
8  */
10 Y.namespace('Moodle.mod_quiz.util.slot');
12 /**
13  * A collection of utility classes for use with slots.
14  *
15  * @class Moodle.mod_quiz.util.slot
16  * @static
17  */
18 Y.Moodle.mod_quiz.util.slot = {
19     CSS: {
20         SLOT : 'slot',
21         QUESTIONTYPEDESCRIPTION : 'qtype_description',
22         CANNOT_DEPEND: 'question_dependency_cannot_depend'
23     },
24     CONSTANTS: {
25         SLOTIDPREFIX : 'slot-',
26         QUESTION : M.util.get_string('question', 'moodle')
27     },
28     SELECTORS: {
29         SLOT: 'li.slot',
30         INSTANCENAME: '.instancename',
31         NUMBER: 'span.slotnumber',
32         PAGECONTENT : 'div#page-content',
33         PAGEBREAK : 'span.page_split_join_wrapper',
34         ICON : 'img.smallicon',
35         QUESTIONTYPEDESCRIPTION : '.qtype_description',
36         SECTIONUL : 'ul.section',
37         DEPENDENCY_WRAPPER : '.question_dependency_wrapper',
38         DEPENDENCY_LINK : '.question_dependency_wrapper .cm-edit-action',
39         DEPENDENCY_ICON : '.question_dependency_wrapper img'
40     },
42     /**
43      * Retrieve the slot item from one of it's child Nodes.
44      *
45      * @method getSlotFromComponent
46      * @param slotcomponent {Node} The component Node.
47      * @return {Node|null} The Slot Node.
48      */
49     getSlotFromComponent: function(slotcomponent) {
50         return Y.one(slotcomponent).ancestor(this.SELECTORS.SLOT, true);
51     },
53     /**
54      * Determines the slot ID for the provided slot.
55      *
56      * @method getId
57      * @param slot {Node} The slot to find an ID for.
58      * @return {Number|false} The ID of the slot in question or false if no ID was found.
59      */
60     getId: function(slot) {
61         // We perform a simple substitution operation to get the ID.
62         var id = slot.get('id').replace(
63                 this.CONSTANTS.SLOTIDPREFIX, '');
65         // Attempt to validate the ID.
66         id = parseInt(id, 10);
67         if (typeof id === 'number' && isFinite(id)) {
68             return id;
69         }
70         return false;
71     },
73     /**
74      * Determines the slot name for the provided slot.
75      *
76      * @method getName
77      * @param slot {Node} The slot to find a name for.
78      * @return {string|false} The name of the slot in question or false if no ID was found.
79      */
80     getName: function(slot) {
81         var instance = slot.one(this.SELECTORS.INSTANCENAME);
82         if (instance) {
83             return instance.get('firstChild').get('data');
84         }
85         return null;
86     },
88     /**
89      * Determines the slot number for the provided slot.
90      *
91      * @method getNumber
92      * @param slot {Node} The slot to find the number for.
93      * @return {Number|false} The number of the slot in question or false if no number was found.
94      */
95     getNumber: function(slot) {
96         if (!slot) {
97             return false;
98         }
99         // We perform a simple substitution operation to get the number.
100         var number = slot.one(this.SELECTORS.NUMBER).get('text').replace(
101                         this.CONSTANTS.QUESTION, '');
102         // Attempt to validate the ID.
103         number = parseInt(number, 10);
104         if (typeof number === 'number' && isFinite(number)) {
105             return number;
106         }
107         return false;
108     },
110     /**
111      * Updates the slot number for the provided slot.
112      *
113      * @method setNumber
114      * @param slot {Node} The slot to update the number for.
115      * @return void
116      */
117     setNumber: function(slot, number) {
118         var numbernode = slot.one(this.SELECTORS.NUMBER);
119         numbernode.setHTML('<span class="accesshide">' + this.CONSTANTS.QUESTION + '</span> ' + number);
120     },
122     /**
123      * Returns a list of all slot elements on the page.
124      *
125      * @method getSlots
126      * @return {node[]} An array containing slot nodes.
127      */
128     getSlots: function() {
129         return Y.all(this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.SLOT);
130     },
132     /**
133      * Returns a list of all slot elements on the page that have numbers. Excudes description questions.
134      *
135      * @method getSlots
136      * @return {node[]} An array containing slot nodes.
137      */
138     getNumberedSlots: function() {
139         var selector = this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL;
140             selector += ' ' + this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')';
141         return Y.all(selector);
142     },
144     /**
145      * Returns the previous slot to the given slot.
146      *
147      * @method getPrevious
148      * @param slot Slot node
149      * @return {node|false} The previous slot node or false.
150      */
151     getPrevious: function(slot) {
152         return slot.previous(this.SELECTORS.SLOT);
153     },
155     /**
156      * Returns the previous numbered slot to the given slot.
157      *
158      * Ignores slots containing description question types.
159      *
160      * @method getPrevious
161      * @param slot Slot node
162      * @return {node|false} The previous slot node or false.
163      */
164     getPreviousNumbered: function(slot) {
165         var previous = slot.previous(this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')');
166         if (previous) {
167             return previous;
168         }
170         var section = slot.ancestor('li.section').previous('li.section');
171         while (section) {
172             var questions = section.all(this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')');
173             if (questions.size() > 0) {
174                 return questions.item(questions.size() - 1);
175             }
176             section = section.previous('li.section');
177         }
178         return false;
179     },
181     /**
182      * Reset the order of the numbers given to each slot.
183      *
184      * @method reorderSlots
185      * @return void
186      */
187     reorderSlots: function() {
188         // Get list of slot nodes.
189         var slots = this.getSlots();
190         // Loop through slots incrementing the number each time.
191         slots.each(function(slot) {
193             if (!Y.Moodle.mod_quiz.util.page.getPageFromSlot(slot)) {
194                 // Move the next page to the front.
195                 var nextpage = slot.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE);
196                 slot.swap(nextpage);
197             }
199             var previousSlot = this.getPreviousNumbered(slot),
200                 previousslotnumber = 0;
201             if (slot.hasClass(this.CSS.QUESTIONTYPEDESCRIPTION)) {
202                 return;
203             }
205             if (previousSlot) {
206                 previousslotnumber = this.getNumber(previousSlot);
207             }
209             // Set slot number.
210             this.setNumber(slot, previousslotnumber + 1);
211         }, this);
212     },
214     /**
215      * Add class only-has-one-slot to those sections that need it.
216      *
217      * @method updateOneSlotSections
218      * @return void
219      */
220     updateOneSlotSections: function() {
221         Y.all('.mod-quiz-edit-content ul.slots li.section').each(function(section) {
222             if (section.all(this.SELECTORS.SLOT).size() > 1) {
223                 section.removeClass('only-has-one-slot');
224             } else {
225                 section.addClass('only-has-one-slot');
226             }
227         }, this);
228     },
230     /**
231      * Remove a slot and related elements from the list of slots.
232      *
233      * @method remove
234      * @param slot Slot node
235      * @return void
236      */
237     remove: function(slot) {
238         var page = Y.Moodle.mod_quiz.util.page.getPageFromSlot(slot);
239         slot.remove();
240         // Is the page empty.
241         if (!Y.Moodle.mod_quiz.util.page.isEmpty(page)) {
242             return;
243         }
244         // If so remove it. Including add menu and page break.
245         Y.Moodle.mod_quiz.util.page.remove(page);
246     },
248     /**
249      * Returns a list of all page break elements on the page.
250      *
251      * @method getPageBreaks
252      * @return {node[]} An array containing page break nodes.
253      */
254     getPageBreaks: function() {
255         var selector = this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL;
256             selector += ' ' + this.SELECTORS.SLOT + this.SELECTORS.PAGEBREAK;
257         return Y.all(selector);
258     },
260     /**
261      * Retrieve the page break element item from the given slot.
262      *
263      * @method getPageBreak
264      * @param slot Slot node
265      * @return {Node|null} The Page Break Node.
266      */
267     getPageBreak: function(slot) {
268         return Y.one(slot).one(this.SELECTORS.PAGEBREAK);
269     },
271     /**
272      * Add a page break and related elements to the list of slots.
273      *
274      * @method addPageBreak
275      * @param beforenode Int | Node | HTMLElement | String to add
276      * @return pagebreak PageBreak node
277      */
278     addPageBreak: function(slot) {
279         var nodetext = M.mod_quiz.resource_toolbox.get('config').addpageiconhtml;
280         nodetext = nodetext.replace('%%SLOT%%', this.getNumber(slot));
281         var pagebreak = Y.Node.create(nodetext);
282         slot.one('div').insert(pagebreak, 'after');
283         return pagebreak;
284     },
286     /**
287      * Remove a pagebreak from the given slot.
288      *
289      * @method removePageBreak
290      * @param slot Slot node
291      * @return boolean
292      */
293     removePageBreak: function(slot) {
294         var pagebreak = this.getPageBreak(slot);
295         if (!pagebreak) {
296             return false;
297         }
298         pagebreak.remove();
299         return true;
300     },
302     /**
303      * Reorder each pagebreak by iterating through each related slot.
304      *
305      * @method reorderPageBreaks
306      * @return void
307      */
308     reorderPageBreaks: function() {
309         // Get list of slot nodes.
310         var slots = this.getSlots(), slotnumber = 0;
311         // Loop through slots incrementing the number each time.
312         slots.each(function(slot, key) {
313             slotnumber++;
314             var pagebreak = this.getPageBreak(slot);
315             var nextitem = slot.next('li.activity');
316             if (!nextitem) {
317                 // Last slot in a section. Should not have an icon.
318                 return;
319             }
321             // No pagebreak and not last slot. Add one.
322             if (!pagebreak) {
323                 pagebreak = this.addPageBreak(slot);
324             }
326             // Remove last page break if there is one.
327             if (pagebreak && key === slots.size() - 1) {
328                 this.removePageBreak(slot);
329             }
331             // Get page break anchor element.
332             var pagebreaklink = pagebreak.get('childNodes').item(0);
334             // Get the correct title.
335             var action = '', iconname = '';
336             if (Y.Moodle.mod_quiz.util.page.isPage(nextitem)) {
337                 action = 'removepagebreak';
338                 iconname = 'e/remove_page_break';
339             } else {
340                 action = 'addpagebreak';
341                 iconname = 'e/insert_page_break';
342             }
344             // Update the link and image titles
345             pagebreaklink.set('title', M.util.get_string(action, 'quiz'));
346             pagebreaklink.setData('action', action);
347             // Update the image title.
348             var icon = pagebreaklink.one(this.SELECTORS.ICON);
349             icon.set('title', M.util.get_string(action, 'quiz'));
350             icon.set('alt', M.util.get_string(action, 'quiz'));
352             // Update the image src.
353             icon.set('src', M.util.image_url(iconname));
355             // Get anchor url parameters as an associative array.
356             var params = Y.QueryString.parse(pagebreaklink.get('href'));
357             // Update slot number.
358             params.slot = slotnumber;
359             // Create the new url.
360             var newurl = '';
361             for (var index in params) {
362                 if (newurl.length) {
363                     newurl += "&";
364                 }
365                 newurl += index + "=" + params[index];
366             }
367             // Update the anchor.
368             pagebreaklink.set('href', newurl);
369         }, this);
370     },
372     /**
373      * Update the dependency icons.
374      *
375      * @method updateAllDependencyIcons
376      * @return void
377      */
378     updateAllDependencyIcons: function() {
379         // Get list of slot nodes.
380         var slots = this.getSlots(),
381             slotnumber = 0,
382             previousslot = null;
383         // Loop through slots incrementing the number each time.
384         slots.each (function(slot) {
385             slotnumber++;
387             if (slotnumber == 1 || previousslot.getData('canfinish') === '0') {
388                 slot.one(this.SELECTORS.DEPENDENCY_WRAPPER).addClass(this.CSS.CANNOT_DEPEND);
389             } else {
390                 slot.one(this.SELECTORS.DEPENDENCY_WRAPPER).removeClass(this.CSS.CANNOT_DEPEND);
391             }
392             this.updateDependencyIcon(slot, null);
394             previousslot = slot;
395         }, this);
396     },
398     /**
399      * Update the slot icon to indicate the new requiresprevious state.
400      *
401      * @method slot Slot node
402      * @method requiresprevious Whether this node now requires the previous one.
403      * @return void
404      */
405     updateDependencyIcon: function(slot, requiresprevious) {
406         var link = slot.one(this.SELECTORS.DEPENDENCY_LINK);
407         var icon = slot.one(this.SELECTORS.DEPENDENCY_ICON);
408         var previousSlot = this.getPrevious(slot);
409         var a = {thisq: this.getNumber(slot)};
410         if (previousSlot) {
411             a.previousq = this.getNumber(previousSlot);
412         }
414         if (requiresprevious === null) {
415             requiresprevious = link.getData('action') === 'removedependency';
416         }
418         if (requiresprevious) {
419             link.set('title', M.util.get_string('questiondependencyremove', 'quiz', a));
420             link.setData('action', 'removedependency');
421             icon.set('alt', M.util.get_string('questiondependsonprevious', 'quiz'));
422             icon.set('src', M.util.image_url('t/locked', 'moodle'));
423         } else {
424             link.set('title', M.util.get_string('questiondependencyadd', 'quiz', a));
425             link.setData('action', 'adddependency');
426             icon.set('alt', M.util.get_string('questiondependencyfree', 'quiz'));
427             icon.set('src', M.util.image_url('t/unlocked', 'moodle'));
428         }
429     }
433 }, '@VERSION@', {"requires": ["node", "moodle-mod_quiz-util-base"]});