MDL-31096 Convert course resource/section javascript to YUI3 Module

AMOS BEGIN
  MOV [hideweekfromothers,moodle],[hidefromothers,format_week]
  MOV [showweekfromothers,moodle],[showfromothers,format_week]
  MOV [hidetopicfromothers,moodle],[hidefromothers,format_topics]
  MOV [showtopicfromothers,moodle],[showfromothers,format_topics]
AMOS END
This commit is contained in:
Andrew Robert Nicols
2012-04-23 21:28:39 +01:00
parent cd6081f558
commit ebaa29d107
13 changed files with 830 additions and 1543 deletions
+6 -6
View File
@@ -55,8 +55,8 @@ $strgroupmy = get_string('groupmy');
$editing = $PAGE->user_is_editing();
if ($editing) {
$strtopichide = get_string('hidetopicfromothers');
$strtopicshow = get_string('showtopicfromothers');
$strtopichide = get_string('hidefromothers', 'format_topics');
$strtopicshow = get_string('showfromothers', 'format_topics');
$strmarkthistopic = get_string('markthistopic');
$strmarkedthistopic = get_string('markedthistopic');
$strmoveup = get_string('moveup');
@@ -189,16 +189,16 @@ while ($section <= $course->numsections) {
if ($PAGE->user_is_editing() && has_capability('moodle/course:update', get_context_instance(CONTEXT_COURSE, $course->id))) {
if ($course->marker == $section) { // Show the "light globe" on/off
echo '<a href="view.php?id='.$course->id.'&amp;marker=0&amp;sesskey='.sesskey().'#section-'.$section.'" title="'.$strmarkedthistopic.'">'.'<img src="'.$OUTPUT->pix_url('i/marked') . '" alt="'.$strmarkedthistopic.'" class="icon"/></a><br />';
echo '<a href="view.php?id='.$course->id.'&amp;marker=0&amp;sesskey='.sesskey().'#section-'.$section.'" title="'.$strmarkedthistopic.'" class="editing_highlight">'.'<img src="'.$OUTPUT->pix_url('i/marked') . '" alt="'.$strmarkedthistopic.'" class="icon"/></a><br />';
} else {
echo '<a href="view.php?id='.$course->id.'&amp;marker='.$section.'&amp;sesskey='.sesskey().'#section-'.$section.'" title="'.$strmarkthistopic.'">'.'<img src="'.$OUTPUT->pix_url('i/marker') . '" alt="'.$strmarkthistopic.'" class="icon"/></a><br />';
echo '<a href="view.php?id='.$course->id.'&amp;marker='.$section.'&amp;sesskey='.sesskey().'#section-'.$section.'" title="'.$strmarkthistopic.'" class="editing_highlight">'.'<img src="'.$OUTPUT->pix_url('i/marker') . '" alt="'.$strmarkthistopic.'" class="icon"/></a><br />';
}
if ($thissection->visible) { // Show the hide/show eye
echo '<a href="view.php?id='.$course->id.'&amp;hide='.$section.'&amp;sesskey='.sesskey().'#section-'.$section.'" title="'.$strtopichide.'">'.
echo '<a href="view.php?id='.$course->id.'&amp;hide='.$section.'&amp;sesskey='.sesskey().'#section-'.$section.'" title="'.$strtopichide.'" class="editing_showhide">'.
'<img src="'.$OUTPUT->pix_url('i/hide') . '" class="icon hide" alt="'.$strtopichide.'" /></a><br />';
} else {
echo '<a href="view.php?id='.$course->id.'&amp;show='.$section.'&amp;sesskey='.sesskey().'#section-'.$section.'" title="'.$strtopicshow.'">'.
echo '<a href="view.php?id='.$course->id.'&amp;show='.$section.'&amp;sesskey='.sesskey().'#section-'.$section.'" title="'.$strtopicshow.'" class="editing_showhide">'.
'<img src="'.$OUTPUT->pix_url('i/show') . '" class="icon hide" alt="'.$strtopicshow.'" /></a><br />';
}
if ($section > 1) { // Add a arrow to move section up
@@ -28,3 +28,5 @@ $string['pluginname'] = 'Topics format';
$string['section0name'] = 'General';
$string['page-course-view-topics'] = 'Any course main page in topics format';
$string['page-course-view-topics-x'] = 'Any course page in topics format';
$string['hidefromothers'] = 'Hide topic';
$string['showfromothers'] = 'Show topic';
+4 -4
View File
@@ -47,8 +47,8 @@ defined('MOODLE_INTERNAL') || die();
$editing = $PAGE->user_is_editing();
if ($editing) {
$strweekhide = get_string('hideweekfromothers');
$strweekshow = get_string('showweekfromothers');
$strweekhide = get_string('hidefromothers', 'format_topics');
$strweekshow = get_string('showfromothers', 'format_topics');
$strmoveup = get_string('moveup');
$strmovedown = get_string('movedown');
}
@@ -194,10 +194,10 @@ defined('MOODLE_INTERNAL') || die();
if ($PAGE->user_is_editing() && has_capability('moodle/course:update', get_context_instance(CONTEXT_COURSE, $course->id))) {
if ($thissection->visible) { // Show the hide/show eye
echo '<a href="view.php?id='.$course->id.'&amp;hide='.$section.'&amp;sesskey='.sesskey().'#section-'.$section.'" title="'.$strweekhide.'">'.
echo '<a href="view.php?id='.$course->id.'&amp;hide='.$section.'&amp;sesskey='.sesskey().'#section-'.$section.'" title="'.$strweekhide.'" class="editing_showhide">'.
'<img src="'.$OUTPUT->pix_url('i/hide') . '" class="icon hide" alt="'.$strweekhide.'" /></a><br />';
} else {
echo '<a href="view.php?id='.$course->id.'&amp;show='.$section.'&amp;sesskey='.sesskey().'#section-'.$section.'" title="'.$strweekshow.'">'.
echo '<a href="view.php?id='.$course->id.'&amp;show='.$section.'&amp;sesskey='.sesskey().'#section-'.$section.'" title="'.$strweekshow.'" class="editing_showhide">'.
'<img src="'.$OUTPUT->pix_url('i/show') . '" class="icon hide" alt="'.$strweekshow.'" /></a><br />';
}
if ($section > 1) { // Add a arrow to move section up
@@ -28,3 +28,5 @@ $string['pluginname'] = 'Weekly format';
$string['section0name'] = 'General';
$string['page-course-view-weeks'] = 'Any course main page in weeks format';
$string['page-course-view-weeks-x'] = 'Any course page in weeks format';
$string['hidefromothers'] = 'Hide week';
$string['showfromothers'] = 'Show week';
+112
View File
@@ -1355,10 +1355,16 @@ function course_set_marker($courseid, $marker) {
/**
* For a given course section, marks it visible or hidden,
* and does the same for every activity in that section
*
* @param int $courseid course id
* @param int $sectionnumber The section number to adjust
* @param int $visibility The new visibility
* @return array A list of resources which were hidden in the section
*/
function set_section_visible($courseid, $sectionnumber, $visibility) {
global $DB;
$resourcestotoggle = array();
if ($section = $DB->get_record("course_sections", array("course"=>$courseid, "section"=>$sectionnumber))) {
$DB->set_field("course_sections", "visible", "$visibility", array("id"=>$section->id));
if (!empty($section->sequence)) {
@@ -1368,7 +1374,19 @@ function set_section_visible($courseid, $sectionnumber, $visibility) {
}
}
rebuild_course_cache($courseid);
// Determine which modules are visible for AJAX update
if (!empty($modules)) {
list($insql, $params) = $DB->get_in_or_equal($modules);
$select = 'id ' . $insql . ' AND visible = ?';
array_push($params, $visibility);
if (!$visibility) {
$select .= ' AND visibleold = 1';
}
$resourcestotoggle = $DB->get_fieldset_select('course_modules', 'id', $select, $params);
}
}
return $resourcestotoggle;
}
/**
@@ -4386,3 +4404,97 @@ function course_page_type_list($pagetype, $parentcontext, $currentcontext) {
);
}
}
/**
* Include the relevant javascript and language strings for the resource
* toolbox YUI module
*
* @param integer $id The ID of the course being applied to
* @param array $modules An array containing the names of the modules in
* use on the page
* @param object $config An object containing configuration parameters for ajax modules including:
* * resourceurl The URL to post changes to for resource changes
* * sectionurl The URL to post changes to for section changes
* * pageparams Additional parameters to pass through in the post
* @return void
*/
function include_course_ajax($course, $modules = array(), $config = null) {
global $PAGE, $CFG, $USER;
// Ensure that ajax should be included
$courseformatajaxsupport = course_format_ajax_support($course->format);
if (!$CFG->enablecourseajax
|| !$PAGE->theme->enablecourseajax
|| !$CFG->enableajax
|| empty($USER->editing)
|| !$PAGE->user_is_editing()
|| ($course->id != SITEID && !$courseformatajaxsupport->capable)) {
return;
}
if (!$config) {
$config = new stdClass();
}
// The URL to use for resource changes
if (!isset($config->resourceurl)) {
$config->resourceurl = '/course/rest.php';
}
// The URL to use for section changes
if (!isset($config->sectionurl)) {
$config->sectionurl = '/course/rest.php';
}
// Any additional parameters which need to be included on page submission
if (!isset($config->pageparams)) {
$config->pageparams = array();
}
// Include toolboxes
$PAGE->requires->yui_module('moodle-course-toolboxes',
'M.course.init_resource_toolbox',
array(array(
'courseid' => $course->id,
'ajaxurl' => $config->resourceurl,
'config' => $config,
))
);
$PAGE->requires->yui_module('moodle-course-toolboxes',
'M.course.init_section_toolbox',
array(array(
'courseid' => $course->id,
'format' => $course->format,
'ajaxurl' => $config->sectionurl,
'config' => $config,
))
);
// Require various strings for the command toolbox
$PAGE->requires->strings_for_js(array(
'moveleft',
'deletechecktype',
'deletechecktypename',
'show',
'hide',
'groupsnone',
'groupsvisible',
'groupsseparate',
'clicktochangeinbrackets',
'markthistopic',
'markedthistopic',
), 'moodle');
// Include format-specific strings
if ($course->id != SITEID) {
$PAGE->requires->strings_for_js(array(
'showfromothers',
'hidefromothers',
), 'format_' . $course->format);
}
// For confirming resource deletion we need the name of the module in question
foreach ($modules as $module => $modname) {
$PAGE->requires->string_for_js('pluginname', $module);
}
}
+5 -10
View File
@@ -87,7 +87,8 @@ switch($requestmethod) {
switch ($field) {
case 'visible':
set_section_visible($course->id, $id, $value);
$resourcestotoggle = set_section_visible($course->id, $id, $value);
echo json_encode(array('resourcestotoggle' => $resourcestotoggle));
break;
case 'move':
@@ -115,20 +116,14 @@ switch($requestmethod) {
set_coursemodule_groupmode($cm->id, $value);
break;
case 'indentleft':
case 'indent':
require_capability('moodle/course:manageactivities', $modcontext);
if ($cm->indent > 0) {
$cm->indent--;
$cm->indent = $value;
if ($cm->indent >= 0) {
$DB->update_record('course_modules', $cm);
}
break;
case 'indentright':
require_capability('moodle/course:manageactivities', $modcontext);
$cm->indent++;
$DB->update_record('course_modules', $cm);
break;
case 'move':
require_capability('moodle/course:manageactivities', $modcontext);
if (!$section = $DB->get_record('course_sections', array('course'=>$course->id, 'section'=>$sectionid))) {
+2 -37
View File
@@ -162,32 +162,6 @@
redirect($CFG->wwwroot .'/');
}
// AJAX-capable course format?
$useajax = false;
$formatajax = course_format_ajax_support($course->format);
if (!empty($CFG->enablecourseajax)
and $formatajax->capable
and !empty($USER->editing)
and ajaxenabled($formatajax->testedbrowsers)
and $PAGE->theme->enablecourseajax
and has_capability('moodle/course:manageactivities', $context)) {
$PAGE->requires->yui2_lib('dragdrop');
$PAGE->requires->yui2_lib('connection');
$PAGE->requires->yui2_lib('selector');
$PAGE->requires->js('/lib/ajax/block_classes.js', true);
$PAGE->requires->js('/lib/ajax/section_classes.js', true);
// Okay, global variable alert. VERY UGLY. We need to create
// this object here before the <blockname>_print_block()
// function is called, since that function needs to set some
// stuff in the javascriptportal object.
$COURSE->javascriptportal = new jsportal();
$useajax = true;
}
$CFG->blocksdrag = $useajax; // this will add a new class to the header so we can style differently
$completion = new completion_info($course);
if ($completion->is_enabled() && ajaxenabled()) {
$PAGE->requires->string_for_js('completion-title-manual-y', 'completion');
@@ -257,16 +231,7 @@
echo html_writer::end_tag('div');
// Use AJAX?
if ($useajax && has_capability('moodle/course:manageactivities', $context)) {
// At the bottom because we want to process sections and activities
// after the relevant html has been generated. We're forced to do this
// because of the way in which lib/ajax/ajaxcourse.js is written.
echo html_writer::script(false, new moodle_url('/lib/ajax/ajaxcourse.js'));
$COURSE->javascriptportal->print_javascript($course->id);
}
// Include the command toolbox YUI module
include_course_ajax($course, $modnamesused);
echo $OUTPUT->footer();
+691
View File
@@ -0,0 +1,691 @@
YUI.add('moodle-course-toolboxes', function(Y) {
WAITICON = {'pix':"i/loading_small",'component':'moodle'};
// The CSS selectors we use
var CSS = {
ACTIVITYLI : 'li.activity',
COMMANDSPAN : 'span.commands',
DELETE : 'a.editing_delete',
DIMCLASS : 'dimmed',
DIMMEDLABELCLASS : 'dimmed_text',
EDITTITLECLASS : 'edittitle',
GENERICICONCLASS : 'iconsmall',
GROUPSNONE : 'a.editing_groupsnone',
GROUPSSEPARATE : 'a.editing_groupsseparate',
GROUPSVISIBLE : 'a.editing_groupsvisible',
HASLABEL : 'label',
HIDE : 'a.editing_hide',
HIGHLIGHT : 'a.editing_highlight',
INSTANCENAME : 'span.instancename',
LIGHTBOX : 'lightbox',
MODINDENTCOUNT : 'mod-indent-',
MODINDENTDIV : 'div.mod-indent',
MODULEIDPREFIX : 'module-',
MOVELEFT : 'a.editing_moveleft',
MOVELEFTCLASS : 'editing_moveleft',
MOVERIGHT : 'a.editing_moveright',
PAGECONTENT : 'div#page-content',
RIGHTDIV : 'div.right',
SECTIONHIDDENCLASS : 'hidden',
SECTIONIDPREFIX : 'section-',
SECTIONLI : 'li.section',
SHOW : 'a.editing_show',
SHOWHIDE : 'a.editing_showhide'
};
/**
* The toolbox classes
*
* TOOLBOX is a generic class which should never be directly instantiated
* RESOURCETOOLBOX is a class extending TOOLBOX containing code specific to resources
* SECTIONTOOLBOX is a class extending TOOLBOX containing code specific to sections
*/
var TOOLBOX = function() {
TOOLBOX.superclass.constructor.apply(this, arguments);
}
Y.extend(TOOLBOX, Y.Base, {
/**
* Replace the button click at the selector with the specified
* callback
*
* @param toolboxtarget The selector of the working area
* @param selector The 'button' to replace
* @param callback The callback to apply
* @param cursor An optional cursor style to apply
*/
replace_button : function(toolboxtarget, selector, callback, cursor) {
if (!cursor) {
// Set the default cursor type to pointer to match the
// anchor
cursor = 'pointer';
}
var button = Y.one(toolboxtarget).all(selector)
.removeAttribute('href')
.setStyle('cursor', cursor);
// on isn't chainable and will return an event
button.on('click', callback, this);
return button;
},
/**
* Toggle the visibility and availability for the specified
* resource show/hide button
*/
toggle_hide_resource_ui : function(button) {
var element = button.ancestor(CSS.ACTIVITYLI);
var hideicon = button.one('img');
var dimarea;
var toggle_class;
if (this.is_label(element)) {
toggle_class = CSS.DIMMEDLABELCLASS;
dimarea = element.one(CSS.MODINDENTDIV + ' div');
} else {
toggle_class = CSS.DIMCLASS;
dimarea = element.one('a');
}
var status = '';
var value;
if (dimarea.hasClass(toggle_class)) {
status = 'hide';
value = 1;
} else {
status = 'show';
value = 0;
}
// Change the UI
dimarea.toggleClass(toggle_class);
var newstring = M.util.get_string(status, 'moodle');
hideicon.setAttrs({
'alt' : newstring,
'title' : newstring,
'src' : M.util.image_url('t/' + status)
});
button.set('title', newstring);
button.set('className', 'editing_'+status);
return value;
},
/**
* Send a request using the REST API
*
* @param data The data to submit
* @param loadingiconat (optional) Show the loading icon spinner at the specified location (replaces text)
* @param lightbox (optional) A lightbox which may contain a section loader
* @param optionalconfig (optional) Any additional configuration to submit
* @return response responseText field from responce
*/
send_request : function(data, loadingiconat, lightbox, optionalconfig) {
// Default data structure
if (!data) {
data = {};
}
// Handle any variables which we must pass back through to
var pageparams = this.get('config').pageparams;
for (varname in pageparams) {
data[varname] = pageparams[varname];
}
// Make a note of the icon for displaying the loadingicon spinner
var originalicon;
if (loadingiconat) {
originalicon = loadingiconat.getAttribute('src');
}
data.sesskey = M.cfg.sesskey;
data.courseId = this.get('courseid');
var uri = M.cfg.wwwroot + this.get('ajaxurl');
// Define the configuration to send with the request
var responsetext = [];
var config = {
method: 'POST',
data: data,
on: {
success: function(tid, response) {
try {
responsetext = Y.JSON.parse(response.responseText);
if (responsetext.error) {
new M.core.ajaxException(responsetext);
}
} catch (e) {}
if (originalicon) {
// Replace the spinner with the original icon We use a pause to give
// positive feedback that something is happening
window.setTimeout(function(e) {
loadingiconat.setAttribute('src', originalicon);
}, 250);
}
if (lightbox) {
window.setTimeout(function(e) {
lightbox.hide();
}, 250);
}
},
failure : function(tid, response) {
if (originalicon) {
loadingiconat.setAttribute('src', originalicon);
}
if (lightbox) {
lightbox.hide();
}
new M.core.ajaxException(response);
}
},
context: this,
sync: true
}
// Apply optional config
if (optionalconfig) {
for (varname in optionalconfig) {
config[varname] = optionalconfig[varname];
}
}
if (loadingiconat) {
loadingiconat.removeAttribute('innerHTML');
loadingiconat.set('src', M.util.image_url(WAITICON.pix, WAITICON.component));
}
// Send the request
Y.io(uri, config);
return responsetext;
},
is_label : function(target) {
return target.hasClass(CSS.HASLABEL);
},
/**
* Return the module ID for the specified element
*
* @param element The <li> element to determine a module-id number for
* @return string The module ID
*/
get_element_id : function(element) {
return element.get('id').replace(CSS.MODULEIDPREFIX, '');
},
/**
* Return the module ID for the specified element
*
* @param element The <li> element to determine a module-id number for
* @return string The module ID
*/
get_section_id : function(section) {
return section.get('id').replace(CSS.SECTIONIDPREFIX, '');
},
add_lightbox: function(node) {
node.setStyle('position', 'relative');
var waiticon = Y.Node.create('<img />')
.setAttrs({
'src' : M.util.image_url(WAITICON.pix, WAITICON.component),
'title' : M.str.moodle.move,
'hspace' : '3'
})
.setStyles({
'position' : 'relative',
'top' : '50%'
});
var lightbox = Y.Node.create('<div></div>')
.setStyles({
'opacity' : '.75',
'position' : 'absolute',
'width' : '100%',
'height' : '100%',
'top' : 0,
'left' : 0,
'backgroundColor' : 'white',
'text-align' : 'center'
})
.setAttribute('class', 'lightbox')
.hide();
lightbox.appendChild(waiticon);
node.append(lightbox);
return lightbox;
}
},
{
NAME : 'course-toolbox',
ATTRS : {
// The ID of the current course
courseid : {
'value' : 0
},
ajaxurl : {
'value' : 0
},
config : {
'value' : 0
}
}
}
);
var RESOURCETOOLBOX = function() {
RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
}
Y.extend(RESOURCETOOLBOX, TOOLBOX, {
// Variables
GROUPS_NONE : 0,
GROUPS_SEPARATE : 1,
GROUPS_VISIBLE : 2,
/**
* Initialize the resource toolbox
*
* Updates all span.commands with relevant handlers and other required changes
*/
initializer : function(config) {
this.setup_for_resource();
M.course.coursebase.register_module(this);
},
/**
* Update any span.commands within the scope of the specified
* selector with AJAX equivelants
*
* @param baseselector The selector to limit scope to
* @return void
*/
setup_for_resource : function(baseselector) {
if (!baseselector) {
var baseselector = CSS.PAGECONTENT;
}
Y.all(baseselector).each(this._setup_for_resource, this);
},
_setup_for_resource : function(toolboxtarget) {
// Move left and right
this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.MOVELEFT, this.move_left);
this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.MOVERIGHT, this.move_right);
// Delete
this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.DELETE, this.delete_resource);
// Show/Hide
var showhide = this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.HIDE, this.toggle_hide_resource);
var shown = this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.SHOW, this.toggle_hide_resource);
showhide = showhide.concat(shown);
showhide.each(function(node) {
var section = node.ancestor(CSS.SECTIONLI);
if (section && section.hasClass(CSS.SECTIONHIDDENCLASS)) {
node.setStyle('cursor', 'auto');
}
});
// Change Group Mode
var groups;
groups = this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.GROUPSNONE, this.toggle_groupmode);
groups.setAttribute('groupmode', this.GROUPS_NONE);
groups = this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.GROUPSSEPARATE, this.toggle_groupmode);
groups.setAttribute('groupmode', this.GROUPS_SEPARATE);
groups = this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.GROUPSVISIBLE, this.toggle_groupmode);
groups.setAttribute('groupmode', this.GROUPS_VISIBLE);
},
move_left : function(e) {
this.move_leftright(e, -1, CSS.MOVELEFT);
},
move_right : function(e) {
this.move_leftright(e, 1, CSS.MOVERIGHT);
},
move_leftright : function(e, direction, buttonselector) {
// Get the element we're working on
var element = e.target.ancestor(CSS.ACTIVITYLI);
// And we need to determine the current and new indent level
var indentdiv = element.one(CSS.MODINDENTDIV);
var indent = indentdiv.getAttribute('class').match(/mod-indent-(\d{1,})/);
if (indent) {
var oldindent = parseInt(indent[1]);
var newindent = Math.max(0, (oldindent + parseInt(direction)));
indentdiv.removeClass(indent[0]);
} else {
var oldindent = 0;
var newindent = 1;
}
// Perform the move
indentdiv.addClass(CSS.MODINDENTCOUNT + newindent);
var data = {
'class' : 'resource',
'field' : 'indent',
'value' : newindent,
'id' : this.get_element_id(element)
};
var editbutton = element.one(buttonselector + ' img');
this.send_request(data, editbutton);
// Handle removal/addition of the moveleft button
if (newindent == 0) {
window.setTimeout(function(e) {
element.one(CSS.MOVELEFT).remove();
}, 250);
} else if (newindent == 1 && oldindent == 0) {
this.add_moveleft(element);
}
},
delete_resource : function(e) {
// Get the element we're working on
var element = e.target.ancestor(CSS.ACTIVITYLI);
var confirmstring = '';
if (this.is_label(element)) {
// Labels are slightly different to other activities
var plugindata = {
type : M.util.get_string('pluginname', 'label')
}
confirmstring = M.util.get_string('deletechecktype', 'moodle', plugindata)
} else {
var plugindata = {
type : M.util.get_string('pluginname', element.getAttribute('class').match(/modtype_([^\s]*)/)[1]),
name : element.one(CSS.INSTANCENAME).get('firstChild').get('data')
}
confirmstring = M.util.get_string('deletechecktypename', 'moodle', plugindata);
}
// Confirm element removal
if (!confirm(confirmstring)) {
return false;
}
// Actually remove the element
element.remove();
var data = {
'class' : 'resource',
'action' : 'DELETE',
'id' : this.get_element_id(element)
};
this.send_request(data);
},
toggle_hide_resource : function(e) {
// Return early if the current section is hidden
var section = e.target.ancestor(CSS.SECTIONLI);
if (section && section.hasClass(CSS.SECTIONHIDDENCLASS)) {
return;
}
// Get the element we're working on
var element = e.target.ancestor(CSS.ACTIVITYLI);
var button = e.target.ancestor('a', true);
var value = this.toggle_hide_resource_ui(button);
// Send the request
var data = {
'class' : 'resource',
'field' : 'visible',
'value' : value,
'id' : this.get_element_id(element)
};
this.send_request(data, button.one('img'));
},
toggle_groupmode : function(e) {
// Get the element we're working on
var element = e.target.ancestor(CSS.ACTIVITYLI);
var button = e.target.ancestor('a', true);
var icon = button.one('img');
// Current Mode
var groupmode = button.getAttribute('groupmode');
groupmode++;
if (groupmode > 2) {
groupmode = 0;
}
button.setAttribute('groupmode', groupmode);
var newtitle = '';
var iconsrc = '';
switch (groupmode) {
case this.GROUPS_NONE:
newtitle = 'groupsnone';
iconsrc = M.util.image_url('t/groupn');
break;
case this.GROUPS_SEPARATE:
newtitle = 'groupsseparate';
iconsrc = M.util.image_url('t/groups');
break;
case this.GROUPS_VISIBLE:
newtitle = 'groupsvisible';
iconsrc = M.util.image_url('t/groupv');
break;
}
newtitle = M.util.get_string('clicktochangeinbrackets', 'moodle',
M.util.get_string(newtitle, 'moodle'));
// Change the UI
icon.setAttrs({
'alt' : newtitle,
'title' : newtitle,
'src' : iconsrc
});
button.setAttribute('title', newtitle);
// And send the request
var data = {
'class' : 'resource',
'field' : 'groupmode',
'value' : groupmode,
'id' : this.get_element_id(element)
};
this.send_request(data, icon);
},
/**
* Add the moveleft button
* This is required after moving left from an initial position of 0
*
* @param target The encapsulating <li> element
*/
add_moveleft : function(target) {
var left_string = M.util.get_string('moveleft', 'moodle');
var newicon = Y.Node.create('<img />')
.addClass(CSS.GENERICICONCLASS)
.setAttrs({
'src' : M.util.image_url('t/left', 'moodle'),
'title' : left_string,
'alt' : left_string
});
var anchor = new Y.Node.create('<a />')
.setStyle('cursor', 'pointer')
.addClass(CSS.MOVELEFTCLASS)
.set('title', left_string);
anchor.appendChild(newicon);
anchor.on('click', this.move_left, this);
target.one(CSS.MOVERIGHT).insert(anchor, 'before');
}
}, {
NAME : 'course-resource-toolbox',
ATTRS : {
courseid : {
'value' : 0
},
format : {
'value' : 'topics'
}
}
});
var SECTIONTOOLBOX = function() {
SECTIONTOOLBOX.superclass.constructor.apply(this, arguments);
}
Y.extend(SECTIONTOOLBOX, TOOLBOX, {
/**
* Initialize the toolboxes module
*
* Updates all span.commands with relevant handlers and other required changes
*/
initializer : function(config) {
this.setup_for_section();
M.course.coursebase.register_module(this);
},
/**
* Update any section areas within the scope of the specified
* selector with AJAX equivelants
*
* @param baseselector The selector to limit scope to
* @return void
*/
setup_for_section : function(baseselector) {
if (!baseselector) {
var baseselector = CSS.PAGECONTENT;
}
Y.all(baseselector).each(this._setup_for_section, this);
},
_setup_for_section : function(toolboxtarget) {
// Section Highlighting
this.replace_button(toolboxtarget, CSS.RIGHTDIV + ' ' + CSS.HIGHLIGHT, this.toggle_highlight);
// Section Visibility
this.replace_button(toolboxtarget, CSS.RIGHTDIV + ' ' + CSS.SHOWHIDE, this.toggle_hide_section);
},
toggle_hide_section : function(e) {
// Get the section we're working on
var section = e.target.ancestor(CSS.SECTIONLI);
var button = e.target.ancestor('a', true);
var hideicon = button.one('img');
// The value to submit
var value;
// The status text for strings and images
var status;
if (!section.hasClass(CSS.SECTIONHIDDENCLASS)) {
section.addClass(CSS.SECTIONHIDDENCLASS);
value = 0;
status = 'show';
} else {
section.removeClass(CSS.SECTIONHIDDENCLASS);
value = 1;
status = 'hide';
}
var newstring = M.util.get_string(status + 'fromothers', 'format_' + this.get('format'));
hideicon.setAttrs({
'alt' : newstring,
'title' : newstring,
'src' : M.util.image_url('i/' + status)
});
button.set('title', newstring);
// Change the highlight status
var data = {
'class' : 'section',
'field' : 'visible',
'id' : this.get_section_id(section),
'value' : value
};
var lightbox = this.add_lightbox(section);
lightbox.show();
var response = this.send_request(data, null, lightbox);
var activities = section.all(CSS.ACTIVITYLI);
activities.each(function(node) {
if (node.one(CSS.SHOW)) {
var button = node.one(CSS.SHOW);
} else {
var button = node.one(CSS.HIDE);
}
var activityid = this.get_element_id(node);
if (Y.Array.indexOf(response.resourcestotoggle, activityid) != -1) {
this.toggle_hide_resource_ui(button);
}
if (value == 0) {
button.setStyle('cursor', 'auto');
} else {
button.setStyle('cursor', 'pointer');
}
}, this);
},
toggle_highlight : function(e) {
// Get the section we're working on
var section = e.target.ancestor(CSS.SECTIONLI);
var button = e.target.ancestor('a', true);
var buttonicon = button.one('img');
// Determine whether the marker is currently set
var togglestatus = section.hasClass('current');
var value = 0;
// Set the current highlighted item text
var old_string = M.util.get_string('markthistopic', 'moodle');
Y.one(CSS.PAGECONTENT)
.all(CSS.SECTIONLI + '.current ' + CSS.HIGHLIGHT)
.set('title', old_string);
Y.one(CSS.PAGECONTENT)
.all(CSS.SECTIONLI + '.current ' + CSS.HIGHLIGHT + ' img')
.set('title', old_string)
.set('alt', old_string)
.set('src', M.util.image_url('i/marker'));
// Remove the highlighting from all sections
var allsections = Y.one(CSS.PAGECONTENT).all(CSS.SECTIONLI)
.removeClass('current');
// Then add it if required to the selected section
if (!togglestatus) {
section.addClass('current');
value = this.get_section_id(section);
var new_string = M.util.get_string('markedthistopic', 'moodle');
button
.set('title', new_string);
buttonicon
.set('title', new_string)
.set('alt', new_string)
.set('src', M.util.image_url('i/marked'));
}
// Change the highlight status
var data = {
'class' : 'course',
'field' : 'marker',
'value' : value
};
var lightbox = this.add_lightbox(section);
lightbox.show();
this.send_request(data, null, lightbox);
}
}, {
NAME : 'course-section-toolbox',
ATTRS : {
courseid : {
'value' : 0
},
format : {
'value' : 'topics'
}
}
});
M.course = M.course || {};
M.course.init_resource_toolbox = function(config) {
return new RESOURCETOOLBOX(config);
};
M.course.init_section_toolbox = function(config) {
return new SECTIONTOOLBOX(config);
};
},
'@VERSION@', {
requires : ['base', 'node', 'io', 'moodle-course-coursebase']
}
);
+3 -1
View File
@@ -95,6 +95,7 @@
echo $OUTPUT->header();
/// Print Section or custom info
get_all_mods($SITE->id, $mods, $modnames, $modnamesplural, $modnamesused);
if (!empty($CFG->customfrontpageinclude)) {
include($CFG->customfrontpageinclude);
@@ -138,7 +139,6 @@
" class=\"iconsmall\" alt=\"$streditsummary\" /></a><br /><br />";
}
get_all_mods($SITE->id, $mods, $modnames, $modnamesplural, $modnamesused);
print_section($SITE, $section, $mods, $modnamesused, true);
if ($editing) {
@@ -147,6 +147,8 @@
echo $OUTPUT->box_end();
}
}
include_course_ajax($SITE, $modnamesused);
if (isloggedin() and !isguestuser() and isset($CFG->frontpageloggedin)) {
$frontpagelayout = $CFG->frontpageloggedin;
+2 -4
View File
@@ -430,6 +430,8 @@ $string['deletedactivity'] = 'Deleted {$a}';
$string['deletedcourse'] = '{$a} has been completely deleted';
$string['deletednot'] = 'Could not delete {$a} !';
$string['deletecheck'] = 'Delete {$a} ?';
$string['deletechecktype'] = 'Are you sure that you want to delete this {$a->type}?';
$string['deletechecktypename'] = 'Are you sure that you want to delete the {$a->type} "{$a->name}"?';
$string['deletecheckfiles'] = 'Are you absolutely sure you want to delete these files?';
$string['deletecheckfull'] = 'Are you absolutely sure you want to completely delete {$a} ?';
$string['deletecheckwarning'] = 'You are about to delete these files';
@@ -806,8 +808,6 @@ $string['hidepicture'] = 'Hide picture';
$string['hidesection'] = 'Hide section {$a}';
$string['hidesettings'] = 'Hide settings';
$string['hideshowblocks'] = 'Hide or show blocks';
$string['hidetopicfromothers'] = 'Hide topic';
$string['hideweekfromothers'] = 'Hide week';
$string['hits'] = 'Hits';
$string['hitsoncourse'] = 'Hits on {$a->coursename} by {$a->username}';
$string['hitsoncoursetoday'] = 'Today\'s hits on {$a->coursename} by {$a->username}';
@@ -1506,8 +1506,6 @@ $string['showreports_help'] = 'Activity reports are available for each participa
$string['showsettings'] = 'Show settings';
$string['showtheselogs'] = 'Show these logs';
$string['showthishelpinlanguage'] = 'Show this help in language: {$a}';
$string['showtopicfromothers'] = 'Show topic';
$string['showweekfromothers'] = 'Show week';
$string['schedule'] = 'Schedule';
$string['since'] = 'Since';
$string['sincelast'] = 'since last login';
-314
View File
@@ -1,314 +0,0 @@
/**
* Contains Main class and supporting functions for ajax course layout
*/
//hide content body until done loading (manipulation looks ugly elsewise)
//document.getElementById('content').style.display = 'none';
// If firebug console is undefined, define a fake one here
if (window.console) {
console.vardump = function(data) {
retval = '';
for (key in data) {
retval += key+' = '+data[key] + "\n";
}
console.log(retval);
};
}
//onload object for handling scripts on page load, this insures they run in my order
function onload_class() {
this.scripts = new Array();
this.debug = true;
}
onload_class.prototype.add = function(script) {
if (this.debug) {
YAHOO.log("onloadobj.add - adding "+script, "junk");
}
this.scripts[this.scripts.length] = script;
};
onload_class.prototype.load = function() {
var scriptcount = this.scripts.length;
if (this.debug) {
YAHOO.log("onloadobj.load - loading "+scriptcount+" scripts", "info");
}
for (i=0; i<scriptcount; i++) {
eval(this.scripts[i]);
}
};
var onloadobj = new onload_class();
//main page object
function main_class() {
this.debug = true;
this.portal = new php_portal_class();
this.blocks = new Array();
this.sections = new Array();
this.sectiondates = {};
this.leftcolumn = null;
this.rightcolumn = null;
this.adminBlock = null;
this.tempBlock = null;
this.icons = [];
this.courseformat = null;
this.marker = null;
this.numsections = null;
this.lastsection = null; // real last section num including unavailable
//things to process onload
onloadobj.add('main.process_document();');
onloadobj.add("if (document.getElementById('content')) document.getElementById('content').style.display='block';");
//connection queue allows xhttp requests to be sent in order
this.connectQueue = [];
this.connectQueueHead = 0;
this.connectQueueConnection = null;
}
main_class.prototype.process_blocks = function() {
//remove unneeded icons (old school position icons and delete/hide
//although they will be read)
var rmIconClasses = ['icon up', 'icon down', 'icon right', 'icon left', 'icon delete', 'icon hide'];
for (var c=0; c<rmIconClasses.length; c++) {
els = YAHOO.util.Dom.getElementsByClassName(rmIconClasses[c]);
for (var x=0; x<els.length; x++) {
els[x].parentNode.removeChild(els[x]);
}
}
//process the block ids passed from php
var blockcount = this.portal.blocks.length;
YAHOO.log("main.processBlocks - processing "+blockcount+" blocks", "info");
for (i=0; i<blockcount; i++) {
this.blocks[i] = new block_class(this.portal.blocks[i][1], "blocks");
//put in correct side array also
if (this.portal.blocks[i][0] == 'l') {
main.leftcolumn.add_block(this.blocks[i]);
} else if (this.portal.blocks[i][0] == 'r') {
main.rightcolumn.add_block(this.blocks[i]);
}
//hide if called for
if (this.portal.blocks[i][2] == 1) {
this.blocks[i].toggle_hide(null, null, true);
}
}
};
main_class.prototype.process_document = function() {
//process the document to get important containers
YAHOO.log("Processing Document", "info");
//process columns for blocks
this.leftcolumn = new column_class('left-column', "blocks", null, 'l');
this.rightcolumn = new column_class('right-column', "blocks", null, 'r');
//process sections
//var ct = 0;
//while (document.getElementById('section-'+ct) != null) {
this.courseformat = this.portal.courseformat;
for(var ct=0; ct <= this.portal.lastsection; ct++){
if (document.getElementById('section-'+ct) != null) {
var dragable = ((ct > 0) && (ct <= this.portal.numsections));
this.sections[ct] = new section_class('section-'+ct, "sections", null, dragable);
this.sections[ct].addToGroup('resources');
if (ct > 0) {
var sectiontitle = YAHOO.util.Selector.query('#section-'+ct+' h3.weekdates')[0];
if (undefined !== sectiontitle) { // Only save date for weekly format
this.sectiondates[ct] = sectiontitle.innerHTML;
}
}
} else {
this.sections[ct] = null;
}
//ct++;
}
if (this.debug) {
YAHOO.log("Processed "+ct+" sections");
}
this.adminBlock = YAHOO.util.Dom.getElementsByClassName('block_adminblock')[0];
this.tempBlock = YAHOO.util.Dom.getElementsByClassName('tempblockhandler')[0];
};
main_class.prototype.mk_safe_for_transport = function(input) {
return input.replace(/&/i, '_.amp._');
};
//return block by id
main_class.prototype.get_block_index = function(el) {
var blockcount = this.blocks.length;
for (i=0; i<blockcount; i++) {
if (this.blocks[i] == el) {
return i;
}
}
};
main_class.prototype.get_section_index = function(el) {
var sectioncount = this.sections.length;
for (i=0; i<sectioncount; i++) {
if (this.sections[i] == el) {
return i;
}
}
return -1;
};
main_class.prototype.mk_button = function(tag, imgSrc, text, attributes, imgAttributes) {
//Create button and return object.
//Set the text: the container TITLE or image ALT attributes can be overridden, eg.
// main.mk_button('a', main.portal.icons['move_2d'], strmove, [['title', strmoveshort]]);
var container = document.createElement(tag);
container.style.cursor = 'pointer';
container.setAttribute('title', text);
var image = document.createElement('img');
image.setAttribute('src', imgSrc);
image.setAttribute('alt', text);
container.appendChild(image);
if (attributes != null) {
for (var c=0; c<attributes.length; c++) {
if (attributes[c][0] == 'title' && this.is_ie()) {
image.setAttribute(attributes[c][0], attributes[c][1]); //IE hack: transfer 'title'.
} else {
container.setAttribute(attributes[c][0], attributes[c][1]);
}
}
}
if (imgAttributes != null) {
for (var c=0; c<imgAttributes.length; c++) {
image.setAttribute(imgAttributes[c][0], imgAttributes[c][1]);
}
}
image.setAttribute('hspace', '3');
return container;
};
main_class.prototype.connect = function(method, urlStub, callback, body) {
if (this.debug) {
YAHOO.log("Making "+method+" connection to /course/rest.php?courseId="+main.portal.id+"&"+urlStub);
}
if (callback == null) {
if (this.debug) {
callback = {
success: function(response) {
YAHOO.log("Response from the Request: " + response.statusText + ": " + response.responseText, 'info');
},
failure: function() {
YAHOO.log("Response from the Request: " + response.statusText + ": " + response.responseText, 'error');
}
};
} else {
callback = {};
}
}
return YAHOO.util.Connect.asyncRequest(method, this.portal.strings['wwwroot']+"/course/rest.php?courseId="+main.portal.id+"&sesskey="+this.portal.strings['sesskey']+"&"+urlStub, callback, body);
};
main_class.prototype.connectQueue_add = function(method, urlStub, callback, body) {
var Qlength = main.connectQueue.length;
main.connectQueue[Qlength] = [];
main.connectQueue[Qlength]['method'] = method;
main.connectQueue[Qlength]['urlStub'] = urlStub;
main.connectQueue[Qlength]['body'] = body;
if (main.connectQueueConnection == null || !YAHOO.util.Connect.isCallInProgress(main.connectQueueConnection)) {
main.connectQueue_fireNext();
}
};
main_class.prototype.connectQueue_fireNext = function() {
var head = main.connectQueueHead;
if (head >= main.connectQueue.length) {
return;
}
var callback = {
success: function(){
main.connectQueue_fireNext();
}
};
main.connectQueueConnection = main.connect(main.connectQueue[head]['method'],
main.connectQueue[head]['urlStub'],
callback,
main.connectQueue[head]['body']);
main.connectQueueHead++;
};
main_class.prototype.update_marker = function(newMarker) {
if (this.marker != null) {
this.marker.toggle_highlight();
}
this.marker = newMarker;
this.marker.toggle_highlight();
this.connect('post', 'class=course&field=marker', null, 'value='+this.marker.sectionId);
};
main_class.prototype.getString = function(identifier, variable) {
if (this.portal.strings[identifier]) {
return this.portal.strings[identifier].replace(/_var_/, variable);
}
};
main_class.prototype.hasString = function(identifier) {
if (this.portal.strings[identifier]) {
return true;
}
return false;
};
main_class.prototype.is_ie = function() {
var agent = navigator.userAgent.toLowerCase();
if ((agent.indexOf('msie') != -1)) {
return true;
}
return false;
};
var main = new main_class();
function php_portal_class() {
//portal to php data
this.id = null;
this.debug = null;
//array of id's of blocks set at end of page load by php
this.blocks = new Array();
this.imagePath = null;
//flag for week fomat
this.isWeek = false;
//strings
this.strings = [];
//icons
this.icons = [];
YAHOO.log("Instantiated php_portal_class", "info");
}
+1 -104
View File
@@ -82,107 +82,4 @@ function ajaxenabled(array $browsers = null) {
} else {
return false;
}
}
// ==============================================================================
// TODO: replace this with something more up-to-date with our coding standards
/**
* Used to create view of document to be passed to JavaScript on pageload.
* We use this class to pass data from PHP to JavaScript.
*/
class jsportal {
var $currentblocksection = null;
var $blocks = array();
/**
* Takes id of block and adds it
*/
function block_add($id, $hidden=false){
$hidden_binary = 0;
if ($hidden) {
$hidden_binary = 1;
}
$this->blocks[count($this->blocks)] = array($this->currentblocksection, $id, $hidden_binary);
}
/**
* Prints the JavaScript code needed to set up AJAX for the course.
*/
function print_javascript($courseid, $return=false) {
global $CFG, $USER, $OUTPUT, $COURSE, $DB;
$blocksoutput = $output = '';
for ($i=0; $i<count($this->blocks); $i++) {
$blocksoutput .= "['".$this->blocks[$i][0]."',
'".$this->blocks[$i][1]."',
'".$this->blocks[$i][2]."']";
if ($i != (count($this->blocks) - 1)) {
$blocksoutput .= ',';
}
}
$output .= "<script type=\"text/javascript\">\n";
$output .= " main.portal.id = ".$courseid.";\n";
$output .= " main.portal.blocks = new Array(".$blocksoutput.");\n";
$output .= " main.portal.strings['courseformat']='".$COURSE->format."';\n";
$output .= " main.portal.strings['wwwroot']='".$CFG->wwwroot."';\n";
$output .= " main.portal.strings['marker']='".addslashes_js(get_string('markthistopic', '', '_var_'))."';\n";
$output .= " main.portal.strings['marked']='".addslashes_js(get_string('markedthistopic', '', '_var_'))."';\n";
$output .= " main.portal.numsections = ".$COURSE->numsections.";\n";
$output .= " main.portal.lastsection = ".$DB->get_field_sql("SELECT MAX(section) FROM {course_sections} WHERE course = ?", array($courseid)).";\n"; // needed for orphaned activities in unavailable sections
$output .= " main.portal.strings['hide']='".addslashes_js(get_string('hide'))."';\n";
$output .= " main.portal.strings['hidesection']='".addslashes_js(get_string('hidesection', '', '_var_'))."';\n";
$output .= " main.portal.strings['show']='".addslashes_js(get_string('show'))."';\n";
$output .= " main.portal.strings['delete']='".addslashes_js(get_string('delete'))."';\n";
$output .= " main.portal.strings['move']='".addslashes_js(get_string('move'))."';\n";
$output .= " main.portal.strings['movesection']='".addslashes_js(get_string('movesection', '', '_var_'))."';\n";
$output .= " main.portal.strings['moveleft']='".addslashes_js(get_string('moveleft'))."';\n";
$output .= " main.portal.strings['moveright']='".addslashes_js(get_string('moveright'))."';\n";
$output .= " main.portal.strings['update']='".addslashes_js(get_string('update'))."';\n";
$output .= " main.portal.strings['groupsnone']='".addslashes_js(get_string('clicktochangeinbrackets', 'moodle', get_string('groupsnone')))."';\n";
$output .= " main.portal.strings['groupsseparate']='".addslashes_js(get_string('clicktochangeinbrackets', 'moodle', get_string('groupsseparate')))."';\n";
$output .= " main.portal.strings['groupsvisible']='".addslashes_js(get_string('clicktochangeinbrackets', 'moodle', get_string('groupsvisible')))."';\n";
$output .= " main.portal.strings['deletecheck']='".addslashes_js(get_string('deletecheckfull','','_var_'))."';\n";
$output .= " main.portal.strings['resource']='".addslashes_js(get_string('resource'))."';\n";
$output .= " main.portal.strings['activity']='".addslashes_js(get_string('activity'))."';\n";
$output .= " main.portal.strings['sesskey']='".sesskey()."';\n";
foreach (array_keys(get_plugin_list('mod')) as $modtype) {
$output .= " main.portal.strings['modtype_".$modtype."']='".addslashes_js(get_string('pluginname', 'mod_'.$modtype))."';\n";
}
$output .= " main.portal.icons['spacerimg']='".$OUTPUT->pix_url('spacer')."';\n";
$output .= " main.portal.icons['marker']='".$OUTPUT->pix_url('i/marker')."';\n";
$output .= " main.portal.icons['marked']='".$OUTPUT->pix_url('i/marked')."';\n";
$output .= " main.portal.icons['ihide']='".$OUTPUT->pix_url('i/hide')."';\n";
$output .= " main.portal.icons['move_2d']='".$OUTPUT->pix_url('i/move_2d')."';\n";
$output .= " main.portal.icons['show']='".$OUTPUT->pix_url('t/show')."';\n";
$output .= " main.portal.icons['hide']='".$OUTPUT->pix_url('t/hide')."';\n";
$output .= " main.portal.icons['delete']='".$OUTPUT->pix_url('t/delete')."';\n";
$output .= " main.portal.icons['groupn']='".$OUTPUT->pix_url('t/groupn')."';\n";
$output .= " main.portal.icons['groups']='".$OUTPUT->pix_url('t/groups')."';\n";
$output .= " main.portal.icons['groupv']='".$OUTPUT->pix_url('t/groupv')."';\n";
if (right_to_left()) {
$output .= " main.portal.icons['backwards']='".$OUTPUT->pix_url('t/right')."';\n";
$output .= " main.portal.icons['forwards']='".$OUTPUT->pix_url('t/left')."';\n";
} else {
$output .= " main.portal.icons['backwards']='".$OUTPUT->pix_url('t/left')."';\n";
$output .= " main.portal.icons['forwards']='".$OUTPUT->pix_url('t/right')."';\n";
}
$output .= " onloadobj.load();\n";
$output .= " main.process_blocks();\n";
$output .= "</script>";
if ($return) {
return $output;
} else {
echo $output;
}
}
}
}
File diff suppressed because it is too large Load Diff