first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,469 @@
YUI.add('moodle-mod_quiz-autosave', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Auto-save functionality for during quiz attempts.
*
* @module moodle-mod_quiz-autosave
*/
/**
* Auto-save functionality for during quiz attempts.
*
* @class M.mod_quiz.autosave
*/
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.autosave = {
/**
* The amount of time (in milliseconds) to wait between TinyMCE detections.
*
* @property TINYMCE_DETECTION_DELAY
* @type Number
* @default 500
* @private
*/
TINYMCE_DETECTION_DELAY: 500,
/**
* The number of times to try redetecting TinyMCE.
*
* @property TINYMCE_DETECTION_REPEATS
* @type Number
* @default 20
* @private
*/
TINYMCE_DETECTION_REPEATS: 20,
/**
* The delay (in milliseconds) between checking hidden input fields.
*
* @property WATCH_HIDDEN_DELAY
* @type Number
* @default 1000
* @private
*/
WATCH_HIDDEN_DELAY: 1000,
/**
* The number of failures to ignore before notifying the user.
*
* @property FAILURES_BEFORE_NOTIFY
* @type Number
* @default 1
* @private
*/
FAILURES_BEFORE_NOTIFY: 1,
/**
* The value to use when resetting the successful save counter.
*
* @property FIRST_SUCCESSFUL_SAVE
* @static
* @type Number
* @default -1
* @private
*/
FIRST_SUCCESSFUL_SAVE: -1,
/**
* The selectors used throughout this class.
*
* @property SELECTORS
* @private
* @type Object
* @static
*/
SELECTORS: {
QUIZ_FORM: '#responseform',
VALUE_CHANGE_ELEMENTS: 'input, textarea, [contenteditable="true"]',
CHANGE_ELEMENTS: 'input, select',
HIDDEN_INPUTS: 'input[type=hidden]',
CONNECTION_ERROR: '#connection-error',
CONNECTION_OK: '#connection-ok'
},
/**
* The script which handles the autosaves.
*
* @property AUTOSAVE_HANDLER
* @type String
* @default M.cfg.wwwroot + '/mod/quiz/autosave.ajax.php'
* @private
*/
AUTOSAVE_HANDLER: M.cfg.wwwroot + '/mod/quiz/autosave.ajax.php',
/**
* The delay (in milliseconds) between a change being made, and it being auto-saved.
*
* @property delay
* @type Number
* @default 120000
* @private
*/
delay: 120000,
/**
* A Node reference to the form we are monitoring.
*
* @property form
* @type Node
* @default null
*/
form: null,
/**
* Whether the form has been modified since the last save started.
*
* @property dirty
* @type boolean
* @default false
*/
dirty: false,
/**
* Timer object for the delay between form modifaction and the save starting.
*
* @property delay_timer
* @type Object
* @default null
* @private
*/
delay_timer: null,
/**
* Y.io transaction for the save ajax request.
*
* @property save_transaction
* @type object
* @default null
*/
save_transaction: null,
/**
* Failed saves count.
*
* @property savefailures
* @type Number
* @default 0
* @private
*/
savefailures: 0,
/**
* Properly bound key change handler.
*
* @property editor_change_handler
* @type EventHandle
* @default null
* @private
*/
editor_change_handler: null,
/**
* Record of the value of all the hidden fields, last time they were checked.
*
* @property hidden_field_values
* @type Object
* @default {}
*/
hidden_field_values: {},
/**
* Initialise the autosave code.
*
* @method init
* @param {Number} delay the delay, in seconds, between a change being detected, and
* a save happening.
*/
init: function(delay) {
this.form = Y.one(this.SELECTORS.QUIZ_FORM);
if (!this.form) {
Y.log('No response form found. Why did you try to set up autosave?', 'debug', 'moodle-mod_quiz-autosave');
return;
}
this.delay = delay * 1000;
this.form.delegate('valuechange', this.value_changed, this.SELECTORS.VALUE_CHANGE_ELEMENTS, this);
this.form.delegate('change', this.value_changed, this.SELECTORS.CHANGE_ELEMENTS, this);
this.form.on('submit', this.stop_autosaving, this);
require(['core_form/events'], function(FormEvent) {
window.addEventListener(FormEvent.eventTypes.uploadChanged, this.value_changed.bind(this));
}.bind(this));
this.init_tinymce(this.TINYMCE_DETECTION_REPEATS);
this.save_hidden_field_values();
this.watch_hidden_fields();
},
save_hidden_field_values: function() {
this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(hidden) {
var name = hidden.get('name');
if (!name) {
return;
}
this.hidden_field_values[name] = hidden.get('value');
}, this);
},
watch_hidden_fields: function() {
this.detect_hidden_field_changes();
Y.later(this.WATCH_HIDDEN_DELAY, this, this.watch_hidden_fields);
},
detect_hidden_field_changes: function() {
this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(hidden) {
var name = hidden.get('name'),
value = hidden.get('value');
if (!name) {
return;
}
if (!(name in this.hidden_field_values) || value !== this.hidden_field_values[name]) {
this.hidden_field_values[name] = value;
this.value_changed({target: hidden});
}
}, this);
},
/**
* Initialise watching of TinyMCE specifically.
*
* Because TinyMCE might load slowly, and after us, we need to keep
* trying, until we detect TinyMCE is there, or enough time has passed.
* This is based on the TINYMCE_DETECTION_DELAY and
* TINYMCE_DETECTION_REPEATS properties.
*
*
* @method init_tinymce
* @param {Number} repeatcount The number of attempts made so far.
*/
init_tinymce: function(repeatcount) {
if (typeof window.tinyMCE === 'undefined') {
if (repeatcount > 0) {
Y.later(this.TINYMCE_DETECTION_DELAY, this, this.init_tinymce, [repeatcount - 1]);
} else {
Y.log('Gave up looking for TinyMCE.', 'debug', 'moodle-mod_quiz-autosave');
}
return;
}
Y.log('Found TinyMCE.', 'debug', 'moodle-mod_quiz-autosave');
this.editor_change_handler = Y.bind(this.editor_changed, this);
if (window.tinyMCE.onAddEditor) {
window.tinyMCE.onAddEditor.add(Y.bind(this.init_tinymce_editor, this));
} else if (window.tinyMCE.on) {
var startSaveTimer = this.start_save_timer_if_necessary.bind(this);
window.tinyMCE.on('AddEditor', function(event) {
event.editor.on('Change Undo Redo keydown', startSaveTimer);
});
// One or more editors might already have been added, so we have to attach
// the event handlers to these as well.
window.tinyMCE.get().forEach(function(editor) {
editor.on('Change Undo Redo keydown', startSaveTimer);
});
}
},
/**
* Initialise watching of a specific TinyMCE editor.
*
* @method init_tinymce_editor
* @param {EventFacade} e
* @param {Object} editor The TinyMCE editor object
*/
init_tinymce_editor: function(e, editor) {
Y.log('Found TinyMCE editor ' + editor.id + '.', 'debug', 'moodle-mod_quiz-autosave');
editor.onChange.add(this.editor_change_handler);
editor.onRedo.add(this.editor_change_handler);
editor.onUndo.add(this.editor_change_handler);
editor.onKeyDown.add(this.editor_change_handler);
},
value_changed: function(e) {
var name = e.target.getAttribute('name');
if (name === 'thispage' || name === 'scrollpos' || (name && name.match(/_:flagged$/))) {
return; // Not interesting.
}
// Fallback to the ID when the name is not present (in the case of content editable).
name = name || '#' + e.target.getAttribute('id');
Y.log('Detected a value change in element ' + name + '.', 'debug', 'moodle-mod_quiz-autosave');
this.start_save_timer_if_necessary();
},
editor_changed: function(editor) {
Y.log('Detected a value change in editor ' + editor.id + '.', 'debug', 'moodle-mod_quiz-autosave');
this.start_save_timer_if_necessary();
},
start_save_timer_if_necessary: function() {
this.dirty = true;
if (this.delay_timer || this.save_transaction) {
// Already counting down or daving.
return;
}
this.start_save_timer();
},
start_save_timer: function() {
this.cancel_delay();
this.delay_timer = Y.later(this.delay, this, this.save_changes);
},
cancel_delay: function() {
if (this.delay_timer && this.delay_timer !== true) {
this.delay_timer.cancel();
}
this.delay_timer = null;
},
save_changes: function() {
this.cancel_delay();
this.dirty = false;
if (this.is_time_nearly_over()) {
Y.log('No more saving, time is nearly over.', 'debug', 'moodle-mod_quiz-autosave');
this.stop_autosaving();
return;
}
Y.log('Doing a save.', 'debug', 'moodle-mod_quiz-autosave');
if (typeof window.tinyMCE !== 'undefined') {
window.tinyMCE.triggerSave();
}
// YUI io.form incorrectly (in my opinion) sends the value of all submit
// buttons in the ajax request. We don't want any submit buttons.
// Therefore, temporarily change the type.
// (Yes, this is a nasty hack. One day this will be re-written as AMD, hopefully).
var allsubmitbuttons = this.form.all('input[type=submit], button[type=submit]');
allsubmitbuttons.setAttribute('type', 'button');
this.save_transaction = Y.io(this.AUTOSAVE_HANDLER, {
method: 'POST',
form: {id: this.form},
on: {
success: this.save_done,
failure: this.save_failed
},
context: this
});
// Change the button types back.
allsubmitbuttons.setAttribute('type', 'submit');
},
save_done: function(transactionid, response) {
var autosavedata = JSON.parse(response.responseText);
if (autosavedata.status !== 'OK') {
// Because IIS is useless, Moodle can't send proper HTTP response
// codes, so we have to detect failures manually.
this.save_failed(transactionid, response);
return;
}
if (typeof autosavedata.timeleft !== 'undefined') {
Y.log('Updating timer: ' + autosavedata.timeleft + ' seconds remain.', 'debug', 'moodle-mod_quiz-timer');
M.mod_quiz.timer.updateEndTime(autosavedata.timeleft);
}
this.update_saved_time_display();
Y.log('Save completed.', 'debug', 'moodle-mod_quiz-autosave');
this.save_transaction = null;
if (this.dirty) {
Y.log('Dirty after save.', 'debug', 'moodle-mod_quiz-autosave');
this.start_save_timer();
}
if (this.savefailures > 0) {
Y.one(this.SELECTORS.CONNECTION_ERROR).hide();
Y.one(this.SELECTORS.CONNECTION_OK).show();
this.savefailures = this.FIRST_SUCCESSFUL_SAVE;
} else if (this.savefailures === this.FIRST_SUCCESSFUL_SAVE) {
Y.one(this.SELECTORS.CONNECTION_OK).hide();
this.savefailures = 0;
}
},
save_failed: function() {
Y.log('Save failed.', 'debug', 'moodle-mod_quiz-autosave');
this.save_transaction = null;
// We want to retry soon.
this.start_save_timer();
this.savefailures = Math.max(1, this.savefailures + 1);
if (this.savefailures === this.FAILURES_BEFORE_NOTIFY) {
Y.one(this.SELECTORS.CONNECTION_ERROR).show();
Y.one(this.SELECTORS.CONNECTION_OK).hide();
}
},
/**
* Inform the user that their answers have been saved.
*
* @method update_saved_time_display
*/
update_saved_time_display: function() {
// We fetch the current language's preferred time format from the language pack.
require(['core/user_date', 'core/notification'], function(UserDate, Notification) {
UserDate.get([{
timestamp: Math.floor(Date.now() / 1000),
format: M.util.get_string('strftimedatetimeshortaccurate', 'langconfig'),
}]).then(function(dateStrs) {
var infoDiv = Y.one('#mod_quiz_navblock .othernav .autosave_info');
infoDiv.set('text', M.util.get_string('lastautosave', 'quiz', dateStrs[0]));
infoDiv.show();
return;
}).catch(Notification.exception);
});
},
is_time_nearly_over: function() {
return M.mod_quiz.timer && M.mod_quiz.timer.endtime &&
(new Date().getTime() + 2 * this.delay) > M.mod_quiz.timer.endtime;
},
stop_autosaving: function() {
this.cancel_delay();
this.delay_timer = true;
if (this.save_transaction) {
this.save_transaction.abort();
}
}
};
}, '@VERSION@', {
"requires": [
"base",
"node",
"event",
"event-valuechange",
"node-event-delegate",
"io-form",
"datatype-date-format"
]
});
File diff suppressed because one or more lines are too long
@@ -0,0 +1,457 @@
YUI.add('moodle-mod_quiz-autosave', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Auto-save functionality for during quiz attempts.
*
* @module moodle-mod_quiz-autosave
*/
/**
* Auto-save functionality for during quiz attempts.
*
* @class M.mod_quiz.autosave
*/
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.autosave = {
/**
* The amount of time (in milliseconds) to wait between TinyMCE detections.
*
* @property TINYMCE_DETECTION_DELAY
* @type Number
* @default 500
* @private
*/
TINYMCE_DETECTION_DELAY: 500,
/**
* The number of times to try redetecting TinyMCE.
*
* @property TINYMCE_DETECTION_REPEATS
* @type Number
* @default 20
* @private
*/
TINYMCE_DETECTION_REPEATS: 20,
/**
* The delay (in milliseconds) between checking hidden input fields.
*
* @property WATCH_HIDDEN_DELAY
* @type Number
* @default 1000
* @private
*/
WATCH_HIDDEN_DELAY: 1000,
/**
* The number of failures to ignore before notifying the user.
*
* @property FAILURES_BEFORE_NOTIFY
* @type Number
* @default 1
* @private
*/
FAILURES_BEFORE_NOTIFY: 1,
/**
* The value to use when resetting the successful save counter.
*
* @property FIRST_SUCCESSFUL_SAVE
* @static
* @type Number
* @default -1
* @private
*/
FIRST_SUCCESSFUL_SAVE: -1,
/**
* The selectors used throughout this class.
*
* @property SELECTORS
* @private
* @type Object
* @static
*/
SELECTORS: {
QUIZ_FORM: '#responseform',
VALUE_CHANGE_ELEMENTS: 'input, textarea, [contenteditable="true"]',
CHANGE_ELEMENTS: 'input, select',
HIDDEN_INPUTS: 'input[type=hidden]',
CONNECTION_ERROR: '#connection-error',
CONNECTION_OK: '#connection-ok'
},
/**
* The script which handles the autosaves.
*
* @property AUTOSAVE_HANDLER
* @type String
* @default M.cfg.wwwroot + '/mod/quiz/autosave.ajax.php'
* @private
*/
AUTOSAVE_HANDLER: M.cfg.wwwroot + '/mod/quiz/autosave.ajax.php',
/**
* The delay (in milliseconds) between a change being made, and it being auto-saved.
*
* @property delay
* @type Number
* @default 120000
* @private
*/
delay: 120000,
/**
* A Node reference to the form we are monitoring.
*
* @property form
* @type Node
* @default null
*/
form: null,
/**
* Whether the form has been modified since the last save started.
*
* @property dirty
* @type boolean
* @default false
*/
dirty: false,
/**
* Timer object for the delay between form modifaction and the save starting.
*
* @property delay_timer
* @type Object
* @default null
* @private
*/
delay_timer: null,
/**
* Y.io transaction for the save ajax request.
*
* @property save_transaction
* @type object
* @default null
*/
save_transaction: null,
/**
* Failed saves count.
*
* @property savefailures
* @type Number
* @default 0
* @private
*/
savefailures: 0,
/**
* Properly bound key change handler.
*
* @property editor_change_handler
* @type EventHandle
* @default null
* @private
*/
editor_change_handler: null,
/**
* Record of the value of all the hidden fields, last time they were checked.
*
* @property hidden_field_values
* @type Object
* @default {}
*/
hidden_field_values: {},
/**
* Initialise the autosave code.
*
* @method init
* @param {Number} delay the delay, in seconds, between a change being detected, and
* a save happening.
*/
init: function(delay) {
this.form = Y.one(this.SELECTORS.QUIZ_FORM);
if (!this.form) {
return;
}
this.delay = delay * 1000;
this.form.delegate('valuechange', this.value_changed, this.SELECTORS.VALUE_CHANGE_ELEMENTS, this);
this.form.delegate('change', this.value_changed, this.SELECTORS.CHANGE_ELEMENTS, this);
this.form.on('submit', this.stop_autosaving, this);
require(['core_form/events'], function(FormEvent) {
window.addEventListener(FormEvent.eventTypes.uploadChanged, this.value_changed.bind(this));
}.bind(this));
this.init_tinymce(this.TINYMCE_DETECTION_REPEATS);
this.save_hidden_field_values();
this.watch_hidden_fields();
},
save_hidden_field_values: function() {
this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(hidden) {
var name = hidden.get('name');
if (!name) {
return;
}
this.hidden_field_values[name] = hidden.get('value');
}, this);
},
watch_hidden_fields: function() {
this.detect_hidden_field_changes();
Y.later(this.WATCH_HIDDEN_DELAY, this, this.watch_hidden_fields);
},
detect_hidden_field_changes: function() {
this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(hidden) {
var name = hidden.get('name'),
value = hidden.get('value');
if (!name) {
return;
}
if (!(name in this.hidden_field_values) || value !== this.hidden_field_values[name]) {
this.hidden_field_values[name] = value;
this.value_changed({target: hidden});
}
}, this);
},
/**
* Initialise watching of TinyMCE specifically.
*
* Because TinyMCE might load slowly, and after us, we need to keep
* trying, until we detect TinyMCE is there, or enough time has passed.
* This is based on the TINYMCE_DETECTION_DELAY and
* TINYMCE_DETECTION_REPEATS properties.
*
*
* @method init_tinymce
* @param {Number} repeatcount The number of attempts made so far.
*/
init_tinymce: function(repeatcount) {
if (typeof window.tinyMCE === 'undefined') {
if (repeatcount > 0) {
Y.later(this.TINYMCE_DETECTION_DELAY, this, this.init_tinymce, [repeatcount - 1]);
} else {
}
return;
}
this.editor_change_handler = Y.bind(this.editor_changed, this);
if (window.tinyMCE.onAddEditor) {
window.tinyMCE.onAddEditor.add(Y.bind(this.init_tinymce_editor, this));
} else if (window.tinyMCE.on) {
var startSaveTimer = this.start_save_timer_if_necessary.bind(this);
window.tinyMCE.on('AddEditor', function(event) {
event.editor.on('Change Undo Redo keydown', startSaveTimer);
});
// One or more editors might already have been added, so we have to attach
// the event handlers to these as well.
window.tinyMCE.get().forEach(function(editor) {
editor.on('Change Undo Redo keydown', startSaveTimer);
});
}
},
/**
* Initialise watching of a specific TinyMCE editor.
*
* @method init_tinymce_editor
* @param {EventFacade} e
* @param {Object} editor The TinyMCE editor object
*/
init_tinymce_editor: function(e, editor) {
editor.onChange.add(this.editor_change_handler);
editor.onRedo.add(this.editor_change_handler);
editor.onUndo.add(this.editor_change_handler);
editor.onKeyDown.add(this.editor_change_handler);
},
value_changed: function(e) {
var name = e.target.getAttribute('name');
if (name === 'thispage' || name === 'scrollpos' || (name && name.match(/_:flagged$/))) {
return; // Not interesting.
}
// Fallback to the ID when the name is not present (in the case of content editable).
name = name || '#' + e.target.getAttribute('id');
this.start_save_timer_if_necessary();
},
editor_changed: function(editor) {
this.start_save_timer_if_necessary();
},
start_save_timer_if_necessary: function() {
this.dirty = true;
if (this.delay_timer || this.save_transaction) {
// Already counting down or daving.
return;
}
this.start_save_timer();
},
start_save_timer: function() {
this.cancel_delay();
this.delay_timer = Y.later(this.delay, this, this.save_changes);
},
cancel_delay: function() {
if (this.delay_timer && this.delay_timer !== true) {
this.delay_timer.cancel();
}
this.delay_timer = null;
},
save_changes: function() {
this.cancel_delay();
this.dirty = false;
if (this.is_time_nearly_over()) {
this.stop_autosaving();
return;
}
if (typeof window.tinyMCE !== 'undefined') {
window.tinyMCE.triggerSave();
}
// YUI io.form incorrectly (in my opinion) sends the value of all submit
// buttons in the ajax request. We don't want any submit buttons.
// Therefore, temporarily change the type.
// (Yes, this is a nasty hack. One day this will be re-written as AMD, hopefully).
var allsubmitbuttons = this.form.all('input[type=submit], button[type=submit]');
allsubmitbuttons.setAttribute('type', 'button');
this.save_transaction = Y.io(this.AUTOSAVE_HANDLER, {
method: 'POST',
form: {id: this.form},
on: {
success: this.save_done,
failure: this.save_failed
},
context: this
});
// Change the button types back.
allsubmitbuttons.setAttribute('type', 'submit');
},
save_done: function(transactionid, response) {
var autosavedata = JSON.parse(response.responseText);
if (autosavedata.status !== 'OK') {
// Because IIS is useless, Moodle can't send proper HTTP response
// codes, so we have to detect failures manually.
this.save_failed(transactionid, response);
return;
}
if (typeof autosavedata.timeleft !== 'undefined') {
M.mod_quiz.timer.updateEndTime(autosavedata.timeleft);
}
this.update_saved_time_display();
this.save_transaction = null;
if (this.dirty) {
this.start_save_timer();
}
if (this.savefailures > 0) {
Y.one(this.SELECTORS.CONNECTION_ERROR).hide();
Y.one(this.SELECTORS.CONNECTION_OK).show();
this.savefailures = this.FIRST_SUCCESSFUL_SAVE;
} else if (this.savefailures === this.FIRST_SUCCESSFUL_SAVE) {
Y.one(this.SELECTORS.CONNECTION_OK).hide();
this.savefailures = 0;
}
},
save_failed: function() {
this.save_transaction = null;
// We want to retry soon.
this.start_save_timer();
this.savefailures = Math.max(1, this.savefailures + 1);
if (this.savefailures === this.FAILURES_BEFORE_NOTIFY) {
Y.one(this.SELECTORS.CONNECTION_ERROR).show();
Y.one(this.SELECTORS.CONNECTION_OK).hide();
}
},
/**
* Inform the user that their answers have been saved.
*
* @method update_saved_time_display
*/
update_saved_time_display: function() {
// We fetch the current language's preferred time format from the language pack.
require(['core/user_date', 'core/notification'], function(UserDate, Notification) {
UserDate.get([{
timestamp: Math.floor(Date.now() / 1000),
format: M.util.get_string('strftimedatetimeshortaccurate', 'langconfig'),
}]).then(function(dateStrs) {
var infoDiv = Y.one('#mod_quiz_navblock .othernav .autosave_info');
infoDiv.set('text', M.util.get_string('lastautosave', 'quiz', dateStrs[0]));
infoDiv.show();
return;
}).catch(Notification.exception);
});
},
is_time_nearly_over: function() {
return M.mod_quiz.timer && M.mod_quiz.timer.endtime &&
(new Date().getTime() + 2 * this.delay) > M.mod_quiz.timer.endtime;
},
stop_autosaving: function() {
this.cancel_delay();
this.delay_timer = true;
if (this.save_transaction) {
this.save_transaction.abort();
}
}
};
}, '@VERSION@', {
"requires": [
"base",
"node",
"event",
"event-valuechange",
"node-event-delegate",
"io-form",
"datatype-date-format"
]
});
@@ -0,0 +1,553 @@
YUI.add('moodle-mod_quiz-dragdrop', function (Y, NAME) {
/* eslint-disable no-unused-vars */
/**
* Drag and Drop for Quiz sections and slots.
*
* @module moodle-mod-quiz-dragdrop
*/
var CSS = {
ACTIONAREA: '.actions',
ACTIVITY: 'activity',
ACTIVITYINSTANCE: 'activityinstance',
CONTENT: 'content',
COURSECONTENT: 'mod-quiz-edit-content',
EDITINGMOVE: 'editing_move',
ICONCLASS: 'iconsmall',
JUMPMENU: 'jumpmenu',
LEFT: 'left',
LIGHTBOX: 'lightbox',
MOVEDOWN: 'movedown',
MOVEUP: 'moveup',
PAGE: 'page',
PAGECONTENT: 'page-content',
RIGHT: 'right',
SECTION: 'slots',
SECTIONADDMENUS: 'section_add_menus',
SECTIONHANDLE: 'section-handle',
SLOTS: 'slots',
SUMMARY: 'summary',
SECTIONDRAGGABLE: 'sectiondraggable'
},
// The CSS selectors we use.
SELECTOR = {
PAGE: 'li.page',
SLOT: 'li.slot'
};
/**
* Section drag and drop.
*
* @class M.mod_quiz.dragdrop.section
* @constructor
* @extends M.core.dragdrop
*/
var DRAGSECTION = function() {
DRAGSECTION.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGSECTION, M.core.dragdrop, {
sectionlistselector: null,
initializer: function() {
// Set group for parent class
this.groups = [CSS.SECTIONDRAGGABLE];
this.samenodeclass = 'section';
this.parentnodeclass = 'slots';
// Check if we are in single section mode
if (Y.Node.one('.' + CSS.JUMPMENU)) {
return false;
}
// Initialise sections dragging
this.sectionlistselector = 'li.section';
if (this.sectionlistselector) {
this.sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + this.sectionlistselector;
this.setup_for_section(this.sectionlistselector);
// Make each li element in the lists of sections draggable
var del = new Y.DD.Delegate({
container: '.' + CSS.COURSECONTENT,
nodes: '.' + CSS.SECTIONDRAGGABLE,
target: true,
handles: ['.' + CSS.LEFT],
dragConfig: {groups: this.groups}
});
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .mod-quiz-edit-content
constrain: '#' + CSS.PAGECONTENT,
stickY: true
});
del.dd.plug(Y.Plugin.DDWinScroll);
}
},
/**
* Apply dragdrop features to the specified selector or node that refers to section(s)
*
* @method setup_for_section
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_section: function(baseselector) {
Y.Node.all(baseselector).each(function(sectionnode) {
// Determine the section ID
var sectionid = Y.Moodle.core_course.util.section.getId(sectionnode);
// We skip the top section as it is not draggable
if (sectionid > 0) {
// Remove move icons
var movedown = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEDOWN);
var moveup = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEUP);
// Add dragger icon
var title = M.util.get_string('movesection', 'moodle', sectionid);
var cssleft = sectionnode.one('.' + CSS.LEFT);
if ((movedown || moveup) && cssleft) {
cssleft.setStyle('cursor', 'move');
cssleft.appendChild(this.get_drag_handle(title, CSS.SECTIONHANDLE, 'icon', true));
if (moveup) {
moveup.remove();
}
if (movedown) {
movedown.remove();
}
// This section can be moved - add the class to indicate this to Y.DD.
sectionnode.addClass(CSS.SECTIONDRAGGABLE);
}
}
}, this);
},
/*
* Drag-dropping related functions
*/
drag_start: function(e) {
// Get our drag object
var drag = e.target;
// Creat a dummy structure of the outer elemnents for clean styles application
var containernode = Y.Node.create('<ul class="slots"></ul>');
var sectionnode = Y.Node.create('<ul class="section"></ul>');
sectionnode.setStyle('margin', 0);
sectionnode.setContent(drag.get('node').get('innerHTML'));
containernode.appendChild(sectionnode);
drag.get('dragNode').setContent(containernode);
drag.get('dragNode').addClass(CSS.COURSECONTENT);
},
drag_dropmiss: function(e) {
// Missed the target, but we assume the user intended to drop it
// on the last last ghost node location, e.drag and e.drop should be
// prepared by global_drag_dropmiss parent so simulate drop_hit(e).
this.drop_hit(e);
},
get_section_index: function(node) {
var sectionlistselector = '.' + CSS.COURSECONTENT + ' li.section',
sectionList = Y.all(sectionlistselector),
nodeIndex = sectionList.indexOf(node),
zeroIndex = sectionList.indexOf(Y.one('#section-0'));
return (nodeIndex - zeroIndex);
},
drop_hit: function(e) {
var drag = e.drag;
// Get references to our nodes and their IDs.
var dragnode = drag.get('node'),
dragnodeid = Y.Moodle.core_course.util.section.getId(dragnode),
loopstart = dragnodeid,
dropnodeindex = this.get_section_index(dragnode),
loopend = dropnodeindex;
if (dragnodeid === dropnodeindex) {
Y.log("Skipping move - same location moving " + dragnodeid + " to " + dropnodeindex,
'debug', 'moodle-mod_quiz-dragdrop');
return;
}
Y.log("Moving from position " + dragnodeid + " to position " + dropnodeindex, 'debug', 'moodle-mod_quiz-dragdrop');
if (loopstart > loopend) {
// If we're going up, we need to swap the loop order
// because loops can't go backwards.
loopstart = dropnodeindex;
loopend = dragnodeid;
}
// Get the list of nodes.
drag.get('dragNode').removeClass(CSS.COURSECONTENT);
var sectionlist = Y.Node.all(this.sectionlistselector);
// Add a lightbox if it's not there.
var lightbox = M.util.add_lightbox(Y, dragnode);
// Handle any variables which we must pass via AJAX.
var params = {},
pageparams = this.get('config').pageparams,
varname;
for (varname in pageparams) {
if (!pageparams.hasOwnProperty(varname)) {
continue;
}
params[varname] = pageparams[varname];
}
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseid = this.get('courseid');
params.quizid = this.get('quizid');
params['class'] = 'section';
params.field = 'move';
params.id = dragnodeid;
params.value = dropnodeindex;
// Perform the AJAX request.
var uri = M.cfg.wwwroot + this.get('ajaxurl');
Y.io(uri, {
method: 'POST',
data: params,
on: {
start: function() {
lightbox.show();
},
success: function(tid, response) {
// Update section titles, we can't simply swap them as
// they might have custom title
try {
var responsetext = Y.JSON.parse(response.responseText);
if (responsetext.error) {
new M.core.ajaxException(responsetext);
}
M.mod_quiz.edit.process_sections(Y, sectionlist, responsetext, loopstart, loopend);
} catch (e) {
// Ignore.
}
// Update all of the section IDs - first unset them, then set them
// to avoid duplicates in the DOM.
var index;
// Classic bubble sort algorithm is applied to the section
// nodes between original drag node location and the new one.
var swapped = false;
do {
swapped = false;
for (index = loopstart; index <= loopend; index++) {
if (Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) >
Y.Moodle.core_course.util.section.getId(sectionlist.item(index))) {
Y.log("Swapping " + Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) +
" with " + Y.Moodle.core_course.util.section.getId(sectionlist.item(index)),
"debug", "moodle-mod_quiz-dragdrop");
// Swap section id.
var sectionid = sectionlist.item(index - 1).get('id');
sectionlist.item(index - 1).set('id', sectionlist.item(index).get('id'));
sectionlist.item(index).set('id', sectionid);
// See what format needs to swap.
M.mod_quiz.edit.swap_sections(Y, index - 1, index);
// Update flag.
swapped = true;
}
}
loopend = loopend - 1;
} while (swapped);
window.setTimeout(function() {
lightbox.hide();
}, 250);
},
failure: function(tid, response) {
this.ajax_failure(response);
lightbox.hide();
}
},
context: this
});
}
}, {
NAME: 'mod_quiz-dragdrop-section',
ATTRS: {
courseid: {
value: null
},
quizid: {
value: null
},
ajaxurl: {
value: 0
},
config: {
value: 0
}
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.init_section_dragdrop = function(params) {
new DRAGSECTION(params);
};
/**
* Resource drag and drop.
*
* @class M.course.dragdrop.resource
* @constructor
* @extends M.core.dragdrop
*/
var DRAGRESOURCE = function() {
DRAGRESOURCE.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGRESOURCE, M.core.dragdrop, {
initializer: function() {
// Set group for parent class
this.groups = ['resource'];
this.samenodeclass = CSS.ACTIVITY;
this.parentnodeclass = CSS.SECTION;
this.samenodelabel = {
identifier: 'dragtoafter',
component: 'quiz'
};
this.parentnodelabel = {
identifier: 'dragtostart',
component: 'quiz'
};
// Go through all sections
this.setup_for_section();
// Initialise drag & drop for all resources/activities
var nodeselector = 'li.' + CSS.ACTIVITY;
var del = new Y.DD.Delegate({
container: '.' + CSS.COURSECONTENT,
nodes: nodeselector,
target: true,
handles: ['.' + CSS.EDITINGMOVE],
dragConfig: {groups: this.groups}
});
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false,
cloneNode: true
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .mod-quiz-edit-content
constrain: '#' + CSS.SLOTS
});
del.dd.plug(Y.Plugin.DDWinScroll);
M.mod_quiz.quizbase.register_module(this);
M.mod_quiz.dragres = this;
},
/**
* Apply dragdrop features to the specified selector or node that refers to section(s)
*
* @method setup_for_section
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_section: function() {
Y.Node.all('.mod-quiz-edit-content ul.slots ul.section').each(function(resources) {
resources.setAttribute('data-draggroups', this.groups.join(' '));
// Define empty ul as droptarget, so that item could be moved to empty list
new Y.DD.Drop({
node: resources,
groups: this.groups,
padding: '20 0 20 0'
});
// Initialise each resource/activity in this section
this.setup_for_resource('li.activity');
}, this);
},
/**
* Apply dragdrop features to the specified selector or node that refers to resource(s)
*
* @method setup_for_resource
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_resource: function(baseselector) {
Y.Node.all(baseselector).each(function(resourcesnode) {
// Replace move icons
var move = resourcesnode.one('a.' + CSS.EDITINGMOVE);
if (move) {
var resourcedraghandle = this.get_drag_handle(M.util.get_string('move', 'moodle'),
CSS.EDITINGMOVE, CSS.ICONCLASS, true);
move.replace(resourcedraghandle);
}
}, this);
},
drag_start: function(e) {
// Get our drag object
var drag = e.target;
drag.get('dragNode').setContent(drag.get('node').get('innerHTML'));
drag.get('dragNode').all('.icon').setStyle('vertical-align', 'baseline');
},
drag_dropmiss: function(e) {
// Missed the target, but we assume the user intended to drop it
// on the last ghost node location, e.drag and e.drop should be
// prepared by global_drag_dropmiss parent so simulate drop_hit(e).
this.drop_hit(e);
},
drop_hit: function(e) {
var drag = e.drag;
// Get a reference to our drag node
var dragnode = drag.get('node');
// Add spinner if it not there
var actionarea = dragnode.one(CSS.ACTIONAREA);
var spinner = M.util.add_spinner(Y, actionarea);
var params = {};
// Handle any variables which we must pass back through to
var pageparams = this.get('config').pageparams;
var varname;
for (varname in pageparams) {
params[varname] = pageparams[varname];
}
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseid = this.get('courseid');
params.quizid = this.get('quizid');
params['class'] = 'resource';
params.field = 'move';
params.id = Number(Y.Moodle.mod_quiz.util.slot.getId(dragnode));
params.sectionId = Y.Moodle.core_course.util.section.getId(dragnode.ancestor('li.section', true));
var previousslot = dragnode.previous(SELECTOR.SLOT);
if (previousslot) {
params.previousid = Number(Y.Moodle.mod_quiz.util.slot.getId(previousslot));
}
var previouspage = dragnode.previous(SELECTOR.PAGE);
if (previouspage) {
params.page = Number(Y.Moodle.mod_quiz.util.page.getId(previouspage));
}
// Do AJAX request
var uri = M.cfg.wwwroot + this.get('ajaxurl');
Y.io(uri, {
method: 'POST',
data: params,
on: {
start: function() {
this.lock_drag_handle(drag, CSS.EDITINGMOVE);
spinner.show();
},
success: function(tid, response) {
var responsetext = Y.JSON.parse(response.responseText);
var params = {element: dragnode, visible: responsetext.visible};
M.mod_quiz.quizbase.invoke_function('set_visibility_resource_ui', params);
this.unlock_drag_handle(drag, CSS.EDITINGMOVE);
window.setTimeout(function() {
spinner.hide();
}, 250);
M.mod_quiz.resource_toolbox.reorganise_edit_page();
},
failure: function(tid, response) {
this.ajax_failure(response);
this.unlock_drag_handle(drag, CSS.SECTIONHANDLE);
spinner.hide();
window.location.reload(true);
}
},
context: this
});
},
global_drop_over: function(e) {
// Overriding parent method so we can stop the slots being dragged before the first page node.
// Check that drop object belong to correct group.
if (!e.drop || !e.drop.inGroup(this.groups)) {
return;
}
// Get a reference to our drag and drop nodes.
var drag = e.drag.get('node'),
drop = e.drop.get('node');
// Save last drop target for the case of missed target processing.
this.lastdroptarget = e.drop;
// Are we dropping within the same parent node?
if (drop.hasClass(this.samenodeclass)) {
var where;
if (this.goingup) {
where = "before";
} else {
where = "after";
}
drop.insert(drag, where);
} else if ((drop.hasClass(this.parentnodeclass) || drop.test('[data-droptarget="1"]')) && !drop.contains(drag)) {
// We are dropping on parent node and it is empty
if (this.goingup) {
drop.append(drag);
} else {
drop.prepend(drag);
}
}
this.drop_over(e);
}
}, {
NAME: 'mod_quiz-dragdrop-resource',
ATTRS: {
courseid: {
value: null
},
quizid: {
value: null
},
ajaxurl: {
value: 0
},
config: {
value: 0
}
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.init_resource_dragdrop = function(params) {
new DRAGRESOURCE(params);
};
}, '@VERSION@', {
"requires": [
"base",
"node",
"io",
"dom",
"dd",
"dd-scroll",
"moodle-core-dragdrop",
"moodle-core-notification",
"moodle-mod_quiz-quizbase",
"moodle-mod_quiz-util-base",
"moodle-mod_quiz-util-page",
"moodle-mod_quiz-util-slot",
"moodle-course-util"
]
});
File diff suppressed because one or more lines are too long
@@ -0,0 +1,547 @@
YUI.add('moodle-mod_quiz-dragdrop', function (Y, NAME) {
/* eslint-disable no-unused-vars */
/**
* Drag and Drop for Quiz sections and slots.
*
* @module moodle-mod-quiz-dragdrop
*/
var CSS = {
ACTIONAREA: '.actions',
ACTIVITY: 'activity',
ACTIVITYINSTANCE: 'activityinstance',
CONTENT: 'content',
COURSECONTENT: 'mod-quiz-edit-content',
EDITINGMOVE: 'editing_move',
ICONCLASS: 'iconsmall',
JUMPMENU: 'jumpmenu',
LEFT: 'left',
LIGHTBOX: 'lightbox',
MOVEDOWN: 'movedown',
MOVEUP: 'moveup',
PAGE: 'page',
PAGECONTENT: 'page-content',
RIGHT: 'right',
SECTION: 'slots',
SECTIONADDMENUS: 'section_add_menus',
SECTIONHANDLE: 'section-handle',
SLOTS: 'slots',
SUMMARY: 'summary',
SECTIONDRAGGABLE: 'sectiondraggable'
},
// The CSS selectors we use.
SELECTOR = {
PAGE: 'li.page',
SLOT: 'li.slot'
};
/**
* Section drag and drop.
*
* @class M.mod_quiz.dragdrop.section
* @constructor
* @extends M.core.dragdrop
*/
var DRAGSECTION = function() {
DRAGSECTION.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGSECTION, M.core.dragdrop, {
sectionlistselector: null,
initializer: function() {
// Set group for parent class
this.groups = [CSS.SECTIONDRAGGABLE];
this.samenodeclass = 'section';
this.parentnodeclass = 'slots';
// Check if we are in single section mode
if (Y.Node.one('.' + CSS.JUMPMENU)) {
return false;
}
// Initialise sections dragging
this.sectionlistselector = 'li.section';
if (this.sectionlistselector) {
this.sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + this.sectionlistselector;
this.setup_for_section(this.sectionlistselector);
// Make each li element in the lists of sections draggable
var del = new Y.DD.Delegate({
container: '.' + CSS.COURSECONTENT,
nodes: '.' + CSS.SECTIONDRAGGABLE,
target: true,
handles: ['.' + CSS.LEFT],
dragConfig: {groups: this.groups}
});
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .mod-quiz-edit-content
constrain: '#' + CSS.PAGECONTENT,
stickY: true
});
del.dd.plug(Y.Plugin.DDWinScroll);
}
},
/**
* Apply dragdrop features to the specified selector or node that refers to section(s)
*
* @method setup_for_section
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_section: function(baseselector) {
Y.Node.all(baseselector).each(function(sectionnode) {
// Determine the section ID
var sectionid = Y.Moodle.core_course.util.section.getId(sectionnode);
// We skip the top section as it is not draggable
if (sectionid > 0) {
// Remove move icons
var movedown = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEDOWN);
var moveup = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEUP);
// Add dragger icon
var title = M.util.get_string('movesection', 'moodle', sectionid);
var cssleft = sectionnode.one('.' + CSS.LEFT);
if ((movedown || moveup) && cssleft) {
cssleft.setStyle('cursor', 'move');
cssleft.appendChild(this.get_drag_handle(title, CSS.SECTIONHANDLE, 'icon', true));
if (moveup) {
moveup.remove();
}
if (movedown) {
movedown.remove();
}
// This section can be moved - add the class to indicate this to Y.DD.
sectionnode.addClass(CSS.SECTIONDRAGGABLE);
}
}
}, this);
},
/*
* Drag-dropping related functions
*/
drag_start: function(e) {
// Get our drag object
var drag = e.target;
// Creat a dummy structure of the outer elemnents for clean styles application
var containernode = Y.Node.create('<ul class="slots"></ul>');
var sectionnode = Y.Node.create('<ul class="section"></ul>');
sectionnode.setStyle('margin', 0);
sectionnode.setContent(drag.get('node').get('innerHTML'));
containernode.appendChild(sectionnode);
drag.get('dragNode').setContent(containernode);
drag.get('dragNode').addClass(CSS.COURSECONTENT);
},
drag_dropmiss: function(e) {
// Missed the target, but we assume the user intended to drop it
// on the last last ghost node location, e.drag and e.drop should be
// prepared by global_drag_dropmiss parent so simulate drop_hit(e).
this.drop_hit(e);
},
get_section_index: function(node) {
var sectionlistselector = '.' + CSS.COURSECONTENT + ' li.section',
sectionList = Y.all(sectionlistselector),
nodeIndex = sectionList.indexOf(node),
zeroIndex = sectionList.indexOf(Y.one('#section-0'));
return (nodeIndex - zeroIndex);
},
drop_hit: function(e) {
var drag = e.drag;
// Get references to our nodes and their IDs.
var dragnode = drag.get('node'),
dragnodeid = Y.Moodle.core_course.util.section.getId(dragnode),
loopstart = dragnodeid,
dropnodeindex = this.get_section_index(dragnode),
loopend = dropnodeindex;
if (dragnodeid === dropnodeindex) {
return;
}
if (loopstart > loopend) {
// If we're going up, we need to swap the loop order
// because loops can't go backwards.
loopstart = dropnodeindex;
loopend = dragnodeid;
}
// Get the list of nodes.
drag.get('dragNode').removeClass(CSS.COURSECONTENT);
var sectionlist = Y.Node.all(this.sectionlistselector);
// Add a lightbox if it's not there.
var lightbox = M.util.add_lightbox(Y, dragnode);
// Handle any variables which we must pass via AJAX.
var params = {},
pageparams = this.get('config').pageparams,
varname;
for (varname in pageparams) {
if (!pageparams.hasOwnProperty(varname)) {
continue;
}
params[varname] = pageparams[varname];
}
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseid = this.get('courseid');
params.quizid = this.get('quizid');
params['class'] = 'section';
params.field = 'move';
params.id = dragnodeid;
params.value = dropnodeindex;
// Perform the AJAX request.
var uri = M.cfg.wwwroot + this.get('ajaxurl');
Y.io(uri, {
method: 'POST',
data: params,
on: {
start: function() {
lightbox.show();
},
success: function(tid, response) {
// Update section titles, we can't simply swap them as
// they might have custom title
try {
var responsetext = Y.JSON.parse(response.responseText);
if (responsetext.error) {
new M.core.ajaxException(responsetext);
}
M.mod_quiz.edit.process_sections(Y, sectionlist, responsetext, loopstart, loopend);
} catch (e) {
// Ignore.
}
// Update all of the section IDs - first unset them, then set them
// to avoid duplicates in the DOM.
var index;
// Classic bubble sort algorithm is applied to the section
// nodes between original drag node location and the new one.
var swapped = false;
do {
swapped = false;
for (index = loopstart; index <= loopend; index++) {
if (Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) >
Y.Moodle.core_course.util.section.getId(sectionlist.item(index))) {
// Swap section id.
var sectionid = sectionlist.item(index - 1).get('id');
sectionlist.item(index - 1).set('id', sectionlist.item(index).get('id'));
sectionlist.item(index).set('id', sectionid);
// See what format needs to swap.
M.mod_quiz.edit.swap_sections(Y, index - 1, index);
// Update flag.
swapped = true;
}
}
loopend = loopend - 1;
} while (swapped);
window.setTimeout(function() {
lightbox.hide();
}, 250);
},
failure: function(tid, response) {
this.ajax_failure(response);
lightbox.hide();
}
},
context: this
});
}
}, {
NAME: 'mod_quiz-dragdrop-section',
ATTRS: {
courseid: {
value: null
},
quizid: {
value: null
},
ajaxurl: {
value: 0
},
config: {
value: 0
}
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.init_section_dragdrop = function(params) {
new DRAGSECTION(params);
};
/**
* Resource drag and drop.
*
* @class M.course.dragdrop.resource
* @constructor
* @extends M.core.dragdrop
*/
var DRAGRESOURCE = function() {
DRAGRESOURCE.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGRESOURCE, M.core.dragdrop, {
initializer: function() {
// Set group for parent class
this.groups = ['resource'];
this.samenodeclass = CSS.ACTIVITY;
this.parentnodeclass = CSS.SECTION;
this.samenodelabel = {
identifier: 'dragtoafter',
component: 'quiz'
};
this.parentnodelabel = {
identifier: 'dragtostart',
component: 'quiz'
};
// Go through all sections
this.setup_for_section();
// Initialise drag & drop for all resources/activities
var nodeselector = 'li.' + CSS.ACTIVITY;
var del = new Y.DD.Delegate({
container: '.' + CSS.COURSECONTENT,
nodes: nodeselector,
target: true,
handles: ['.' + CSS.EDITINGMOVE],
dragConfig: {groups: this.groups}
});
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false,
cloneNode: true
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .mod-quiz-edit-content
constrain: '#' + CSS.SLOTS
});
del.dd.plug(Y.Plugin.DDWinScroll);
M.mod_quiz.quizbase.register_module(this);
M.mod_quiz.dragres = this;
},
/**
* Apply dragdrop features to the specified selector or node that refers to section(s)
*
* @method setup_for_section
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_section: function() {
Y.Node.all('.mod-quiz-edit-content ul.slots ul.section').each(function(resources) {
resources.setAttribute('data-draggroups', this.groups.join(' '));
// Define empty ul as droptarget, so that item could be moved to empty list
new Y.DD.Drop({
node: resources,
groups: this.groups,
padding: '20 0 20 0'
});
// Initialise each resource/activity in this section
this.setup_for_resource('li.activity');
}, this);
},
/**
* Apply dragdrop features to the specified selector or node that refers to resource(s)
*
* @method setup_for_resource
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_resource: function(baseselector) {
Y.Node.all(baseselector).each(function(resourcesnode) {
// Replace move icons
var move = resourcesnode.one('a.' + CSS.EDITINGMOVE);
if (move) {
var resourcedraghandle = this.get_drag_handle(M.util.get_string('move', 'moodle'),
CSS.EDITINGMOVE, CSS.ICONCLASS, true);
move.replace(resourcedraghandle);
}
}, this);
},
drag_start: function(e) {
// Get our drag object
var drag = e.target;
drag.get('dragNode').setContent(drag.get('node').get('innerHTML'));
drag.get('dragNode').all('.icon').setStyle('vertical-align', 'baseline');
},
drag_dropmiss: function(e) {
// Missed the target, but we assume the user intended to drop it
// on the last ghost node location, e.drag and e.drop should be
// prepared by global_drag_dropmiss parent so simulate drop_hit(e).
this.drop_hit(e);
},
drop_hit: function(e) {
var drag = e.drag;
// Get a reference to our drag node
var dragnode = drag.get('node');
// Add spinner if it not there
var actionarea = dragnode.one(CSS.ACTIONAREA);
var spinner = M.util.add_spinner(Y, actionarea);
var params = {};
// Handle any variables which we must pass back through to
var pageparams = this.get('config').pageparams;
var varname;
for (varname in pageparams) {
params[varname] = pageparams[varname];
}
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseid = this.get('courseid');
params.quizid = this.get('quizid');
params['class'] = 'resource';
params.field = 'move';
params.id = Number(Y.Moodle.mod_quiz.util.slot.getId(dragnode));
params.sectionId = Y.Moodle.core_course.util.section.getId(dragnode.ancestor('li.section', true));
var previousslot = dragnode.previous(SELECTOR.SLOT);
if (previousslot) {
params.previousid = Number(Y.Moodle.mod_quiz.util.slot.getId(previousslot));
}
var previouspage = dragnode.previous(SELECTOR.PAGE);
if (previouspage) {
params.page = Number(Y.Moodle.mod_quiz.util.page.getId(previouspage));
}
// Do AJAX request
var uri = M.cfg.wwwroot + this.get('ajaxurl');
Y.io(uri, {
method: 'POST',
data: params,
on: {
start: function() {
this.lock_drag_handle(drag, CSS.EDITINGMOVE);
spinner.show();
},
success: function(tid, response) {
var responsetext = Y.JSON.parse(response.responseText);
var params = {element: dragnode, visible: responsetext.visible};
M.mod_quiz.quizbase.invoke_function('set_visibility_resource_ui', params);
this.unlock_drag_handle(drag, CSS.EDITINGMOVE);
window.setTimeout(function() {
spinner.hide();
}, 250);
M.mod_quiz.resource_toolbox.reorganise_edit_page();
},
failure: function(tid, response) {
this.ajax_failure(response);
this.unlock_drag_handle(drag, CSS.SECTIONHANDLE);
spinner.hide();
window.location.reload(true);
}
},
context: this
});
},
global_drop_over: function(e) {
// Overriding parent method so we can stop the slots being dragged before the first page node.
// Check that drop object belong to correct group.
if (!e.drop || !e.drop.inGroup(this.groups)) {
return;
}
// Get a reference to our drag and drop nodes.
var drag = e.drag.get('node'),
drop = e.drop.get('node');
// Save last drop target for the case of missed target processing.
this.lastdroptarget = e.drop;
// Are we dropping within the same parent node?
if (drop.hasClass(this.samenodeclass)) {
var where;
if (this.goingup) {
where = "before";
} else {
where = "after";
}
drop.insert(drag, where);
} else if ((drop.hasClass(this.parentnodeclass) || drop.test('[data-droptarget="1"]')) && !drop.contains(drag)) {
// We are dropping on parent node and it is empty
if (this.goingup) {
drop.append(drag);
} else {
drop.prepend(drag);
}
}
this.drop_over(e);
}
}, {
NAME: 'mod_quiz-dragdrop-resource',
ATTRS: {
courseid: {
value: null
},
quizid: {
value: null
},
ajaxurl: {
value: 0
},
config: {
value: 0
}
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.init_resource_dragdrop = function(params) {
new DRAGRESOURCE(params);
};
}, '@VERSION@', {
"requires": [
"base",
"node",
"io",
"dom",
"dd",
"dd-scroll",
"moodle-core-dragdrop",
"moodle-core-notification",
"moodle-mod_quiz-quizbase",
"moodle-mod_quiz-util-base",
"moodle-mod_quiz-util-page",
"moodle-mod_quiz-util-slot",
"moodle-course-util"
]
});
@@ -0,0 +1,55 @@
YUI.add('moodle-mod_quiz-modform', function (Y, NAME) {
/**
* The modform class has all the JavaScript specific to mod/quiz/mod_form.php.
*
* @module moodle-mod_quiz-modform
*/
var MODFORM = function() {
MODFORM.superclass.constructor.apply(this, arguments);
};
/**
* The coursebase class to provide shared functionality to Modules within
* Moodle.
*
* @class M.course.coursebase
* @constructor
*/
Y.extend(MODFORM, Y.Base, {
repaginateCheckbox: null,
qppSelect: null,
qppInitialValue: 0,
initializer: function() {
this.repaginateCheckbox = Y.one('#id_repaginatenow');
if (!this.repaginateCheckbox) {
// The checkbox only appears when editing an existing quiz.
return;
}
this.qppSelect = Y.one('#id_questionsperpage');
this.qppInitialValue = this.qppSelect.get('value');
this.qppSelect.on('change', this.qppChanged, this);
},
qppChanged: function() {
Y.later(50, this, function() {
if (!this.repaginateCheckbox.get('disabled')) {
this.repaginateCheckbox.set('checked', this.qppSelect.get('value') !== this.qppInitialValue);
}
});
}
});
// Ensure that M.mod_quiz exists and that coursebase is initialised correctly
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.modform = M.mod_quiz.modform || new MODFORM();
M.mod_quiz.modform.init = function() {
return new MODFORM();
};
}, '@VERSION@', {"requires": ["base", "node", "event"]});
@@ -0,0 +1 @@
YUI.add("moodle-mod_quiz-modform",function(e,i){var t=function(){t.superclass.constructor.apply(this,arguments)};e.extend(t,e.Base,{repaginateCheckbox:null,qppSelect:null,qppInitialValue:0,initializer:function(){this.repaginateCheckbox=e.one("#id_repaginatenow"),this.repaginateCheckbox&&(this.qppSelect=e.one("#id_questionsperpage"),this.qppInitialValue=this.qppSelect.get("value"),this.qppSelect.on("change",this.qppChanged,this))},qppChanged:function(){e.later(50,this,function(){this.repaginateCheckbox.get("disabled")||this.repaginateCheckbox.set("checked",this.qppSelect.get("value")!==this.qppInitialValue)})}}),M.mod_quiz=M.mod_quiz||{},M.mod_quiz.modform=M.mod_quiz.modform||new t,M.mod_quiz.modform.init=function(){return new t}},"@VERSION@",{requires:["base","node","event"]});
@@ -0,0 +1,55 @@
YUI.add('moodle-mod_quiz-modform', function (Y, NAME) {
/**
* The modform class has all the JavaScript specific to mod/quiz/mod_form.php.
*
* @module moodle-mod_quiz-modform
*/
var MODFORM = function() {
MODFORM.superclass.constructor.apply(this, arguments);
};
/**
* The coursebase class to provide shared functionality to Modules within
* Moodle.
*
* @class M.course.coursebase
* @constructor
*/
Y.extend(MODFORM, Y.Base, {
repaginateCheckbox: null,
qppSelect: null,
qppInitialValue: 0,
initializer: function() {
this.repaginateCheckbox = Y.one('#id_repaginatenow');
if (!this.repaginateCheckbox) {
// The checkbox only appears when editing an existing quiz.
return;
}
this.qppSelect = Y.one('#id_questionsperpage');
this.qppInitialValue = this.qppSelect.get('value');
this.qppSelect.on('change', this.qppChanged, this);
},
qppChanged: function() {
Y.later(50, this, function() {
if (!this.repaginateCheckbox.get('disabled')) {
this.repaginateCheckbox.set('checked', this.qppSelect.get('value') !== this.qppInitialValue);
}
});
}
});
// Ensure that M.mod_quiz exists and that coursebase is initialised correctly
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.modform = M.mod_quiz.modform || new MODFORM();
M.mod_quiz.modform.init = function() {
return new MODFORM();
};
}, '@VERSION@', {"requires": ["base", "node", "event"]});
@@ -0,0 +1,77 @@
YUI.add('moodle-mod_quiz-questionchooser', function (Y, NAME) {
var CSS = {
ADDNEWQUESTIONBUTTONS: '.menu [data-action="addquestion"]',
CREATENEWQUESTION: 'div.createnewquestion',
CHOOSERDIALOGUE: 'div.chooserdialoguebody',
CHOOSERHEADER: 'div.choosertitle'
};
/**
* The questionchooser class is responsible for instantiating and displaying the question chooser
* when viewing a quiz in editing mode.
*
* @class questionchooser
* @constructor
* @protected
* @extends M.core.chooserdialogue
*/
var QUESTIONCHOOSER = function() {
QUESTIONCHOOSER.superclass.constructor.apply(this, arguments);
};
Y.extend(QUESTIONCHOOSER, M.core.chooserdialogue, {
initializer: function() {
Y.one('body').delegate('click', this.display_dialogue, CSS.ADDNEWQUESTIONBUTTONS, this);
},
display_dialogue: function(e) {
e.preventDefault();
var dialogue = Y.one(CSS.CREATENEWQUESTION + ' ' + CSS.CHOOSERDIALOGUE),
header = Y.one(CSS.CREATENEWQUESTION + ' ' + CSS.CHOOSERHEADER);
if (this.container === null) {
// Setup the dialogue, and then prepare the chooser if it's not already been set up.
this.setup_chooser_dialogue(dialogue, header, {});
this.prepare_chooser();
}
// Update all of the hidden fields within the questionbank form.
var parameters = Y.QueryString.parse(e.currentTarget.get('search').substring(1));
var form = this.container.one('form');
this.parameters_to_hidden_input(parameters, form, 'returnurl');
this.parameters_to_hidden_input(parameters, form, 'cmid');
this.parameters_to_hidden_input(parameters, form, 'category');
this.parameters_to_hidden_input(parameters, form, 'addonpage');
this.parameters_to_hidden_input(parameters, form, 'appendqnumstring');
// Display the chooser dialogue.
this.display_chooser(e);
},
parameters_to_hidden_input: function(parameters, form, name) {
var value;
if (parameters.hasOwnProperty(name)) {
value = parameters[name];
} else {
value = '';
}
var input = form.one('input[name=' + name + ']');
if (!input) {
input = form.appendChild('<input type="hidden">');
input.set('name', name);
}
input.set('value', value);
}
}, {
NAME: 'mod_quiz-questionchooser'
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.init_questionchooser = function() {
M.mod_quiz.question_chooser = new QUESTIONCHOOSER({});
return M.mod_quiz.question_chooser;
};
}, '@VERSION@', {"requires": ["moodle-core-chooserdialogue", "moodle-mod_quiz-util", "querystring-parse"]});
@@ -0,0 +1 @@
YUI.add("moodle-mod_quiz-questionchooser",function(t,e){var o='.menu [data-action="addquestion"]',n="div.createnewquestion",r="div.chooserdialoguebody",s="div.choosertitle",i=function(){i.superclass.constructor.apply(this,arguments)};t.extend(i,M.core.chooserdialogue,{initializer:function(){t.one("body").delegate("click",this.display_dialogue,o,this)},display_dialogue:function(e){var o,i;e.preventDefault(),o=t.one(n+" "+r),i=t.one(n+" "+s),null===this.container&&(this.setup_chooser_dialogue(o,i,{}),this.prepare_chooser()),o=t.QueryString.parse(e.currentTarget.get("search").substring(1)),i=this.container.one("form"),this.parameters_to_hidden_input(o,i,"returnurl"),this.parameters_to_hidden_input(o,i,"cmid"),this.parameters_to_hidden_input(o,i,"category"),this.parameters_to_hidden_input(o,i,"addonpage"),this.parameters_to_hidden_input(o,i,"appendqnumstring"),this.display_chooser(e)},parameters_to_hidden_input:function(e,o,i){var e=e.hasOwnProperty(i)?e[i]:"",t=o.one("input[name="+i+"]");t||(t=o.appendChild('<input type="hidden">')).set("name",i),t.set("value",e)}},{NAME:"mod_quiz-questionchooser"}),M.mod_quiz=M.mod_quiz||{},M.mod_quiz.init_questionchooser=function(){return M.mod_quiz.question_chooser=new i({}),M.mod_quiz.question_chooser}},"@VERSION@",{requires:["moodle-core-chooserdialogue","moodle-mod_quiz-util","querystring-parse"]});
@@ -0,0 +1,77 @@
YUI.add('moodle-mod_quiz-questionchooser', function (Y, NAME) {
var CSS = {
ADDNEWQUESTIONBUTTONS: '.menu [data-action="addquestion"]',
CREATENEWQUESTION: 'div.createnewquestion',
CHOOSERDIALOGUE: 'div.chooserdialoguebody',
CHOOSERHEADER: 'div.choosertitle'
};
/**
* The questionchooser class is responsible for instantiating and displaying the question chooser
* when viewing a quiz in editing mode.
*
* @class questionchooser
* @constructor
* @protected
* @extends M.core.chooserdialogue
*/
var QUESTIONCHOOSER = function() {
QUESTIONCHOOSER.superclass.constructor.apply(this, arguments);
};
Y.extend(QUESTIONCHOOSER, M.core.chooserdialogue, {
initializer: function() {
Y.one('body').delegate('click', this.display_dialogue, CSS.ADDNEWQUESTIONBUTTONS, this);
},
display_dialogue: function(e) {
e.preventDefault();
var dialogue = Y.one(CSS.CREATENEWQUESTION + ' ' + CSS.CHOOSERDIALOGUE),
header = Y.one(CSS.CREATENEWQUESTION + ' ' + CSS.CHOOSERHEADER);
if (this.container === null) {
// Setup the dialogue, and then prepare the chooser if it's not already been set up.
this.setup_chooser_dialogue(dialogue, header, {});
this.prepare_chooser();
}
// Update all of the hidden fields within the questionbank form.
var parameters = Y.QueryString.parse(e.currentTarget.get('search').substring(1));
var form = this.container.one('form');
this.parameters_to_hidden_input(parameters, form, 'returnurl');
this.parameters_to_hidden_input(parameters, form, 'cmid');
this.parameters_to_hidden_input(parameters, form, 'category');
this.parameters_to_hidden_input(parameters, form, 'addonpage');
this.parameters_to_hidden_input(parameters, form, 'appendqnumstring');
// Display the chooser dialogue.
this.display_chooser(e);
},
parameters_to_hidden_input: function(parameters, form, name) {
var value;
if (parameters.hasOwnProperty(name)) {
value = parameters[name];
} else {
value = '';
}
var input = form.one('input[name=' + name + ']');
if (!input) {
input = form.appendChild('<input type="hidden">');
input.set('name', name);
}
input.set('value', value);
}
}, {
NAME: 'mod_quiz-questionchooser'
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.init_questionchooser = function() {
M.mod_quiz.question_chooser = new QUESTIONCHOOSER({});
return M.mod_quiz.question_chooser;
};
}, '@VERSION@', {"requires": ["moodle-core-chooserdialogue", "moodle-mod_quiz-util", "querystring-parse"]});
@@ -0,0 +1,142 @@
YUI.add('moodle-mod_quiz-quizbase', function (Y, NAME) {
/**
* The quizbase class to provide shared functionality to Modules within Moodle.
*
* @module moodle-mod_quiz-quizbase
*/
var QUIZBASENAME = 'mod_quiz-quizbase';
var QUIZBASE = function() {
QUIZBASE.superclass.constructor.apply(this, arguments);
};
/**
* The coursebase class to provide shared functionality to Modules within
* Moodle.
*
* @class M.course.coursebase
* @constructor
*/
Y.extend(QUIZBASE, Y.Base, {
// Registered Modules
registermodules: [],
/**
* Register a new Javascript Module
*
* @method register_module
* @param {Object} The instantiated module to call functions on
* @chainable
*/
register_module: function(object) {
this.registermodules.push(object);
return this;
},
/**
* Invoke the specified function in all registered modules with the given arguments
*
* @method invoke_function
* @param {String} functionname The name of the function to call
* @param {mixed} args The argument supplied to the function
* @chainable
*/
invoke_function: function(functionname, args) {
var module;
for (module in this.registermodules) {
if (functionname in this.registermodules[module]) {
this.registermodules[module][functionname](args);
}
}
return this;
}
}, {
NAME: QUIZBASENAME,
ATTRS: {}
});
// Ensure that M.course exists and that coursebase is initialised correctly
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.quizbase = M.mod_quiz.quizbase || new QUIZBASE();
// Abstract functions that needs to be defined per format (course/format/somename/format.js)
M.mod_quiz.edit = M.mod_quiz.edit || {};
/**
* Swap section (should be defined in format.js if requred)
*
* @param {YUI} Y YUI3 instance
* @param {string} node1 node to swap to
* @param {string} node2 node to swap with
* @return {NodeList} section list
*/
M.mod_quiz.edit.swap_sections = function(Y, node1, node2) {
var CSS = {
COURSECONTENT: 'mod-quiz-edit-content',
SECTIONADDMENUS: 'section_add_menus'
};
var sectionlist = Y.Node.all('.' + CSS.COURSECONTENT + ' li.section');
// Swap menus.
sectionlist.item(node1).one('.' + CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.' + CSS.SECTIONADDMENUS));
};
/**
* Process sections after ajax response (should be defined in format.js)
* If some response is expected, we pass it over to format, as it knows better
* hot to process it.
*
* @param {YUI} Y YUI3 instance
* @param {NodeList} list of sections
* @param {array} response ajax response
* @param {string} sectionfrom first affected section
* @param {string} sectionto last affected section
* @return void
*/
M.mod_quiz.edit.process_sections = function(Y, sectionlist, response, sectionfrom, sectionto) {
var CSS = {
SECTIONNAME: 'sectionname'
},
SELECTORS = {
SECTIONLEFTSIDE: '.left .section-handle .icon'
};
if (response.action === 'move') {
// If moving up swap around 'sectionfrom' and 'sectionto' so the that loop operates.
if (sectionfrom > sectionto) {
var temp = sectionto;
sectionto = sectionfrom;
sectionfrom = temp;
}
// Update titles and move icons in all affected sections.
var ele, str, stridx, newstr;
for (var i = sectionfrom; i <= sectionto; i++) {
// Update section title.
sectionlist.item(i).one('.' + CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
// Update move icon.
ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
str = ele.getAttribute('alt');
stridx = str.lastIndexOf(' ');
newstr = str.substr(0, stridx + 1) + i;
ele.setAttribute('alt', newstr);
ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed.
// Remove the current class as section has been moved.
sectionlist.item(i).removeClass('current');
}
// If there is a current section, apply corresponding class in order to highlight it.
if (response.current !== -1) {
// Add current class to the required section.
sectionlist.item(response.current).addClass('current');
}
}
};
}, '@VERSION@', {"requires": ["base", "node"]});
@@ -0,0 +1 @@
YUI.add("moodle-mod_quiz-quizbase",function(e,t){var i=function(){i.superclass.constructor.apply(this,arguments)};e.extend(i,e.Base,{registermodules:[],register_module:function(e){return this.registermodules.push(e),this},invoke_function:function(e,t){for(var i in this.registermodules)e in this.registermodules[i]&&this.registermodules[i][e](t);return this}},{NAME:"mod_quiz-quizbase",ATTRS:{}}),M.mod_quiz=M.mod_quiz||{},M.mod_quiz.quizbase=M.mod_quiz.quizbase||new i,M.mod_quiz.edit=M.mod_quiz.edit||{},M.mod_quiz.edit.swap_sections=function(e,t,i){var s="mod-quiz-edit-content",o="section_add_menus",s=e.Node.all("."+s+" li.section");s.item(t).one("."+o).swap(s.item(i).one("."+o))},M.mod_quiz.edit.process_sections=function(a,e,t,i,s){var o,n,u,r,d,m="sectionname",c=".left .section-handle .icon";if("move"===t.action){for(s<i&&(o=s,s=i,i=o),d=i;d<=s;d++)e.item(d).one("."+m).setContent(t.sectiontitles[d]),r=(u=(n=e.item(d).one(c)).getAttribute("alt")).lastIndexOf(" "),r=u.substr(0,r+1)+d,n.setAttribute("alt",r),n.setAttribute("title",r),e.item(d).removeClass("current");-1!==t.current&&e.item(t.current).addClass("current")}}},"@VERSION@",{requires:["base","node"]});
@@ -0,0 +1,142 @@
YUI.add('moodle-mod_quiz-quizbase', function (Y, NAME) {
/**
* The quizbase class to provide shared functionality to Modules within Moodle.
*
* @module moodle-mod_quiz-quizbase
*/
var QUIZBASENAME = 'mod_quiz-quizbase';
var QUIZBASE = function() {
QUIZBASE.superclass.constructor.apply(this, arguments);
};
/**
* The coursebase class to provide shared functionality to Modules within
* Moodle.
*
* @class M.course.coursebase
* @constructor
*/
Y.extend(QUIZBASE, Y.Base, {
// Registered Modules
registermodules: [],
/**
* Register a new Javascript Module
*
* @method register_module
* @param {Object} The instantiated module to call functions on
* @chainable
*/
register_module: function(object) {
this.registermodules.push(object);
return this;
},
/**
* Invoke the specified function in all registered modules with the given arguments
*
* @method invoke_function
* @param {String} functionname The name of the function to call
* @param {mixed} args The argument supplied to the function
* @chainable
*/
invoke_function: function(functionname, args) {
var module;
for (module in this.registermodules) {
if (functionname in this.registermodules[module]) {
this.registermodules[module][functionname](args);
}
}
return this;
}
}, {
NAME: QUIZBASENAME,
ATTRS: {}
});
// Ensure that M.course exists and that coursebase is initialised correctly
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.quizbase = M.mod_quiz.quizbase || new QUIZBASE();
// Abstract functions that needs to be defined per format (course/format/somename/format.js)
M.mod_quiz.edit = M.mod_quiz.edit || {};
/**
* Swap section (should be defined in format.js if requred)
*
* @param {YUI} Y YUI3 instance
* @param {string} node1 node to swap to
* @param {string} node2 node to swap with
* @return {NodeList} section list
*/
M.mod_quiz.edit.swap_sections = function(Y, node1, node2) {
var CSS = {
COURSECONTENT: 'mod-quiz-edit-content',
SECTIONADDMENUS: 'section_add_menus'
};
var sectionlist = Y.Node.all('.' + CSS.COURSECONTENT + ' li.section');
// Swap menus.
sectionlist.item(node1).one('.' + CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.' + CSS.SECTIONADDMENUS));
};
/**
* Process sections after ajax response (should be defined in format.js)
* If some response is expected, we pass it over to format, as it knows better
* hot to process it.
*
* @param {YUI} Y YUI3 instance
* @param {NodeList} list of sections
* @param {array} response ajax response
* @param {string} sectionfrom first affected section
* @param {string} sectionto last affected section
* @return void
*/
M.mod_quiz.edit.process_sections = function(Y, sectionlist, response, sectionfrom, sectionto) {
var CSS = {
SECTIONNAME: 'sectionname'
},
SELECTORS = {
SECTIONLEFTSIDE: '.left .section-handle .icon'
};
if (response.action === 'move') {
// If moving up swap around 'sectionfrom' and 'sectionto' so the that loop operates.
if (sectionfrom > sectionto) {
var temp = sectionto;
sectionto = sectionfrom;
sectionfrom = temp;
}
// Update titles and move icons in all affected sections.
var ele, str, stridx, newstr;
for (var i = sectionfrom; i <= sectionto; i++) {
// Update section title.
sectionlist.item(i).one('.' + CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
// Update move icon.
ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
str = ele.getAttribute('alt');
stridx = str.lastIndexOf(' ');
newstr = str.substr(0, stridx + 1) + i;
ele.setAttribute('alt', newstr);
ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed.
// Remove the current class as section has been moved.
sectionlist.item(i).removeClass('current');
}
// If there is a current section, apply corresponding class in order to highlight it.
if (response.current !== -1) {
// Add current class to the required section.
sectionlist.item(response.current).addClass('current');
}
}
};
}, '@VERSION@', {"requires": ["base", "node"]});
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
YUI.add('moodle-mod_quiz-util-base', function (Y, NAME) {
/**
* The Moodle.mod_quiz.util classes provide quiz-related utility functions.
*
* @module moodle-mod_quiz-util
* @main
*/
Y.namespace('Moodle.mod_quiz.util');
/**
* A collection of general utility functions for use in quiz.
*
* @class Moodle.mod_quiz.util
* @static
*/
}, '@VERSION@');
@@ -0,0 +1 @@
YUI.add("moodle-mod_quiz-util-base",function(o,d){o.namespace("Moodle.mod_quiz.util")},"@VERSION@");
@@ -0,0 +1,20 @@
YUI.add('moodle-mod_quiz-util-base', function (Y, NAME) {
/**
* The Moodle.mod_quiz.util classes provide quiz-related utility functions.
*
* @module moodle-mod_quiz-util
* @main
*/
Y.namespace('Moodle.mod_quiz.util');
/**
* A collection of general utility functions for use in quiz.
*
* @class Moodle.mod_quiz.util
* @static
*/
}, '@VERSION@');
@@ -0,0 +1,345 @@
YUI.add('moodle-mod_quiz-util-page', function (Y, NAME) {
/* global YUI */
/**
* A collection of utility classes for use with pages.
*
* @module moodle-mod_quiz-util
* @submodule moodle-mod_quiz-util-page
*/
Y.namespace('Moodle.mod_quiz.util.page');
/**
* A collection of utility classes for use with pages.
*
* @class Moodle.mod_quiz.util.page
* @static
*/
Y.Moodle.mod_quiz.util.page = {
CSS: {
PAGE: 'page'
},
CONSTANTS: {
ACTIONMENUIDPREFIX: 'action-menu-',
ACTIONMENUBARIDSUFFIX: '-menubar',
ACTIONMENUMENUIDSUFFIX: '-menu',
PAGEIDPREFIX: 'page-',
PAGENUMBERPREFIX: M.util.get_string('page', 'moodle') + ' '
},
SELECTORS: {
ACTIONMENU: 'div.moodle-actionmenu',
ACTIONMENUBAR: '.menubar',
ACTIONMENUMENU: '.menu',
ADDASECTION: '[data-action="addasection"]',
PAGE: 'li.page',
INSTANCENAME: '.instancename',
NUMBER: 'h4'
},
/**
* Retrieve the page item from one of it's child Nodes.
*
* @method getPageFromComponent
* @param pagecomponent {Node} The component Node.
* @return {Node|null} The Page Node.
*/
getPageFromComponent: function(pagecomponent) {
return Y.one(pagecomponent).ancestor(this.SELECTORS.PAGE, true);
},
/**
* Retrieve the page item from one of it's previous siblings.
*
* @method getPageFromSlot
* @param pagecomponent {Node} The component Node.
* @return {Node|null} The Page Node.
*/
getPageFromSlot: function(slot) {
return Y.one(slot).previous(this.SELECTORS.PAGE);
},
/**
* Returns the page ID for the provided page.
*
* @method getId
* @param page {Node} The page to find an ID for.
* @return {Number|false} The ID of the page in question or false if no ID was found.
*/
getId: function(page) {
// We perform a simple substitution operation to get the ID.
var id = page.get('id').replace(
this.CONSTANTS.PAGEIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
},
/**
* Updates the page id for the provided page.
*
* @method setId
* @param page {Node} The page to update the number for.
* @param id int The id value.
* @return void
*/
setId: function(page, id) {
page.set('id', this.CONSTANTS.PAGEIDPREFIX + id);
},
/**
* Determines the page name for the provided page.
*
* @method getName
* @param page {Node} The page to find a name for.
* @return {string|false} The name of the page in question or false if no ID was found.
*/
getName: function(page) {
var instance = page.one(this.SELECTORS.INSTANCENAME);
if (instance) {
return instance.get('firstChild').get('data');
}
return null;
},
/**
* Determines the page number for the provided page.
*
* @method getNumber
* @param page {Node} The page to find a number for.
* @return {Number|false} The number of the page in question or false if no number was found.
*/
getNumber: function(page) {
// We perform a simple substitution operation to get the number.
var number = page.one(this.SELECTORS.NUMBER).get('text').replace(
this.CONSTANTS.PAGENUMBERPREFIX, '');
// Attempt to validate the ID.
number = parseInt(number, 10);
if (typeof number === 'number' && isFinite(number)) {
return number;
}
return false;
},
/**
* Updates the page number for the provided page.
*
* @method setNumber
* @param page {Node} The page to update the number for.
* @return void
*/
setNumber: function(page, number) {
page.one(this.SELECTORS.NUMBER).set('text', this.CONSTANTS.PAGENUMBERPREFIX + number);
},
/**
* Returns a list of all page elements.
*
* @method getPages
* @return {node[]} An array containing page nodes.
*/
getPages: function() {
return Y.all(Y.Moodle.mod_quiz.util.slot.SELECTORS.PAGECONTENT + ' ' +
Y.Moodle.mod_quiz.util.slot.SELECTORS.SECTIONUL + ' ' +
this.SELECTORS.PAGE);
},
/**
* Is the given element a page element?
*
* @method isPage
* @param page Page node
* @return boolean
*/
isPage: function(page) {
if (!page) {
return false;
}
return page.hasClass(this.CSS.PAGE);
},
/**
* Does the page have atleast one slot?
*
* @method isEmpty
* @param page Page node
* @return boolean
*/
isEmpty: function(page) {
var activity = page.next('li.activity');
if (!activity) {
return true;
}
return !activity.hasClass('slot');
},
/**
* Add a page and related elements to the list of slots.
*
* @method add
* @param beforenode Int | Node | HTMLElement | String to add
* @return page Page node
*/
add: function(beforenode) {
var pagenumber = this.getNumber(this.getPageFromSlot(beforenode)) + 1;
var pagehtml = M.mod_quiz.resource_toolbox.get('config').pagehtml;
// Normalise the page number.
pagehtml = pagehtml.replace(/%%PAGENUMBER%%/g, pagenumber);
// Create the page node.
var page = Y.Node.create(pagehtml);
// Assign is as a drop target.
YUI().use('dd-drop', function(Y) {
var drop = new Y.DD.Drop({
node: page,
groups: M.mod_quiz.dragres.groups
});
page.drop = drop;
});
// Insert in the correct place.
beforenode.insert(page, 'after');
// Enhance the add menu to make if fully visible and clickable.
if (typeof M.core.actionmenu !== "undefined") {
M.core.actionmenu.newDOMNode(page);
}
return page;
},
/**
* Remove a page and related elements from the list of slots.
*
* @method remove
* @param page Page node
* @return void
*/
remove: function(page, keeppagebreak) {
// Remove page break from previous slot.
var previousslot = page.previous(Y.Moodle.mod_quiz.util.slot.SELECTORS.SLOT);
if (!keeppagebreak && previousslot) {
Y.Moodle.mod_quiz.util.slot.removePageBreak(previousslot);
}
page.remove();
},
/**
* Reset the order of the numbers given to each page.
*
* @method reorderPages
* @return void
*/
reorderPages: function() {
// Get list of page nodes.
var pages = this.getPages();
var currentpagenumber = 0;
// Loop through pages incrementing the number each time.
pages.each(function(page) {
// Is the page empty?
if (this.isEmpty(page)) {
var keeppagebreak = page.next('li.slot') ? true : false;
this.remove(page, keeppagebreak);
return;
}
currentpagenumber++;
// Set page number.
this.setNumber(page, currentpagenumber);
this.setId(page, currentpagenumber);
}, this);
// Reorder action menus
this.reorderActionMenus();
},
/**
* Reset the order of the numbers given to each action menu.
*
* @method reorderActionMenus
* @return void
*/
reorderActionMenus: function() {
// Get list of action menu nodes.
var actionmenus = this.getActionMenus();
// Loop through pages incrementing the number each time.
actionmenus.each(function(actionmenu, key) {
var previousActionMenu = actionmenus.item(key - 1),
previousActionMenunumber = 0;
if (previousActionMenu) {
previousActionMenunumber = this.getActionMenuId(previousActionMenu);
}
var id = previousActionMenunumber + 1;
// Set menu id.
this.setActionMenuId(actionmenu, id);
// Update action-menu-1-menubar
var menubar = actionmenu.one(this.SELECTORS.ACTIONMENUBAR);
menubar.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id + this.CONSTANTS.ACTIONMENUBARIDSUFFIX);
// Update action-menu-1-menu
var menumenu = actionmenu.one(this.SELECTORS.ACTIONMENUMENU);
menumenu.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id + this.CONSTANTS.ACTIONMENUMENUIDSUFFIX);
// Update the URL of the add-section action.
menumenu.one(this.SELECTORS.ADDASECTION).set('href',
menumenu.one(this.SELECTORS.ADDASECTION).get('href').replace(/\baddsectionatpage=\d+\b/, 'addsectionatpage=' + id));
}, this);
},
/**
* Returns a list of all page elements.
*
* @method getActionMenus
* @return {node[]} An array containing page nodes.
*/
getActionMenus: function() {
return Y.all(Y.Moodle.mod_quiz.util.slot.SELECTORS.PAGECONTENT + ' ' +
Y.Moodle.mod_quiz.util.slot.SELECTORS.SECTIONUL + ' ' +
this.SELECTORS.ACTIONMENU);
},
/**
* Returns the ID for the provided action menu.
*
* @method getId
* @param actionmenu {Node} The actionmenu to find an ID for.
* @return {Number|false} The ID of the actionmenu in question or false if no ID was found.
*/
getActionMenuId: function(actionmenu) {
// We perform a simple substitution operation to get the ID.
var id = actionmenu.get('id').replace(
this.CONSTANTS.ACTIONMENUIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
},
/**
* Updates the page id for the provided page.
*
* @method setId
* @param page {Node} The page to update the number for.
* @param id int The id value.
* @return void
*/
setActionMenuId: function(actionmenu, id) {
actionmenu.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id);
}
};
}, '@VERSION@', {"requires": ["node", "moodle-mod_quiz-util-base"]});
@@ -0,0 +1 @@
YUI.add("moodle-mod_quiz-util-page",function(n,e){n.namespace("Moodle.mod_quiz.util.page"),n.Moodle.mod_quiz.util.page={CSS:{PAGE:"page"},CONSTANTS:{ACTIONMENUIDPREFIX:"action-menu-",ACTIONMENUBARIDSUFFIX:"-menubar",ACTIONMENUMENUIDSUFFIX:"-menu",PAGEIDPREFIX:"page-",PAGENUMBERPREFIX:M.util.get_string("page","moodle")+" "},SELECTORS:{ACTIONMENU:"div.moodle-actionmenu",ACTIONMENUBAR:".menubar",ACTIONMENUMENU:".menu",ADDASECTION:'[data-action="addasection"]',PAGE:"li.page",INSTANCENAME:".instancename",NUMBER:"h4"},getPageFromComponent:function(e){return n.one(e).ancestor(this.SELECTORS.PAGE,!0)},getPageFromSlot:function(e){return n.one(e).previous(this.SELECTORS.PAGE)},getId:function(e){e=e.get("id").replace(this.CONSTANTS.PAGEIDPREFIX,"");return!("number"!=typeof(e=parseInt(e,10))||!isFinite(e))&&e},setId:function(e,t){e.set("id",this.CONSTANTS.PAGEIDPREFIX+t)},getName:function(e){e=e.one(this.SELECTORS.INSTANCENAME);return e?e.get("firstChild").get("data"):null},getNumber:function(e){e=e.one(this.SELECTORS.NUMBER).get("text").replace(this.CONSTANTS.PAGENUMBERPREFIX,"");return!("number"!=typeof(e=parseInt(e,10))||!isFinite(e))&&e},setNumber:function(e,t){e.one(this.SELECTORS.NUMBER).set("text",this.CONSTANTS.PAGENUMBERPREFIX+t)},getPages:function(){return n.all(n.Moodle.mod_quiz.util.slot.SELECTORS.PAGECONTENT+" "+n.Moodle.mod_quiz.util.slot.SELECTORS.SECTIONUL+" "+this.SELECTORS.PAGE)},isPage:function(e){return!!e&&e.hasClass(this.CSS.PAGE)},isEmpty:function(e){e=e.next("li.activity");return!e||!e.hasClass("slot")},add:function(e){var t=this.getNumber(this.getPageFromSlot(e))+1,i=M.mod_quiz.resource_toolbox.get("config").pagehtml,i=i.replace(/%%PAGENUMBER%%/g,t),o=n.Node.create(i);return YUI().use("dd-drop",function(e){e=new e.DD.Drop({node:o,groups:M.mod_quiz.dragres.groups});o.drop=e}),e.insert(o,"after"),"undefined"!=typeof M.core.actionmenu&&M.core.actionmenu.newDOMNode(o),o},remove:function(e,t){var i=e.previous(n.Moodle.mod_quiz.util.slot.SELECTORS.SLOT);!t&&i&&n.Moodle.mod_quiz.util.slot.removePageBreak(i),e.remove()},reorderPages:function(){var e=this.getPages(),i=0;e.each(function(e){var t;if(this.isEmpty(e))return t=!!e.next("li.slot"),void this.remove(e,t);i++,this.setNumber(e,i),this.setId(e,i)},this),this.reorderActionMenus()},reorderActionMenus:function(){var o=this.getActionMenus();o.each(function(e,t){var t=o.item(t-1),i=0;t&&(i=this.getActionMenuId(t)),this.setActionMenuId(e,t=i+1),e.one(this.SELECTORS.ACTIONMENUBAR).set("id",this.CONSTANTS.ACTIONMENUIDPREFIX+t+this.CONSTANTS.ACTIONMENUBARIDSUFFIX),(i=e.one(this.SELECTORS.ACTIONMENUMENU)).set("id",this.CONSTANTS.ACTIONMENUIDPREFIX+t+this.CONSTANTS.ACTIONMENUMENUIDSUFFIX),i.one(this.SELECTORS.ADDASECTION).set("href",i.one(this.SELECTORS.ADDASECTION).get("href").replace(/\baddsectionatpage=\d+\b/,"addsectionatpage="+t))},this)},getActionMenus:function(){return n.all(n.Moodle.mod_quiz.util.slot.SELECTORS.PAGECONTENT+" "+n.Moodle.mod_quiz.util.slot.SELECTORS.SECTIONUL+" "+this.SELECTORS.ACTIONMENU)},getActionMenuId:function(e){e=e.get("id").replace(this.CONSTANTS.ACTIONMENUIDPREFIX,"");return!("number"!=typeof(e=parseInt(e,10))||!isFinite(e))&&e},setActionMenuId:function(e,t){e.set("id",this.CONSTANTS.ACTIONMENUIDPREFIX+t)}}},"@VERSION@",{requires:["node","moodle-mod_quiz-util-base"]});
@@ -0,0 +1,345 @@
YUI.add('moodle-mod_quiz-util-page', function (Y, NAME) {
/* global YUI */
/**
* A collection of utility classes for use with pages.
*
* @module moodle-mod_quiz-util
* @submodule moodle-mod_quiz-util-page
*/
Y.namespace('Moodle.mod_quiz.util.page');
/**
* A collection of utility classes for use with pages.
*
* @class Moodle.mod_quiz.util.page
* @static
*/
Y.Moodle.mod_quiz.util.page = {
CSS: {
PAGE: 'page'
},
CONSTANTS: {
ACTIONMENUIDPREFIX: 'action-menu-',
ACTIONMENUBARIDSUFFIX: '-menubar',
ACTIONMENUMENUIDSUFFIX: '-menu',
PAGEIDPREFIX: 'page-',
PAGENUMBERPREFIX: M.util.get_string('page', 'moodle') + ' '
},
SELECTORS: {
ACTIONMENU: 'div.moodle-actionmenu',
ACTIONMENUBAR: '.menubar',
ACTIONMENUMENU: '.menu',
ADDASECTION: '[data-action="addasection"]',
PAGE: 'li.page',
INSTANCENAME: '.instancename',
NUMBER: 'h4'
},
/**
* Retrieve the page item from one of it's child Nodes.
*
* @method getPageFromComponent
* @param pagecomponent {Node} The component Node.
* @return {Node|null} The Page Node.
*/
getPageFromComponent: function(pagecomponent) {
return Y.one(pagecomponent).ancestor(this.SELECTORS.PAGE, true);
},
/**
* Retrieve the page item from one of it's previous siblings.
*
* @method getPageFromSlot
* @param pagecomponent {Node} The component Node.
* @return {Node|null} The Page Node.
*/
getPageFromSlot: function(slot) {
return Y.one(slot).previous(this.SELECTORS.PAGE);
},
/**
* Returns the page ID for the provided page.
*
* @method getId
* @param page {Node} The page to find an ID for.
* @return {Number|false} The ID of the page in question or false if no ID was found.
*/
getId: function(page) {
// We perform a simple substitution operation to get the ID.
var id = page.get('id').replace(
this.CONSTANTS.PAGEIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
},
/**
* Updates the page id for the provided page.
*
* @method setId
* @param page {Node} The page to update the number for.
* @param id int The id value.
* @return void
*/
setId: function(page, id) {
page.set('id', this.CONSTANTS.PAGEIDPREFIX + id);
},
/**
* Determines the page name for the provided page.
*
* @method getName
* @param page {Node} The page to find a name for.
* @return {string|false} The name of the page in question or false if no ID was found.
*/
getName: function(page) {
var instance = page.one(this.SELECTORS.INSTANCENAME);
if (instance) {
return instance.get('firstChild').get('data');
}
return null;
},
/**
* Determines the page number for the provided page.
*
* @method getNumber
* @param page {Node} The page to find a number for.
* @return {Number|false} The number of the page in question or false if no number was found.
*/
getNumber: function(page) {
// We perform a simple substitution operation to get the number.
var number = page.one(this.SELECTORS.NUMBER).get('text').replace(
this.CONSTANTS.PAGENUMBERPREFIX, '');
// Attempt to validate the ID.
number = parseInt(number, 10);
if (typeof number === 'number' && isFinite(number)) {
return number;
}
return false;
},
/**
* Updates the page number for the provided page.
*
* @method setNumber
* @param page {Node} The page to update the number for.
* @return void
*/
setNumber: function(page, number) {
page.one(this.SELECTORS.NUMBER).set('text', this.CONSTANTS.PAGENUMBERPREFIX + number);
},
/**
* Returns a list of all page elements.
*
* @method getPages
* @return {node[]} An array containing page nodes.
*/
getPages: function() {
return Y.all(Y.Moodle.mod_quiz.util.slot.SELECTORS.PAGECONTENT + ' ' +
Y.Moodle.mod_quiz.util.slot.SELECTORS.SECTIONUL + ' ' +
this.SELECTORS.PAGE);
},
/**
* Is the given element a page element?
*
* @method isPage
* @param page Page node
* @return boolean
*/
isPage: function(page) {
if (!page) {
return false;
}
return page.hasClass(this.CSS.PAGE);
},
/**
* Does the page have atleast one slot?
*
* @method isEmpty
* @param page Page node
* @return boolean
*/
isEmpty: function(page) {
var activity = page.next('li.activity');
if (!activity) {
return true;
}
return !activity.hasClass('slot');
},
/**
* Add a page and related elements to the list of slots.
*
* @method add
* @param beforenode Int | Node | HTMLElement | String to add
* @return page Page node
*/
add: function(beforenode) {
var pagenumber = this.getNumber(this.getPageFromSlot(beforenode)) + 1;
var pagehtml = M.mod_quiz.resource_toolbox.get('config').pagehtml;
// Normalise the page number.
pagehtml = pagehtml.replace(/%%PAGENUMBER%%/g, pagenumber);
// Create the page node.
var page = Y.Node.create(pagehtml);
// Assign is as a drop target.
YUI().use('dd-drop', function(Y) {
var drop = new Y.DD.Drop({
node: page,
groups: M.mod_quiz.dragres.groups
});
page.drop = drop;
});
// Insert in the correct place.
beforenode.insert(page, 'after');
// Enhance the add menu to make if fully visible and clickable.
if (typeof M.core.actionmenu !== "undefined") {
M.core.actionmenu.newDOMNode(page);
}
return page;
},
/**
* Remove a page and related elements from the list of slots.
*
* @method remove
* @param page Page node
* @return void
*/
remove: function(page, keeppagebreak) {
// Remove page break from previous slot.
var previousslot = page.previous(Y.Moodle.mod_quiz.util.slot.SELECTORS.SLOT);
if (!keeppagebreak && previousslot) {
Y.Moodle.mod_quiz.util.slot.removePageBreak(previousslot);
}
page.remove();
},
/**
* Reset the order of the numbers given to each page.
*
* @method reorderPages
* @return void
*/
reorderPages: function() {
// Get list of page nodes.
var pages = this.getPages();
var currentpagenumber = 0;
// Loop through pages incrementing the number each time.
pages.each(function(page) {
// Is the page empty?
if (this.isEmpty(page)) {
var keeppagebreak = page.next('li.slot') ? true : false;
this.remove(page, keeppagebreak);
return;
}
currentpagenumber++;
// Set page number.
this.setNumber(page, currentpagenumber);
this.setId(page, currentpagenumber);
}, this);
// Reorder action menus
this.reorderActionMenus();
},
/**
* Reset the order of the numbers given to each action menu.
*
* @method reorderActionMenus
* @return void
*/
reorderActionMenus: function() {
// Get list of action menu nodes.
var actionmenus = this.getActionMenus();
// Loop through pages incrementing the number each time.
actionmenus.each(function(actionmenu, key) {
var previousActionMenu = actionmenus.item(key - 1),
previousActionMenunumber = 0;
if (previousActionMenu) {
previousActionMenunumber = this.getActionMenuId(previousActionMenu);
}
var id = previousActionMenunumber + 1;
// Set menu id.
this.setActionMenuId(actionmenu, id);
// Update action-menu-1-menubar
var menubar = actionmenu.one(this.SELECTORS.ACTIONMENUBAR);
menubar.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id + this.CONSTANTS.ACTIONMENUBARIDSUFFIX);
// Update action-menu-1-menu
var menumenu = actionmenu.one(this.SELECTORS.ACTIONMENUMENU);
menumenu.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id + this.CONSTANTS.ACTIONMENUMENUIDSUFFIX);
// Update the URL of the add-section action.
menumenu.one(this.SELECTORS.ADDASECTION).set('href',
menumenu.one(this.SELECTORS.ADDASECTION).get('href').replace(/\baddsectionatpage=\d+\b/, 'addsectionatpage=' + id));
}, this);
},
/**
* Returns a list of all page elements.
*
* @method getActionMenus
* @return {node[]} An array containing page nodes.
*/
getActionMenus: function() {
return Y.all(Y.Moodle.mod_quiz.util.slot.SELECTORS.PAGECONTENT + ' ' +
Y.Moodle.mod_quiz.util.slot.SELECTORS.SECTIONUL + ' ' +
this.SELECTORS.ACTIONMENU);
},
/**
* Returns the ID for the provided action menu.
*
* @method getId
* @param actionmenu {Node} The actionmenu to find an ID for.
* @return {Number|false} The ID of the actionmenu in question or false if no ID was found.
*/
getActionMenuId: function(actionmenu) {
// We perform a simple substitution operation to get the ID.
var id = actionmenu.get('id').replace(
this.CONSTANTS.ACTIONMENUIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
},
/**
* Updates the page id for the provided page.
*
* @method setId
* @param page {Node} The page to update the number for.
* @param id int The id value.
* @return void
*/
setActionMenuId: function(actionmenu, id) {
actionmenu.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id);
}
};
}, '@VERSION@', {"requires": ["node", "moodle-mod_quiz-util-base"]});
@@ -0,0 +1,445 @@
YUI.add('moodle-mod_quiz-util-slot', function (Y, NAME) {
/**
* A collection of utility classes for use with slots.
*
* @module moodle-mod_quiz-util
* @submodule moodle-mod_quiz-util-slot
*/
Y.namespace('Moodle.mod_quiz.util.slot');
/**
* A collection of utility classes for use with slots.
*
* @class Moodle.mod_quiz.util.slot
* @static
*/
Y.Moodle.mod_quiz.util.slot = {
CSS: {
SLOT: 'slot',
QUESTIONTYPEDESCRIPTION: 'qtype_description',
CANNOT_DEPEND: 'question_dependency_cannot_depend'
},
CONSTANTS: {
SLOTIDPREFIX: 'slot-',
QUESTION: M.util.get_string('question', 'moodle')
},
SELECTORS: {
SLOT: 'li.slot',
INSTANCENAME: '.instancename',
NUMBER: 'span.slotnumber',
PAGECONTENT: 'div#page-content',
PAGEBREAK: 'span.page_split_join_wrapper',
ICON: '.icon',
QUESTIONTYPEDESCRIPTION: '.qtype_description',
SECTIONUL: 'ul.section',
DEPENDENCY_WRAPPER: '.question_dependency_wrapper',
DEPENDENCY_LINK: '.question_dependency_wrapper .cm-edit-action',
DEPENDENCY_ICON: '.question_dependency_wrapper .icon'
},
/**
* Retrieve the slot item from one of it's child Nodes.
*
* @method getSlotFromComponent
* @param slotcomponent {Node} The component Node.
* @return {Node|null} The Slot Node.
*/
getSlotFromComponent: function(slotcomponent) {
return Y.one(slotcomponent).ancestor(this.SELECTORS.SLOT, true);
},
/**
* Determines the slot ID for the provided slot.
*
* @method getId
* @param slot {Node} The slot to find an ID for.
* @return {Number|false} The ID of the slot in question or false if no ID was found.
*/
getId: function(slot) {
// We perform a simple substitution operation to get the ID.
var id = slot.get('id').replace(
this.CONSTANTS.SLOTIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
},
/**
* Determines the slot name for the provided slot.
*
* @method getName
* @param slot {Node} The slot to find a name for.
* @return {string|false} The name of the slot in question or false if no ID was found.
*/
getName: function(slot) {
var instance = slot.one(this.SELECTORS.INSTANCENAME);
if (instance) {
return instance.get('firstChild').get('data');
}
return null;
},
/**
* Determines the slot number for the provided slot.
*
* @method getNumber
* @param slot {Node} The slot to find the number for.
* @return {Number|false} The number of the slot in question or false if no number was found.
*/
getNumber: function(slot) {
if (!slot) {
return false;
}
// We perform a simple substitution operation to get the number.
var number = slot.one(this.SELECTORS.NUMBER).get('text').replace(
this.CONSTANTS.QUESTION, '');
// Attempt to validate the ID.
number = parseInt(number, 10);
if (typeof number === 'number' && isFinite(number)) {
return number;
}
return false;
},
/**
* Updates the slot number for the provided slot.
*
* @method setNumber
* @param slot {Node} The slot to update the number for.
* @return void
*/
setNumber: function(slot, number) {
var numbernode = slot.one(this.SELECTORS.NUMBER);
numbernode.setHTML('<span class="accesshide">' + this.CONSTANTS.QUESTION + '</span> ' + number);
},
/**
* Returns a list of all slot elements on the page.
*
* @method getSlots
* @return {node[]} An array containing slot nodes.
*/
getSlots: function() {
return Y.all(this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.SLOT);
},
/**
* Returns a list of all slot elements on the page that have numbers. Excudes description questions.
*
* @method getSlots
* @return {node[]} An array containing slot nodes.
*/
getNumberedSlots: function() {
var selector = this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL;
selector += ' ' + this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')';
return Y.all(selector);
},
/**
* Returns the previous slot to the given slot.
*
* @method getPrevious
* @param slot Slot node
* @return {node|false} The previous slot node or false.
*/
getPrevious: function(slot) {
return slot.previous(this.SELECTORS.SLOT);
},
/**
* Returns the previous numbered slot to the given slot.
*
* Ignores slots containing description question types.
*
* @method getPrevious
* @param slot Slot node
* @return {node|false} The previous slot node or false.
*/
getPreviousNumbered: function(slot) {
var previous = slot.previous(this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')');
if (previous) {
return previous;
}
var section = slot.ancestor('li.section').previous('li.section');
while (section) {
var questions = section.all(this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')');
if (questions.size() > 0) {
return questions.item(questions.size() - 1);
}
section = section.previous('li.section');
}
return false;
},
/**
* Reset the order of the numbers given to each slot.
*
* @method reorderSlots
* @return void
*/
reorderSlots: function() {
// Get list of slot nodes.
var slots = this.getSlots();
// Loop through slots incrementing the number each time.
slots.each(function(slot) {
if (!Y.Moodle.mod_quiz.util.page.getPageFromSlot(slot)) {
// Move the next page to the front.
var nextpage = slot.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE);
slot.swap(nextpage);
}
var previousSlot = this.getPreviousNumbered(slot),
previousslotnumber = 0;
if (slot.hasClass(this.CSS.QUESTIONTYPEDESCRIPTION)) {
return;
}
if (previousSlot) {
previousslotnumber = this.getNumber(previousSlot);
}
// Set slot number.
this.setNumber(slot, previousslotnumber + 1);
}, this);
},
/**
* Add class only-has-one-slot to those sections that need it.
*
* @method updateOneSlotSections
* @return void
*/
updateOneSlotSections: function() {
Y.all('.mod-quiz-edit-content ul.slots li.section').each(function(section) {
if (section.all(this.SELECTORS.SLOT).size() > 1) {
section.removeClass('only-has-one-slot');
} else {
section.addClass('only-has-one-slot');
}
}, this);
},
/**
* Remove a slot and related elements from the list of slots.
*
* @method remove
* @param slot Slot node
* @return void
*/
remove: function(slot) {
var page = Y.Moodle.mod_quiz.util.page.getPageFromSlot(slot);
slot.remove();
// Is the page empty.
if (!Y.Moodle.mod_quiz.util.page.isEmpty(page)) {
return;
}
// If so remove it. Including add menu and page break.
Y.Moodle.mod_quiz.util.page.remove(page);
},
/**
* Returns a list of all page break elements on the page.
*
* @method getPageBreaks
* @return {node[]} An array containing page break nodes.
*/
getPageBreaks: function() {
var selector = this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL;
selector += ' ' + this.SELECTORS.SLOT + this.SELECTORS.PAGEBREAK;
return Y.all(selector);
},
/**
* Retrieve the page break element item from the given slot.
*
* @method getPageBreak
* @param slot Slot node
* @return {Node|null} The Page Break Node.
*/
getPageBreak: function(slot) {
return Y.one(slot).one(this.SELECTORS.PAGEBREAK);
},
/**
* Add a page break and related elements to the list of slots.
*
* @method addPageBreak
* @param beforenode Int | Node | HTMLElement | String to add
* @return pagebreak PageBreak node
*/
addPageBreak: function(slot) {
var nodetext = M.mod_quiz.resource_toolbox.get('config').addpageiconhtml;
nodetext = nodetext.replace('%%SLOT%%', this.getNumber(slot));
var pagebreak = Y.Node.create(nodetext);
slot.one('div').insert(pagebreak, 'after');
return pagebreak;
},
/**
* Remove a pagebreak from the given slot.
*
* @method removePageBreak
* @param slot Slot node
* @return boolean
*/
removePageBreak: function(slot) {
var pagebreak = this.getPageBreak(slot);
if (!pagebreak) {
return false;
}
pagebreak.remove();
return true;
},
/**
* Reorder each pagebreak by iterating through each related slot.
*
* @method reorderPageBreaks
* @return void
*/
reorderPageBreaks: function() {
// Get list of slot nodes.
var slots = this.getSlots();
var slotnumber = 0;
// Loop through slots incrementing the number each time.
slots.each(function(slot, key) {
slotnumber++;
var pagebreak = this.getPageBreak(slot);
var nextitem = slot.next('li.activity');
if (!nextitem) {
// Last slot in a section. Should not have an icon.
return;
}
// No pagebreak and not last slot. Add one.
if (!pagebreak) {
pagebreak = this.addPageBreak(slot);
}
// Remove last page break if there is one.
if (pagebreak && key === slots.size() - 1) {
this.removePageBreak(slot);
}
// Get page break anchor element.
var pagebreaklink = pagebreak.get('childNodes').item(0);
// Get the correct title.
var action = '';
var iconname = '';
if (Y.Moodle.mod_quiz.util.page.isPage(nextitem)) {
action = 'removepagebreak';
iconname = 'e/remove_page_break';
} else {
action = 'addpagebreak';
iconname = 'e/insert_page_break';
}
// Update the link and image titles
pagebreaklink.set('title', M.util.get_string(action, 'quiz'));
pagebreaklink.setData('action', action);
// Update the image title.
var icon = pagebreaklink.one(this.SELECTORS.ICON);
icon.set('title', M.util.get_string(action, 'quiz'));
icon.set('alt', M.util.get_string(action, 'quiz'));
// Update the image src.
icon.set('src', M.util.image_url(iconname));
// Get anchor url parameters as an associative array.
var params = Y.QueryString.parse(pagebreaklink.get('href'));
// Update slot number.
params.slot = slotnumber;
// Create the new url.
var newurl = '';
for (var index in params) {
if (newurl.length) {
newurl += "&";
}
newurl += index + "=" + params[index];
}
// Update the anchor.
pagebreaklink.set('href', newurl);
}, this);
},
/**
* Update the dependency icons.
*
* @method updateAllDependencyIcons
* @return void
*/
updateAllDependencyIcons: function() {
// Get list of slot nodes.
var slots = this.getSlots(),
slotnumber = 0,
previousslot = null;
// Loop through slots incrementing the number each time.
slots.each(function(slot) {
slotnumber++;
if (slotnumber == 1 || previousslot.getData('canfinish') === '0') {
slot.one(this.SELECTORS.DEPENDENCY_WRAPPER).addClass(this.CSS.CANNOT_DEPEND);
} else {
slot.one(this.SELECTORS.DEPENDENCY_WRAPPER).removeClass(this.CSS.CANNOT_DEPEND);
}
this.updateDependencyIcon(slot, null);
previousslot = slot;
}, this);
},
/**
* Update the slot icon to indicate the new requiresprevious state.
*
* @method slot Slot node
* @method requiresprevious Whether this node now requires the previous one.
* @return void
*/
updateDependencyIcon: function(slot, requiresprevious) {
var link = slot.one(this.SELECTORS.DEPENDENCY_LINK);
var icon = slot.one(this.SELECTORS.DEPENDENCY_ICON);
var previousSlot = this.getPrevious(slot);
var a = {thisq: this.getNumber(slot)};
if (previousSlot) {
a.previousq = this.getNumber(previousSlot);
}
if (requiresprevious === null) {
requiresprevious = link.getData('action') === 'removedependency';
}
if (requiresprevious) {
link.set('title', M.util.get_string('questiondependencyremove', 'quiz', a));
link.setData('action', 'removedependency');
window.require(['core/templates'], function(Templates) {
Templates.renderPix('t/locked', 'core', M.util.get_string('questiondependsonprevious', 'quiz')).then(
function(html) {
icon.replace(html);
}
);
});
} else {
link.set('title', M.util.get_string('questiondependencyadd', 'quiz', a));
link.setData('action', 'adddependency');
window.require(['core/templates'], function(Templates) {
Templates.renderPix('t/unlocked', 'core', M.util.get_string('questiondependencyfree', 'quiz')).then(
function(html) {
icon.replace(html);
}
);
});
}
}
};
}, '@VERSION@', {"requires": ["node", "moodle-mod_quiz-util-base"]});
File diff suppressed because one or more lines are too long
@@ -0,0 +1,445 @@
YUI.add('moodle-mod_quiz-util-slot', function (Y, NAME) {
/**
* A collection of utility classes for use with slots.
*
* @module moodle-mod_quiz-util
* @submodule moodle-mod_quiz-util-slot
*/
Y.namespace('Moodle.mod_quiz.util.slot');
/**
* A collection of utility classes for use with slots.
*
* @class Moodle.mod_quiz.util.slot
* @static
*/
Y.Moodle.mod_quiz.util.slot = {
CSS: {
SLOT: 'slot',
QUESTIONTYPEDESCRIPTION: 'qtype_description',
CANNOT_DEPEND: 'question_dependency_cannot_depend'
},
CONSTANTS: {
SLOTIDPREFIX: 'slot-',
QUESTION: M.util.get_string('question', 'moodle')
},
SELECTORS: {
SLOT: 'li.slot',
INSTANCENAME: '.instancename',
NUMBER: 'span.slotnumber',
PAGECONTENT: 'div#page-content',
PAGEBREAK: 'span.page_split_join_wrapper',
ICON: '.icon',
QUESTIONTYPEDESCRIPTION: '.qtype_description',
SECTIONUL: 'ul.section',
DEPENDENCY_WRAPPER: '.question_dependency_wrapper',
DEPENDENCY_LINK: '.question_dependency_wrapper .cm-edit-action',
DEPENDENCY_ICON: '.question_dependency_wrapper .icon'
},
/**
* Retrieve the slot item from one of it's child Nodes.
*
* @method getSlotFromComponent
* @param slotcomponent {Node} The component Node.
* @return {Node|null} The Slot Node.
*/
getSlotFromComponent: function(slotcomponent) {
return Y.one(slotcomponent).ancestor(this.SELECTORS.SLOT, true);
},
/**
* Determines the slot ID for the provided slot.
*
* @method getId
* @param slot {Node} The slot to find an ID for.
* @return {Number|false} The ID of the slot in question or false if no ID was found.
*/
getId: function(slot) {
// We perform a simple substitution operation to get the ID.
var id = slot.get('id').replace(
this.CONSTANTS.SLOTIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
},
/**
* Determines the slot name for the provided slot.
*
* @method getName
* @param slot {Node} The slot to find a name for.
* @return {string|false} The name of the slot in question or false if no ID was found.
*/
getName: function(slot) {
var instance = slot.one(this.SELECTORS.INSTANCENAME);
if (instance) {
return instance.get('firstChild').get('data');
}
return null;
},
/**
* Determines the slot number for the provided slot.
*
* @method getNumber
* @param slot {Node} The slot to find the number for.
* @return {Number|false} The number of the slot in question or false if no number was found.
*/
getNumber: function(slot) {
if (!slot) {
return false;
}
// We perform a simple substitution operation to get the number.
var number = slot.one(this.SELECTORS.NUMBER).get('text').replace(
this.CONSTANTS.QUESTION, '');
// Attempt to validate the ID.
number = parseInt(number, 10);
if (typeof number === 'number' && isFinite(number)) {
return number;
}
return false;
},
/**
* Updates the slot number for the provided slot.
*
* @method setNumber
* @param slot {Node} The slot to update the number for.
* @return void
*/
setNumber: function(slot, number) {
var numbernode = slot.one(this.SELECTORS.NUMBER);
numbernode.setHTML('<span class="accesshide">' + this.CONSTANTS.QUESTION + '</span> ' + number);
},
/**
* Returns a list of all slot elements on the page.
*
* @method getSlots
* @return {node[]} An array containing slot nodes.
*/
getSlots: function() {
return Y.all(this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.SLOT);
},
/**
* Returns a list of all slot elements on the page that have numbers. Excudes description questions.
*
* @method getSlots
* @return {node[]} An array containing slot nodes.
*/
getNumberedSlots: function() {
var selector = this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL;
selector += ' ' + this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')';
return Y.all(selector);
},
/**
* Returns the previous slot to the given slot.
*
* @method getPrevious
* @param slot Slot node
* @return {node|false} The previous slot node or false.
*/
getPrevious: function(slot) {
return slot.previous(this.SELECTORS.SLOT);
},
/**
* Returns the previous numbered slot to the given slot.
*
* Ignores slots containing description question types.
*
* @method getPrevious
* @param slot Slot node
* @return {node|false} The previous slot node or false.
*/
getPreviousNumbered: function(slot) {
var previous = slot.previous(this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')');
if (previous) {
return previous;
}
var section = slot.ancestor('li.section').previous('li.section');
while (section) {
var questions = section.all(this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')');
if (questions.size() > 0) {
return questions.item(questions.size() - 1);
}
section = section.previous('li.section');
}
return false;
},
/**
* Reset the order of the numbers given to each slot.
*
* @method reorderSlots
* @return void
*/
reorderSlots: function() {
// Get list of slot nodes.
var slots = this.getSlots();
// Loop through slots incrementing the number each time.
slots.each(function(slot) {
if (!Y.Moodle.mod_quiz.util.page.getPageFromSlot(slot)) {
// Move the next page to the front.
var nextpage = slot.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE);
slot.swap(nextpage);
}
var previousSlot = this.getPreviousNumbered(slot),
previousslotnumber = 0;
if (slot.hasClass(this.CSS.QUESTIONTYPEDESCRIPTION)) {
return;
}
if (previousSlot) {
previousslotnumber = this.getNumber(previousSlot);
}
// Set slot number.
this.setNumber(slot, previousslotnumber + 1);
}, this);
},
/**
* Add class only-has-one-slot to those sections that need it.
*
* @method updateOneSlotSections
* @return void
*/
updateOneSlotSections: function() {
Y.all('.mod-quiz-edit-content ul.slots li.section').each(function(section) {
if (section.all(this.SELECTORS.SLOT).size() > 1) {
section.removeClass('only-has-one-slot');
} else {
section.addClass('only-has-one-slot');
}
}, this);
},
/**
* Remove a slot and related elements from the list of slots.
*
* @method remove
* @param slot Slot node
* @return void
*/
remove: function(slot) {
var page = Y.Moodle.mod_quiz.util.page.getPageFromSlot(slot);
slot.remove();
// Is the page empty.
if (!Y.Moodle.mod_quiz.util.page.isEmpty(page)) {
return;
}
// If so remove it. Including add menu and page break.
Y.Moodle.mod_quiz.util.page.remove(page);
},
/**
* Returns a list of all page break elements on the page.
*
* @method getPageBreaks
* @return {node[]} An array containing page break nodes.
*/
getPageBreaks: function() {
var selector = this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL;
selector += ' ' + this.SELECTORS.SLOT + this.SELECTORS.PAGEBREAK;
return Y.all(selector);
},
/**
* Retrieve the page break element item from the given slot.
*
* @method getPageBreak
* @param slot Slot node
* @return {Node|null} The Page Break Node.
*/
getPageBreak: function(slot) {
return Y.one(slot).one(this.SELECTORS.PAGEBREAK);
},
/**
* Add a page break and related elements to the list of slots.
*
* @method addPageBreak
* @param beforenode Int | Node | HTMLElement | String to add
* @return pagebreak PageBreak node
*/
addPageBreak: function(slot) {
var nodetext = M.mod_quiz.resource_toolbox.get('config').addpageiconhtml;
nodetext = nodetext.replace('%%SLOT%%', this.getNumber(slot));
var pagebreak = Y.Node.create(nodetext);
slot.one('div').insert(pagebreak, 'after');
return pagebreak;
},
/**
* Remove a pagebreak from the given slot.
*
* @method removePageBreak
* @param slot Slot node
* @return boolean
*/
removePageBreak: function(slot) {
var pagebreak = this.getPageBreak(slot);
if (!pagebreak) {
return false;
}
pagebreak.remove();
return true;
},
/**
* Reorder each pagebreak by iterating through each related slot.
*
* @method reorderPageBreaks
* @return void
*/
reorderPageBreaks: function() {
// Get list of slot nodes.
var slots = this.getSlots();
var slotnumber = 0;
// Loop through slots incrementing the number each time.
slots.each(function(slot, key) {
slotnumber++;
var pagebreak = this.getPageBreak(slot);
var nextitem = slot.next('li.activity');
if (!nextitem) {
// Last slot in a section. Should not have an icon.
return;
}
// No pagebreak and not last slot. Add one.
if (!pagebreak) {
pagebreak = this.addPageBreak(slot);
}
// Remove last page break if there is one.
if (pagebreak && key === slots.size() - 1) {
this.removePageBreak(slot);
}
// Get page break anchor element.
var pagebreaklink = pagebreak.get('childNodes').item(0);
// Get the correct title.
var action = '';
var iconname = '';
if (Y.Moodle.mod_quiz.util.page.isPage(nextitem)) {
action = 'removepagebreak';
iconname = 'e/remove_page_break';
} else {
action = 'addpagebreak';
iconname = 'e/insert_page_break';
}
// Update the link and image titles
pagebreaklink.set('title', M.util.get_string(action, 'quiz'));
pagebreaklink.setData('action', action);
// Update the image title.
var icon = pagebreaklink.one(this.SELECTORS.ICON);
icon.set('title', M.util.get_string(action, 'quiz'));
icon.set('alt', M.util.get_string(action, 'quiz'));
// Update the image src.
icon.set('src', M.util.image_url(iconname));
// Get anchor url parameters as an associative array.
var params = Y.QueryString.parse(pagebreaklink.get('href'));
// Update slot number.
params.slot = slotnumber;
// Create the new url.
var newurl = '';
for (var index in params) {
if (newurl.length) {
newurl += "&";
}
newurl += index + "=" + params[index];
}
// Update the anchor.
pagebreaklink.set('href', newurl);
}, this);
},
/**
* Update the dependency icons.
*
* @method updateAllDependencyIcons
* @return void
*/
updateAllDependencyIcons: function() {
// Get list of slot nodes.
var slots = this.getSlots(),
slotnumber = 0,
previousslot = null;
// Loop through slots incrementing the number each time.
slots.each(function(slot) {
slotnumber++;
if (slotnumber == 1 || previousslot.getData('canfinish') === '0') {
slot.one(this.SELECTORS.DEPENDENCY_WRAPPER).addClass(this.CSS.CANNOT_DEPEND);
} else {
slot.one(this.SELECTORS.DEPENDENCY_WRAPPER).removeClass(this.CSS.CANNOT_DEPEND);
}
this.updateDependencyIcon(slot, null);
previousslot = slot;
}, this);
},
/**
* Update the slot icon to indicate the new requiresprevious state.
*
* @method slot Slot node
* @method requiresprevious Whether this node now requires the previous one.
* @return void
*/
updateDependencyIcon: function(slot, requiresprevious) {
var link = slot.one(this.SELECTORS.DEPENDENCY_LINK);
var icon = slot.one(this.SELECTORS.DEPENDENCY_ICON);
var previousSlot = this.getPrevious(slot);
var a = {thisq: this.getNumber(slot)};
if (previousSlot) {
a.previousq = this.getNumber(previousSlot);
}
if (requiresprevious === null) {
requiresprevious = link.getData('action') === 'removedependency';
}
if (requiresprevious) {
link.set('title', M.util.get_string('questiondependencyremove', 'quiz', a));
link.setData('action', 'removedependency');
window.require(['core/templates'], function(Templates) {
Templates.renderPix('t/locked', 'core', M.util.get_string('questiondependsonprevious', 'quiz')).then(
function(html) {
icon.replace(html);
}
);
});
} else {
link.set('title', M.util.get_string('questiondependencyadd', 'quiz', a));
link.setData('action', 'adddependency');
window.require(['core/templates'], function(Templates) {
Templates.renderPix('t/unlocked', 'core', M.util.get_string('questiondependencyfree', 'quiz')).then(
function(html) {
icon.replace(html);
}
);
});
}
}
};
}, '@VERSION@', {"requires": ["node", "moodle-mod_quiz-util-base"]});