Commit d1bf622a authored by Robert Lyon's avatar Robert Lyon

Bug 1316917: New approach to star rating using bootstrap glyphs

This is a slimmer / stripped down system for doing jquery ratings.

Instead of relying on styling radio buttons it relies on bootstrap
glyphs and a hidden input field.

The code is also controlled by a pieform element

See lib/form/elements/ratings.php for more info about that part

It also has some new settings in the Extensions -> artefact -> comment
config form. They include settign the colour for the star icon, or
using a different icon, eg hearts/thumbs up, and the number of ratings
to show (3 - 12)

behatnotneeded

Change-Id: Ibf529efcb9a665c9f303242ed12d0c7b3dee2356
Signed-off-by: Robert Lyon's avatarRobert Lyon <robertl@catalyst.net.nz>
parent 75205f36
......@@ -57,10 +57,8 @@ $elements['message'] = array(
);
if (get_config_plugin('artefact', 'comment', 'commentratings')) {
$elements['rating'] = array(
'type' => 'radio',
'type' => 'ratings',
'title' => get_string('rating', 'artefact.comment'),
'options' => array('1' => '', '2' => '', '3' => '', '4' => '', '5' => ''),
'class' => 'star',
'defaultvalue' => $comment->get('rating'),
);
}
......
......@@ -29,7 +29,6 @@ $string['commentmadepublic'] = "Comment made public";
$string['commentdeletedauthornotification'] = "Your comment on %s was deleted:\n%s";
$string['commentdeletednotificationsubject'] = 'Comment on %s deleted';
$string['commentnotinview'] = 'Comment %d not in page %d';
$string['commentratings'] = 'Enable comment ratings';
$string['commentremoved'] = 'Comment removed';
$string['commentremovedbyauthor'] = 'Comment removed by the author';
$string['commentremovedbyowner'] = 'Comment removed by the owner';
......@@ -112,3 +111,15 @@ To see %s online, follow this link:
$string['artefactdefaultpermissions'] = 'Default comment permission';
$string['artefactdefaultpermissionsdescription'] = 'The selected artefact types will have comments enabled on creation. Users can override these settings for individual artefacts.';
// Extension config form
$string['commentratings'] = 'Enable comment ratings';
$string['ratingicons'] = 'Icon to use to display ratings';
$string['ratinglength'] = 'Number of rating choices';
$string['ratingcolour'] = 'Colour';
$string['ratingcolourdesc'] = 'The colour to display the rating choices in. A chosen rating will display the icon in the solid colour, and an unchosen one will display the colour in the icon outline.';
$string['star'] = 'Star';
$string['heart'] = 'Heart';
$string['thumbsup'] = 'Thumbs up';
$string['ok'] = 'Tick';
$string['ratingexample'] = 'Generated example';
......@@ -2,7 +2,7 @@
<!-- @copyright For copyright information on Mahara, please see the README file distributed with this software. -->
<h3>Comment ratings</h3>
<p>Turn this on to enable 5-star ratings on artefacts and pages.</p>
<p>Note that ratings cannot be on their own and require either a comment or
a file attachment.</p>
<p>Turn this on to enable ratings on artefacts and pages.</p>
<p>You can choose a rating scale from 3 to 12 items, the icon, and the icon colour to use. </p>
<p>If you change the number of rating choices, please note that the ratings themselves will not be recalculated to fit that new scale.</p>
<p>Note that ratings cannot be on their own and require either a comment or a file attachment.</p>
......@@ -15,7 +15,8 @@ require_once('activity.php');
require_once('license.php');
define('MIN_RATING', 1);
define('MAX_RATING', 5);
$maxrating = get_config_plugin('artefact', 'comment', 'ratinglength');
define('MAX_RATING', $maxrating ? $maxrating : 5);
function valid_rating($ratingstr) {
if (empty($ratingstr)) {
......@@ -870,6 +871,10 @@ class ArtefactTypeComment extends ArtefactType {
$smarty->assign('viewid', $data->view);
$smarty->assign('baseurl', $data->baseurl);
$smarty->assign('onview', $onview);
$icon = get_config_plugin('artefact', 'comment', 'ratingicon');
$smarty->assign('star', $icon ? $icon : 'star');
$colour = get_config_plugin('artefact', 'comment', 'ratingcolour');
$smarty->assign('colour', $colour ? $colour : '#DBB80E');
$data->tablerows = $smarty->fetch('artefact:comment:commentlist.tpl');
......@@ -934,10 +939,8 @@ class ArtefactTypeComment extends ArtefactType {
);
if (get_config_plugin('artefact', 'comment', 'commentratings')) {
$form['elements']['rating'] = array(
'type' => 'radio',
'type' => 'ratings',
'title' => get_string('rating', 'artefact.comment'),
'options' => array('1' => '', '2' => '', '3' => '', '4' => '', '5' => ''),
'class' => 'star',
);
}
$form['elements']['ispublic'] = array(
......@@ -1081,6 +1084,10 @@ class ArtefactTypeComment extends ArtefactType {
}
public static function get_config_options() {
$length = get_config_plugin('artefact', 'comment', 'ratinglength');
$length = empty($length) ? 5 : $length;
$colour = get_config_plugin('artefact', 'comment', 'ratingcolour');
$colour = empty($colour) ? '#DBB80E' : $colour;
$elements = array(
'commentratings' => array(
'type' => 'switchbox',
......@@ -1088,6 +1095,48 @@ class ArtefactTypeComment extends ArtefactType {
'defaultvalue' => get_config_plugin('artefact', 'comment', 'commentratings'),
'help' => true,
),
'ratingicon' => array(
'type' => 'select',
'title' => get_string('ratingicons', 'artefact.comment'),
'defaultvalue' => get_config_plugin('artefact', 'comment', 'ratingicon'),
'options' => array(
'star' => get_string('star', 'artefact.comment'),
'heart' => get_string('heart', 'artefact.comment'),
'thumbs-up' => get_string('thumbsup', 'artefact.comment'),
'check-circle' => get_string('ok', 'artefact.comment'),
),
),
'ratinglength' => array(
'type' => 'select',
'title' => get_string('ratinglength', 'artefact.comment'),
'defaultvalue' => $length,
'options' => array(
'3' => '3',
'4' => '4',
'5' => '5',
'6' => '6',
'7' => '7',
'8' => '8',
'9' => '9',
'10' => '10',
'11' => '11',
'12' => '12',
),
),
'ratingcolour' => array(
'type' => 'color',
'title' => get_string('ratingcolour', 'artefact.comment'),
'defaultvalue' => $colour,
'description' => get_string('ratingcolourdesc', 'artefact.comment'),
),
'ratingexample' => array(
'type' => 'ratings',
'title' => get_string('ratingexample', 'artefact.comment'),
'readonly' => true,
'defaultvalue' => ceil($length / 2),
'iconempty' => true,
'officon' => 'dummy',
),
);
return array(
'name' => 'commentconfig',
......@@ -1098,7 +1147,9 @@ class ArtefactTypeComment extends ArtefactType {
}
public static function save_config_options($form, $values) {
foreach (array('commentratings') as $settingname) {
$valid = array('commentratings', 'ratingicon', 'ratinglength',
'ratingcolour');
foreach ($valid as $settingname) {
set_config_plugin('artefact', 'comment', $settingname, $values[$settingname]);
}
}
......
jquery.rating.js
=================
Website: http://code.google.com/p/jquery-star-rating-plugin/
Version: 3.14
Changes:
* Added a pager hook for reskining
* Changed name and path of images in theme/raw/style/jquery.rating.css
* Remove the $.browser check for IE8 & older (because jQuery no longer ships it by default, and
we no longer support IE8.)
\ No newline at end of file
// Customised system based on the proof of concept workings of
// https://github.com/robertlabrie/bootstrap-star-rating
(function ( $ ) {
$.fn.rating = function( method, options ) {
method = method || 'create';
// This is the easiest way to have default options.
var settings = $.extend({
// These are the defaults.
limit: 5,
value: 0,
glyph: "icon-star",
offglyph: "icon-ban",
emptyglyph: false,
coloroff: "gray",
coloron: "gold",
size: "1.0em",
padding: "0 3px",
cursor: "default",
readonly: false,
onClick: function () {},
endofarray: "idontmatter"
}, options );
var style = "";
style = style + "font-size:" + settings.size + "; ";
style = style + "color:" + settings.coloroff + "; ";
style = style + "cursor:" + settings.cursor + "; ";
style = style + "padding:" + settings.padding + "; ";
if (method == 'create') {
// Initialize the data-rating property
this.each(function() {
attr = $(this).attr('data-rating');
if (attr === undefined || attr === false) { $(this).attr('data-rating',settings.value); }
});
// Add 'no rating' glyph
this.append('<span data-value="0" class="ratingicon icon ' + settings.offglyph + '" style="' + style + '" aria-hidden="true"></span>');
// Loop through the glyphs
for (var i = 0; i < settings.limit; i++) {
this.append('<span data-value="' + (i+1) + '" class="ratingicon icon ' + settings.glyph + '" style="' + style + '" aria-hidden="true"></span>');
}
// Paint the glyphs
this.each(function() { paint($(this)); });
}
if (method == 'set') {
this.attr('data-rating',options);
this.each(function() { paint($(this)); });
}
if (method == 'get') {
return this.attr('data-rating');
}
// Register the click events
this.find("span.ratingicon").click(function() {
if (settings.readonly !== true) {
rating = $(this).attr('data-value')
$(this).parent().attr('data-rating',rating);
paint($(this).parent());
settings.onClick.call( $(this).parent() );
}
});
function paint(div) {
rating = parseInt(div.attr('data-rating'));
// If there is an input in the div lets set it's value
div.find("input").val(rating);
div.find("span.ratingicon").each(function() {
// Now paint the glyphs
var rating = parseInt($(this).parent().attr('data-rating'));
var value = parseInt($(this).attr('data-value'));
if (value > rating || (value == 0 && rating > 0)) {
$(this).css('color',settings.coloroff);
if (settings.emptyglyph) {
if ($(this).hasClass(settings.glyph)) {
// need to add the '-o' before the end for thumbs
if (settings.glyph == 'icon-thumbs-up') {
$(this).removeClass(settings.glyph).addClass('icon-thumbs-o-up');
}
else {
$(this).removeClass(settings.glyph).addClass(settings.glyph + '-o');
}
}
}
}
else {
$(this).css('color',settings.coloron);
}
});
}
};
}( jQuery ));
/*
### jQuery Star Rating Plugin v3.14 - 2012-01-26 ###
* Home: http://www.fyneworks.com/jquery/star-rating/
* Code: http://code.google.com/p/jquery-star-rating-plugin/
*
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
###
*/
/*# AVOID COLLISIONS #*/
;if(window.jQuery) (function($){
/*# AVOID COLLISIONS #*/
// plugin initialization
$.fn.rating = function(options){
if(this.length==0) return this; // quick fail
// Handle API methods
if(typeof arguments[0]=='string'){
// Perform API methods on individual elements
if(this.length>1){
var args = arguments;
return this.each(function(){
$.fn.rating.apply($(this), args);
});
};
// Invoke API method handler
$.fn.rating[arguments[0]].apply(this, $.makeArray(arguments).slice(1) || []);
// Quick exit...
return this;
};
// Initialize options for this call
var options = $.extend(
{}/* new object */,
$.fn.rating.options/* default options */,
options || {} /* just-in-time options */
);
// Allow multiple controls with the same name by making each call unique
$.fn.rating.calls++;
// loop through each matched element
this
.not('.star-rating-applied')
.addClass('star-rating-applied')
.each(function(){
// Load control parameters / find context / etc
var control, input = $(this);
var eid = (this.name || 'unnamed-rating').replace(/\[|\]/g, '_').replace(/^\_+|\_+$/g,'');
var context = $(this.form || document.body);
// FIX: http://code.google.com/p/jquery-star-rating-plugin/issues/detail?id=23
var raters = context.data('rating');
if(!raters || raters.call!=$.fn.rating.calls) raters = { count:0, call:$.fn.rating.calls };
var rater = raters[eid];
// if rater is available, verify that the control still exists
if(rater) control = rater.data('rating');
if(rater && control)//{// save a byte!
// add star to control if rater is available and the same control still exists
control.count++;
//}// save a byte!
else{
// create new control if first star or control element was removed/replaced
// Initialize options for this rater
control = $.extend(
{}/* new object */,
options || {} /* current call options */,
($.metadata? input.metadata(): ($.meta?input.data():null)) || {}, /* metadata options */
{ count:0, stars: [], inputs: [] }
);
// increment number of rating controls
control.serial = raters.count++;
// create rating element
rater = $('<span class="star-rating-control"/>');
input.before(rater);
// Mark element for initialization (once all stars are ready)
rater.addClass('rating-to-be-drawn');
// Accept readOnly setting from 'disabled' property
if(input.attr('disabled') || input.hasClass('disabled')) control.readOnly = true;
// Accept required setting from class property (class='required')
if(input.hasClass('required')) control.required = true;
// Create 'cancel' button
rater.append(
control.cancel = $('<div class="rating-cancel"><a title="' + control.cancel + '">' + control.cancelValue + '</a></div>')
.mouseover(function(){
$(this).rating('drain');
$(this).addClass('star-rating-hover');
//$(this).rating('focus');
})
.mouseout(function(){
$(this).rating('draw');
$(this).removeClass('star-rating-hover');
//$(this).rating('blur');
})
.click(function(){
$(this).rating('select');
})
.data('rating', control)
);
}; // first element of group
// insert rating star
var star = $('<div class="star-rating rater-'+ control.serial +'"><a title="' + (this.title || this.value) + '">' + this.value + '</a></div>');
rater.append(star);
// inherit attributes from input element
if(this.id) star.attr('id', this.id);
if(this.className) star.addClass(this.className);
// Half-stars?
if(control.half) control.split = 2;
// Prepare division control
if(typeof control.split=='number' && control.split>0){
var stw = ($.fn.width ? star.width() : 0) || control.starWidth;
var spi = (control.count % control.split), spw = Math.floor(stw/control.split);
star
// restrict star's width and hide overflow (already in CSS)
.width(spw)
// move the star left by using a negative margin
// this is work-around to IE's stupid box model (position:relative doesn't work)
.find('a').css({ 'margin-left':'-'+ (spi*spw) +'px' })
};
// readOnly?
if(control.readOnly)//{ //save a byte!
// Mark star as readOnly so user can customize display
star.addClass('star-rating-readonly');
//} //save a byte!
else//{ //save a byte!
// Enable hover css effects
star.addClass('star-rating-live')
// Attach mouse events
.mouseover(function(){
$(this).rating('fill');
$(this).rating('focus');
})
.mouseout(function(){
$(this).rating('draw');
$(this).rating('blur');
})
.click(function(){
$(this).rating('select');
})
;
//}; //save a byte!
// set current selection
if(this.checked) control.current = star;
// set current select for links
if(this.nodeName=="A"){
if($(this).hasClass('selected'))
control.current = star;
};
// hide input element
input.hide();
// backward compatibility, form element to plugin
input.change(function(){
$(this).rating('select');
});
// attach reference to star to input element and vice-versa
star.data('rating.input', input.data('rating.star', star));
// store control information in form (or body when form not available)
control.stars[control.stars.length] = star[0];
control.inputs[control.inputs.length] = input[0];
control.rater = raters[eid] = rater;
control.context = context;
input.data('rating', control);
rater.data('rating', control);
star.data('rating', control);
context.data('rating', raters);
}); // each element
// Initialize ratings (first draw)
$('.rating-to-be-drawn').rating('draw').removeClass('rating-to-be-drawn');
return this; // don't break the chain...
};
/*--------------------------------------------------------*/
/*
### Core functionality and API ###
*/
$.extend($.fn.rating, {
// Used to append a unique serial number to internal control ID
// each time the plugin is invoked so same name controls can co-exist
calls: 0,
focus: function(){
var control = this.data('rating'); if(!control) return this;
if(!control.focus) return this; // quick fail if not required
// find data for event
var input = $(this).data('rating.input') || $( this.tagName=='INPUT' ? this : null );
// focus handler, as requested by focusdigital.co.uk
if(control.focus) control.focus.apply(input[0], [input.val(), $('a', input.data('rating.star'))[0]]);
}, // $.fn.rating.focus
blur: function(){
var control = this.data('rating'); if(!control) return this;
if(!control.blur) return this; // quick fail if not required
// find data for event
var input = $(this).data('rating.input') || $( this.tagName=='INPUT' ? this : null );
// blur handler, as requested by focusdigital.co.uk
if(control.blur) control.blur.apply(input[0], [input.val(), $('a', input.data('rating.star'))[0]]);
}, // $.fn.rating.blur
fill: function(){ // fill to the current mouse position.
var control = this.data('rating'); if(!control) return this;
// do not execute when control is in read-only mode
if(control.readOnly) return;
// Reset all stars and highlight them up to this element
this.rating('drain');
this.prevAll().andSelf().filter('.rater-'+ control.serial).addClass('star-rating-hover');
},// $.fn.rating.fill
drain: function() { // drain all the stars.
var control = this.data('rating'); if(!control) return this;
// do not execute when control is in read-only mode
if(control.readOnly) return;
// Reset all stars
control.rater.children().filter('.rater-'+ control.serial).removeClass('star-rating-on').removeClass('star-rating-hover');
},// $.fn.rating.drain
draw: function(){ // set value and stars to reflect current selection
var control = this.data('rating'); if(!control) return this;
// Clear all stars
this.rating('drain');
// Set control value
if(control.current){
control.current.data('rating.input').attr('checked','checked');
control.current.prevAll().andSelf().filter('.rater-'+ control.serial).addClass('star-rating-on');
}
else
$(control.inputs).removeAttr('checked');
// Show/hide 'cancel' button
control.cancel[control.readOnly || control.required?'hide':'show']();
// Add/remove read-only classes to remove hand pointer
this.siblings()[control.readOnly?'addClass':'removeClass']('star-rating-readonly');
},// $.fn.rating.draw
select: function(value,wantCallBack){ // select a value
// ***** MODIFICATION *****
// Thanks to faivre.thomas - http://code.google.com/p/jquery-star-rating-plugin/issues/detail?id=27
//
// ***** LIST OF MODIFICATION *****
// ***** added Parameter wantCallBack : false if you don't want a callback. true or undefined if you want postback to be performed at the end of this method'
// ***** recursive calls to this method were like : ... .rating('select') it's now like .rating('select',undefined,wantCallBack); (parameters are set.)
// ***** line which is calling callback
// ***** /LIST OF MODIFICATION *****
var control = this.data('rating'); if(!control) return this;
// do not execute when control is in read-only mode
if(control.readOnly) return;
// clear selection
control.current = null;
// programmatically (based on user input)
if(typeof value!='undefined'){
// select by index (0 based)
if(typeof value=='number')
return $(control.stars[value]).rating('select',undefined,wantCallBack);
// select by literal value (must be passed as a string
if(typeof value=='string')
//return
$.each(control.stars, function(){
if($(this).data('rating.input').val()==value) $(this).rating('select',undefined,wantCallBack);
});
}
else
control.current = this[0].tagName=='INPUT' ?
this.data('rating.star') :
(this.is('.rater-'+ control.serial) ? this : null);
// Update rating control state
this.data('rating', control);
// Update display
this.rating('draw');
// find data for event
var input = $( control.current ? control.current.data('rating.input') : null );
// click callback, as requested here: http://plugins.jquery.com/node/1655
// **** MODIFICATION *****
// Thanks to faivre.thomas - http://code.google.com/p/jquery-star-rating-plugin/issues/detail?id=27
//
//old line doing the callback :
//if(control.callback) control.callback.apply(input[0], [input.val(), $('a', control.current)[0]]);// callback event
//
//new line doing the callback (if i want :)
if((wantCallBack ||wantCallBack == undefined) && control.callback) control.callback.apply(input[0], [input.val(), $('a', control.current)[0]]);// callback event
//to ensure retro-compatibility, wantCallBack must be considered as true by default
// **** /MODIFICATION *****
},// $.fn.rating.select
readOnly: function(toggle, disable){ // make the control read-only (still submits value)