MDL-51802 core: new template for quick editing a title

convert editing tag name to use new template
This commit is contained in:
Marina Glancy 2016-01-18 15:18:14 +08:00
parent 2f45a11ac4
commit cdc5f9785b
23 changed files with 636 additions and 171 deletions

View File

@ -277,6 +277,7 @@ $string['idnumbertaken'] = 'This ID number is already in use';
$string['idnumbertoolong'] = 'ID number is too long';
$string['importformatnotimplement'] = 'Sorry, importing this format is not yet implemented!';
$string['incorrectext'] = 'File has an incorrect extension';
$string['inplaceeditableerror'] = 'Error calling update processor';
$string['installproblem'] = 'It is usually not possible to recover from errors triggered during installation, you may need to create a new database or use a different database prefix if you want to retry the installation.';
$string['internalauthpassworderror'] = 'Missing password or invalid password policy for internal authentication';
$string['invalidaccess'] = 'This page was not accessed correctly';

1
lib/amd/build/inplace_editable.min.js vendored Normal file
View File

@ -0,0 +1 @@
define(["jquery","core/ajax","core/templates","core/notification","core/str","core/config"],function(a,b,c,d,e,f){return a("body").on("click keypress","[data-inplaceeditable] [data-inplaceeditablelink]",function(g){if("keypress"!==g.type||13===g.keyCode){g.stopImmediatePropagation(),g.preventDefault();var h=a(this),i=h.closest("[data-inplaceeditable]"),j=function(e,f){var g=b.call([{methodname:"core_update_inplace_editable",args:{itemid:e.attr("data-itemid"),component:e.attr("data-component"),itemtype:e.attr("data-itemtype"),value:f}}],!0);a.when.apply(a,g).done(function(a){var b=e.attr("data-value");c.render("core/inplace_editable",a).done(function(a,b){c.replaceNode(e,a,b),e.find("[data-inplaceeditablelink]").focus()}),e.trigger({type:"updated",ajaxreturn:a,oldvalue:b})}).fail(function(b){var c=a.Event("updatefailed",{exception:b,newvalue:f});e.trigger(c),c.isDefaultPrevented()||d.exception(b)})},k=function(a){var b=a.find("input");b.off(),a.html(a.attr("data-oldcontent")),a.removeAttr("data-oldcontent"),a.removeClass("inplaceeditingon")},l=function(){a("span.inplaceeditable.inplaceeditingon").each(function(){k(a(this))})},m=function(b,c){for(var d=b,e=0;c>e;e++)d+=String(Math.floor(10*Math.random()));return 0===a("#"+d).length?d:m(b,c)},n=function(b){b.addClass("inplaceeditingon"),b.attr("data-oldcontent",b.html()),e.get_string("edittitleinstructions").done(function(c){var d=a('<span class="editinstructions">'+c+"</span>").attr("id",m("id_editinstructions_",20)),e=a('<input type="text"/>').attr("id",m("id_inplacevalue_",20)).attr("value",b.attr("data-value")).attr("aria-describedby",d.attr("id")),g=a('<label class="accesshide">'+i.attr("data-editlabel")+"</label>").attr("for",e.attr("id"));b.html("").append(d).append(g).append(e),e.focus(),e.select(),e.on("keyup keypress focusout",function(a){f.behatsiterunning&&"focusout"===a.type||("keypress"===a.type&&13===a.keyCode&&(j(b,e.val()),k(b)),("keyup"===a.type&&27===a.keyCode||"focusout"===a.type)&&k(b))})})};l(),n(i)}}),{}});

View File

@ -1 +1 @@
define(["jquery","core/ajax","core/templates","core/notification","core/str","core/config"],function(a,b,c,d,e,f){return{init_tagindex_page:function(){a("body").delegate(".tagarea[data-ta] a[data-quickload=1]","click",function(d){d.preventDefault();var e=a(this),f=e.context.search.replace(/^\?/,""),g=e.closest(".tagarea[data-ta]"),h=f.split("&").reduce(function(a,b){var c=b.split("=");return a[c[0]]=decodeURIComponent(c[1]),a},{}),i=b.call([{methodname:"core_tag_get_tagindex",args:{tagindex:h}}],!0);a.when.apply(a,i).done(function(a){c.render("core_tag/index",a).done(function(a){g.replaceWith(a)})})})},init_manage_page:function(){var g=function(b){var c=b.closest("tr").get(0);if(c){var d=a(c).find("td.col-timemodified").get(0);e.get_string("now").done(function(b){a(d).html(b)})}};a(".tag-management-table").delegate(".tagisstandard","click",function(d){d.preventDefault();var e=a(this),f=e.attr("data-id"),h=e.attr("data-value"),i="1"===h?0:1,j=b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:f,isstandard:i}]}},{methodname:"core_tag_get_tags",args:{tags:[{id:f}]}}],!0);a.when.apply(a,j).done(function(a,b){void 0===a.warnings[0]&&void 0!==b.tags[0]&&c.render("core_tag/tagisstandard",b.tags[0]).done(function(a){g(e);var b=e.parent();e.replaceWith(a),b.find(".tagisstandard").get(0).focus()})})}),a(".tag-management-table").delegate(".tagflag","click",function(d){d.preventDefault();var e=a(this),f=e.attr("data-id"),h=e.attr("data-value"),i="0"===h?1:0,j=b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:f,flag:i}]}},{methodname:"core_tag_get_tags",args:{tags:[{id:f}]}}],!0);a.when.apply(a,j).done(function(b,d){if(void 0===b.warnings[0]&&void 0!==d.tags[0]){var f=e.closest("tr").get(0);f&&(d.tags[0].flag?a(f).addClass("flagged-tag"):a(f).removeClass("flagged-tag")),c.render("core_tag/tagflag",d.tags[0]).done(function(a){g(e);var b=e.parent();e.replaceWith(a),b.find(".tagflag").get(0).focus()})}})}),a(".tag-management-table").delegate("a.tagdelete","click",function(b){b.preventDefault();var c=a(this).attr("href");e.get_strings([{key:"delete"},{key:"confirmdeletetag",component:"tag"},{key:"yes"},{key:"no"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],function(){window.location.href=c})})}),a("#tag-management-delete").click(function(b){var c=a(this).closest("form").get(0),f=a(c).find("input[type=checkbox]:checked").length;return f?(b.preventDefault(),void e.get_strings([{key:"delete"},{key:"confirmdeletetags",component:"tag"},{key:"yes"},{key:"no"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],function(){c.submit()})})):!1}),a(".tag-management-table").delegate(".tagnameedit","click keypress",function(h){if("keypress"!==h.type||13===h.keyCode){h.stopImmediatePropagation(),h.preventDefault();var i=a(this),j=a(i.closest("td").get(0)),k=a(j.find("input").get(0)),l=i.attr("data-id"),m=function(f,h){var i=b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:f,rawname:h}]}},{methodname:"core_tag_get_tags",args:{tags:[{id:f}]}}],!0);a.when.apply(a,i).done(function(b,f){void 0!==b.warnings[0]?e.get_string("error").done(function(a){d.alert(a,b.warnings[0].message)}):void 0!==f.tags[0]&&c.render("core_tag/tagname",f.tags[0]).done(function(b){g(j),j.html(b),a(j.find(".tagnameedit").get(0)).focus()})})},n=function(){a(".tag-management-table td.tageditingon").each(function(){var b=a(this),c=a(b.find("input").get(0));c.off(),b.removeClass("tageditingon"),c.val(b.attr("data-value"))})};n(),j.addClass("tageditingon"),j.attr("data-value",k.val()),k.select(),k.on("keypress focusout",function(a){f.behatsiterunning&&"focusout"===a.type||("keypress"===a.type&&13===a.keyCode&&(m(l,k.val()),n()),("keypress"===a.type&&27===a.keyCode||"focusout"===a.type)&&n())})}})}}});
define(["jquery","core/ajax","core/templates","core/notification","core/str"],function(a,b,c,d,e){return{init_tagindex_page:function(){a("body").delegate(".tagarea[data-ta] a[data-quickload=1]","click",function(d){d.preventDefault();var e=a(this),f=e.context.search.replace(/^\?/,""),g=e.closest(".tagarea[data-ta]"),h=f.split("&").reduce(function(a,b){var c=b.split("=");return a[c[0]]=decodeURIComponent(c[1]),a},{}),i=b.call([{methodname:"core_tag_get_tagindex",args:{tagindex:h}}],!0);a.when.apply(a,i).done(function(a){c.render("core_tag/index",a).done(function(a){g.replaceWith(a)})})})},init_manage_page:function(){var f=function(b){var c=b.closest("tr").get(0);if(c){var d=a(c).find("td.col-timemodified").get(0);e.get_string("now").done(function(b){a(d).html(b)})}};a(".tag-management-table").delegate(".tagisstandard","click",function(d){d.preventDefault();var e=a(this),g=e.attr("data-id"),h=e.attr("data-value"),i="1"===h?0:1,j=b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:g,isstandard:i}]}},{methodname:"core_tag_get_tags",args:{tags:[{id:g}]}}],!0);a.when.apply(a,j).done(function(a,b){void 0===a.warnings[0]&&void 0!==b.tags[0]&&c.render("core_tag/tagisstandard",b.tags[0]).done(function(a){f(e);var b=e.parent();e.replaceWith(a),b.find(".tagisstandard").get(0).focus()})})}),a(".tag-management-table").delegate(".tagflag","click",function(d){d.preventDefault();var e=a(this),g=e.attr("data-id"),h=e.attr("data-value"),i="0"===h?1:0,j=b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:g,flag:i}]}},{methodname:"core_tag_get_tags",args:{tags:[{id:g}]}}],!0);a.when.apply(a,j).done(function(b,d){if(void 0===b.warnings[0]&&void 0!==d.tags[0]){var g=e.closest("tr").get(0);g&&(d.tags[0].flag?a(g).addClass("flagged-tag"):a(g).removeClass("flagged-tag")),c.render("core_tag/tagflag",d.tags[0]).done(function(a){f(e);var b=e.parent();e.replaceWith(a),b.find(".tagflag").get(0).focus()})}})}),a(".tag-management-table").delegate("a.tagdelete","click",function(b){b.preventDefault();var c=a(this).attr("href");e.get_strings([{key:"delete"},{key:"confirmdeletetag",component:"tag"},{key:"yes"},{key:"no"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],function(){window.location.href=c})})}),a("#tag-management-delete").click(function(b){var c=a(this).closest("form").get(0),f=a(c).find("input[type=checkbox]:checked").length;return f?(b.preventDefault(),void e.get_strings([{key:"delete"},{key:"confirmdeletetags",component:"tag"},{key:"yes"},{key:"no"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],function(){c.submit()})})):!1})}}});

View File

@ -0,0 +1,138 @@
// 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/>.
/**
* AJAX helper for the inline editing a value.
*
* This script is automatically included from template core/inplace_editable
* It registers a click-listener on [data-inplaceeditablelink] link (the "inplace edit" icon),
* then replaces the displayed value with an input field. On "Enter" it sends a request
* to web service core_update_inplace_editable, which invokes the specified callback.
* Any exception thrown by the web service (or callback) is displayed as an error popup.
*
* @module core/inplace_editable
* @package core
* @copyright 2016 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.1
*/
define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/config'],
function($, ajax, templates, notification, str, cfg) {
$('body').on('click keypress', '[data-inplaceeditable] [data-inplaceeditablelink]', function(e) {
if (e.type === 'keypress' && e.keyCode !== 13) {
return;
}
e.stopImmediatePropagation();
e.preventDefault();
var target = $(this),
mainelement = target.closest('[data-inplaceeditable]');
var update_value = function(mainelement, value) {
var promises = ajax.call([{
methodname: 'core_update_inplace_editable',
args: { itemid : mainelement.attr('data-itemid'),
component : mainelement.attr('data-component') ,
itemtype : mainelement.attr('data-itemtype') ,
value : value }
}], true);
$.when.apply($, promises)
.done( function(data) {
var oldvalue = mainelement.attr('data-value');
templates.render('core/inplace_editable', data).done(function(html, js) {
templates.replaceNode(mainelement, html, js);
mainelement.find('[data-inplaceeditablelink]').focus();
});
mainelement.trigger({type: 'updated', ajaxreturn: data, oldvalue: oldvalue});
}).fail(function(ex) {
var e = $.Event('updatefailed', { exception: ex, newvalue: value });
mainelement.trigger(e);
if (!e.isDefaultPrevented()) {
notification.exception(ex);
}
});
};
var turn_editing_off = function(el) {
var input = el.find('input');
input.off();
el.html(el.attr('data-oldcontent'));
el.removeAttr('data-oldcontent');
el.removeClass('inplaceeditingon');
};
var turn_editing_off_everywhere = function() {
$('span.inplaceeditable.inplaceeditingon').each(function() {
turn_editing_off($( this));
});
};
var unique_id = function(prefix, idlength) {
var uniqid = prefix;
for (var i = 0; i < idlength; i++) {
uniqid += String(Math.floor(Math.random() * 10));
}
// Make sure this ID is not already taken by an existing element.
if ($("#" + uniqid).length === 0) {
return uniqid;
}
return unique_id(prefix, idlength);
};
var turn_editing_on = function(el) {
el.addClass('inplaceeditingon');
el.attr('data-oldcontent', el.html());
str.get_string('edittitleinstructions').done(function(s) {
var instr = $('<span class="editinstructions">' + s + '</span>').
attr('id', unique_id('id_editinstructions_', 20)),
inputelement = $('<input type="text"/>').
attr('id', unique_id('id_inplacevalue_', 20)).
attr('value', el.attr('data-value')).
attr('aria-describedby', instr.attr('id')),
lbl = $('<label class="accesshide">' + mainelement.attr('data-editlabel') + '</label>').
attr('for', inputelement.attr('id'));
el.html('').append(instr).append(lbl).append(inputelement);
inputelement.focus();
inputelement.select();
inputelement.on('keyup keypress focusout', function(e) {
if (cfg.behatsiterunning && e.type === 'focusout') {
// Behat triggers focusout too often.
return;
}
if (e.type === 'keypress' && e.keyCode === 13) {
// We need 'keypress' event for Enter because keyup/keydown would catch Enter that was
// pressed in other fields.
update_value(el, inputelement.val());
turn_editing_off(el);
}
if ((e.type === 'keyup' && e.keyCode === 27) || e.type === 'focusout') {
// We need 'keyup' event for Escape because keypress does not work with Escape.
turn_editing_off(el);
}
});
});
};
// Turn editing on for the current element and register handler for Enter/Esc keys.
turn_editing_off_everywhere();
turn_editing_on(mainelement);
});
return {};
});

View File

@ -22,8 +22,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.0
*/
define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/config'],
function($, ajax, templates, notification, str, cfg) {
define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'],
function($, ajax, templates, notification, str) {
return /** @alias module:core/tag */ {
/**
@ -174,74 +174,6 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
}
);
});
// Edit tag name.
$('.tag-management-table').delegate('.tagnameedit', 'click keypress', function(e) {
if (e.type === 'keypress' && e.keyCode !== 13) {
return;
}
e.stopImmediatePropagation();
e.preventDefault();
var target = $(this),
tdelement = $( target.closest('td').get(0) ),
inputelement = $( tdelement.find('input').get(0) ),
tagid = target.attr('data-id');
var change_name = function(tagid, newname) {
var promises = ajax.call([{
methodname: 'core_tag_update_tags',
args: { tags : [ { id : tagid , rawname : newname } ] }
}, {
methodname: 'core_tag_get_tags',
args: { tags : [ { id : tagid } ] }
}], true);
$.when.apply($, promises)
.done( function(updateresult, data) {
if (updateresult.warnings[0] !== undefined) {
str.get_string('error').done(function(s) {
notification.alert(s, updateresult.warnings[0].message);
});
} else if (data.tags[0] !== undefined) {
templates.render('core_tag/tagname', data.tags[0]).done(function(html) {
update_modified(tdelement);
tdelement.html(html);
$(tdelement.find('.tagnameedit').get(0)).focus();
});
}
});
};
var turn_editing_off = function() {
$('.tag-management-table td.tageditingon').each(function() {
var td = $( this ),
input = $( td.find('input').get(0) );
input.off();
td.removeClass('tageditingon');
// Reset input value to the one that was there before editing.
input.val(td.attr('data-value'));
});
};
// Turn editing on for the current element and register handler for Enter/Esc keys.
turn_editing_off();
tdelement.addClass('tageditingon');
tdelement.attr('data-value', inputelement.val());
inputelement.select();
inputelement.on('keypress focusout', function(e) {
if (cfg.behatsiterunning && e.type === 'focusout') {
// Behat triggers focusout too often.
return;
}
if (e.type === 'keypress' && e.keyCode === 13) {
change_name(tagid, inputelement.val());
turn_editing_off();
}
if ((e.type === 'keypress' && e.keyCode === 27) || e.type === 'focusout') {
turn_editing_off();
}
});
});
}
};
});

View File

@ -107,8 +107,14 @@ class behat_form_field {
// dealing with a fgroup element.
$instance = $this->guess_type();
$instance->field->keyDown($char, $modifier);
$instance->field->keyPress($char, $modifier);
$instance->field->keyUp($char, $modifier);
try {
$instance->field->keyPress($char, $modifier);
$instance->field->keyUp($char, $modifier);
} catch (WebDriver\Exception $e) {
// If the JS handler attached to keydown or keypress destroys the element
// the later events may trigger errors because form element no longer exist
// or is not visible. Ignore such exceptions here.
}
}
/**

View File

@ -0,0 +1,146 @@
<?php
// 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/>.
/**
* Contains class \core\output\inplace_editable
*
* @package core
* @category output
* @copyright 2016 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\output;
use templatable;
use renderable;
use lang_string;
/**
* Class allowing to quick edit a title inline
*
* This class is used for displaying an element that can be in-place edited by the user. To display call:
* echo $OUTPUT->render($element);
* or
* echo $OUTPUT->render_from_template('core/inplace_editable', $element->export_for_template($OUTPUT));
*
* Template core/inplace_editable will automatically load javascript module with the same name
* core/inplace_editable. Javascript module registers a click-listener on edit link and
* then replaces the displayed value with an input field. On "Enter" it sends a request
* to web service core_update_inplace_editable, which invokes the callback from the component.
* Any exception thrown by the web service (or callback) is displayed as an error popup.
*
* Callback {$component}_inplace_editable($itemtype, $itemid, $newvalue) must be present in the lib.php file of
* the component or plugin. It must return instance of this class.
*
* @package core
* @category output
* @copyright 2016 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class inplace_editable implements templatable, renderable {
/**
* @var string component responsible for diplsying/updating
*/
protected $component = null;
/**
* @var string itemtype inside the component
*/
protected $itemtype = null;
/**
* @var int identifier of the editable element (usually database id)
*/
protected $itemid = null;
/**
* @var string value of the editable element as it is present in the database
*/
protected $value = null;
/**
* @var string value of the editable element as it should be displayed,
* must be formatted and may contain links or other html tags
*/
protected $displayvalue = null;
/**
* @var string label for the input element (for screenreaders)
*/
protected $editlabel = null;
/**
* @var string hint for the input element (for screenreaders)
*/
protected $edithint = null;
/**
* @var bool indicates if the current user is allowed to edit this element - set in constructor after permissions are checked
*/
protected $editable = false;
/**
* Constructor.
*
* @param string $component name of the component or plugin responsible for the updating of the value (must declare callback)
* @param string $itemtype type of the item inside the component - each component/plugin may implement multiple inplace-editable elements
* @param int $itemid identifier of the item that can be edited in-place
* @param bool $editable whether this value is editable (check capabilities and editing mode), if false, only "displayvalue"
* will be displayed without anything else
* @param string $displayvalue what needs to be displayed to the user, it must be cleaned, with applied filters (call
* {@link format_string()}). It may be wrapped in an html link, contain icons or other decorations
* @param string $value what needs to be edited - usually raw value from the database, it may contain multilang tags
* @param lang_string|string $edithint hint (title) that will be displayed under the edit link
* @param lang_string|string $editlabel label for the input element in the editing mode (for screenreaders)
*/
public function __construct($component, $itemtype, $itemid, $editable,
$displayvalue, $value = null, $edithint = null, $editlabel = null) {
$this->component = $component;
$this->itemtype = $itemtype;
$this->itemid = $itemid;
$this->editable = $editable;
$this->displayvalue = $displayvalue;
$this->value = $value;
$this->edithint = $edithint;
$this->editlabel = $editlabel;
}
/**
* Export this data so it can be used as the context for a mustache template (core/inplace_editable).
*
* @param renderer_base $output typically, the renderer that's calling this function
* @return array data context for a mustache template
*/
public function export_for_template(\renderer_base $output) {
if (!$this->editable) {
return array(
'displayvalue' => (string)$this->displayvalue
);
}
return array(
'component' => $this->component,
'itemtype' => $this->itemtype,
'itemid' => $this->itemid,
'displayvalue' => (string)$this->displayvalue,
'value' => (string)$this->value,
'edithint' => (string)$this->edithint,
'editlabel' => (string)$this->editlabel,
);
}
}

View File

@ -1060,6 +1060,15 @@ $functions = array(
'ajax' => true,
),
'core_update_inplace_editable' => array(
'classname' => 'core_external',
'methodname' => 'update_inplace_editable',
'classpath' => 'lib/external/externallib.php',
'description' => 'Generic service to update title',
'type' => 'write',
'loginrequired' => true,
'ajax' => true
),
// === Calendar related functions ===

View File

@ -346,4 +346,65 @@ class core_external extends external_api {
)
);
}
/**
* Parameters for function update_inplace_editable()
*
* @since Moodle 3.1
* @return external_function_parameters
*/
public static function update_inplace_editable_parameters() {
return new external_function_parameters(
array(
'component' => new external_value(PARAM_COMPONENT, 'component responsible for the update', VALUE_REQUIRED),
'itemtype' => new external_value(PARAM_NOTAGS, 'type of the updated item inside the component', VALUE_REQUIRED),
'itemid' => new external_value(PARAM_INT, 'identifier of the updated item', VALUE_REQUIRED),
'value' => new external_value(PARAM_RAW, 'new value', VALUE_REQUIRED),
));
}
/**
* Update any component's editable value assuming that component implements necessary callback
*
* @since Moodle 3.1
* @param string $component
* @param string $itemtype
* @param string $itemid
* @param string $value
*/
public static function update_inplace_editable($component, $itemtype, $itemid, $value) {
global $PAGE;
// Validate and normalize parameters.
$params = self::validate_parameters(self::update_inplace_editable_parameters(),
array('component' => $component, 'itemtype' => $itemtype, 'itemid' => $itemid, 'value' => $value));
if (!$functionname = component_callback_exists($component, 'inplace_editable')) {
throw new \moodle_exception('inplaceeditableerror');
}
$tmpl = component_callback($params['component'], 'inplace_editable',
array($params['itemtype'], $params['itemid'], $params['value']));
if (!$tmpl || !($tmpl instanceof \core\output\inplace_editable)) {
throw new \moodle_exception('inplaceeditableerror');
}
return $tmpl->export_for_template($PAGE->get_renderer('core'));
}
/**
* Return structure for update_inplace_editable()
*
* @since Moodle 3.1
* @return external_description
*/
public static function update_inplace_editable_returns() {
return new external_single_structure(
array(
'displayvalue' => new external_value(PARAM_RAW, 'display value (may contain link or other html tags)'),
'component' => new external_value(PARAM_NOTAGS, 'component responsible for the update', VALUE_OPTIONAL),
'itemtype' => new external_value(PARAM_NOTAGS, 'itemtype', VALUE_OPTIONAL),
'value' => new external_value(PARAM_RAW, 'value of the item as it is stored', VALUE_OPTIONAL),
'itemid' => new external_value(PARAM_RAW, 'identifier of the updated item', VALUE_OPTIONAL),
'edithint' => new external_value(PARAM_NOTAGS, 'hint for editing element', VALUE_OPTIONAL),
'editlabel' => new external_value(PARAM_NOTAGS, 'label for editing element', VALUE_OPTIONAL),
)
);
}
}

View File

@ -135,4 +135,27 @@ class core_external_testcase extends externallib_advanced_testcase {
$this->assertSame($string['string'], $wsstrings[$string['stringid']]);
}
}
/**
* Test update_inplace_editable()
*/
public function test_update_inplace_editable() {
$this->resetAfterTest(true);
// Call service for component that does not have inplace_editable callback.
try {
core_external::update_inplace_editable('tool_log', 'itemtype', 1, 'newvalue');
$this->fail('Exception expected');
} catch (moodle_exception $e) {
$this->assertEquals('Error calling update processor', $e->getMessage());
}
// This is a very basic test for the return value of the external function.
// More detailed test for tag updating can be found in core_tag component.
$this->setAdminUser();
$tag = $this->getDataGenerator()->create_tag();
$res = core_external::update_inplace_editable('core_tag', 'tagname', $tag->id, 'new tag name');
$res = external_api::clean_returnvalue(core_external::update_inplace_editable_returns(), $res);
$this->assertEquals('new tag name', $res['value']);
}
}

View File

@ -4120,6 +4120,16 @@ EOD;
$list = new \core_tag\output\taglist($tags, $label, $classes, $limit, $pagecontext);
return $this->render_from_template('core_tag/taglist', $list->export_for_template($this));
}
/**
* Renders element for inline editing of any value
*
* @param \core\output\inplace_editable $element
* @return string
*/
public function render_inplace_editable(\core\output\inplace_editable $element) {
return $this->render_from_template('core/inplace_editable', $element->export_for_template($this));
}
}
/**

View File

@ -0,0 +1,60 @@
{{!
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/>.
}}
{{!
@template core/inplace_editable
Displays the value that can be edited inline.
Classes required for JS:
* none
Data attributes required for JS:
* data-inplaceeditable
* data-inplaceeditablelink
* data-identifier
* data-callback
Context variables required for this template:
* none
Example context (json):
{
"displayvalue" : "<a href=\"#\">Moodle</a>",
"value" : "Moodle",
"itemid" : "1",
"component" : "core_tag",
"itemtype" : "tagname",
"edithint" : "Edit this",
"editlabel" : "New name for this"
}
}}
{{#component}}
<span class="inplaceeditable" data-inplaceeditable="1" data-component="{{component}}" data-itemtype="{{itemtype}}" data-itemid="{{itemid}}"
data-value="{{value}}" data-editlabel="{{editlabel}}">
{{{displayvalue}}}
<a href="#" class="quickeditlink visibleifjs" data-inplaceeditablelink="1" title="{{edithint}}">
{{#pix}}t/editstring,core,{{edithint}}{{/pix}}
</a>
</span>
{{#js}}
require(['core/inplace_editable']);
{{/js}}
{{/component}}
{{^component}}
{{{displayvalue}}}
{{/component}}

View File

@ -179,8 +179,8 @@ class core_tag_manage_table extends table_sql {
*/
public function col_name($tag) {
global $OUTPUT;
$tagoutput = new core_tag\output\tag($tag);
return $OUTPUT->render_from_template('core_tag/tagname', $tagoutput->export_for_template($OUTPUT));
$tagoutput = new core_tag\output\tagname($tag);
return $OUTPUT->render_from_template('core/inplace_editable', $tagoutput->export_for_template($OUTPUT));
}
/**

View File

@ -0,0 +1,55 @@
<?php
// 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/>.
/**
* Contains class core_tag\output\tagname
*
* @package core_tag
* @copyright 2016 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_tag\output;
use context_system;
use lang_string;
use html_writer;
use core_tag_tag;
/**
* Class to preapare a tag name for display.
*
* @package core_tag
* @copyright 2016 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tagname extends \core\output\inplace_editable {
/**
* Constructor.
*
* @param stdClass|core_tag $tag
*/
public function __construct($tag) {
$editable = has_capability('moodle/tag:manage', context_system::instance());
$edithint = new lang_string('editname', 'core_tag');
$editlabel = new lang_string('newnamefor', 'core_tag', $tag->rawname);
$value = $tag->rawname;
$displayvalue = html_writer::link(core_tag_tag::make_url($tag->tagcollid, $tag->rawname),
core_tag_tag::make_display_name($tag));
parent::__construct('core_tag', 'tagname', $tag->id, $editable, $displayvalue, $value, $edithint, $editlabel);
}
}

View File

@ -892,14 +892,12 @@ class core_tag_tag {
$data['rawname'] = clean_param($data['rawname'], PARAM_TAG);
$name = core_text::strtolower($data['rawname']);
if (!$name) {
if (!$name || $data['rawname'] === $this->rawname) {
unset($data['rawname']);
} else if ($existing = static::get_by_name($this->tagcollid, $name, 'id')) {
// Prevent the rename if a tag with that name already exists.
if ($existing->id != $this->id) {
debugging('New tag name already exists, you should check it before calling core_tag_tag::update()',
DEBUG_DEVELOPER);
unset($data['rawname']);
throw new moodle_exception('namesalreadybeeingused', 'core_tag');
}
}
if (isset($data['rawname'])) {

View File

@ -40,3 +40,20 @@ function tag_page_type_list($pagetype, $parentcontext, $currentcontext) {
'tag-manage'=>get_string('page-tag-manage', 'tag')
);
}
/**
* Implements callback inplace_editable() allowing to edit values in-place
*
* @param string $itemtype
* @param int $itemid
* @param mixed $newvalue
* @return \core\output\inplace_editable
*/
function core_tag_inplace_editable($itemtype, $itemid, $newvalue) {
if ($itemtype === 'tagname') {
require_capability('moodle/tag:manage', context_system::instance());
$tag = core_tag_tag::get($itemid, '*', MUST_EXIST);
$tag->update(array('rawname' => $newvalue));
return new \core_tag\output\tagname($tag);
}
}

View File

@ -1,57 +0,0 @@
{{!
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/>.
}}
{{!
@template core_tag/tagname
Displays the tag name in the tag management table.
For the Javascript handlers it is important that this template contains
an element with .tagnameedit class (to switch on editing) and also
an input element.
When element with class .tagnameedit is clicked, JS will add class
.tageditingon to the container td tag.
Classes required for JS:
* tagnameedit
Data attributes required for JS:
* data-id
Context variables required for this template:
* none
Example context (json):
{
"id" : "1",
"name" : "moodle",
"rawname" : "Moodle",
"viewurl" : "http://moodle.com/"
}
}}
<span class="displaytagname">
<a href="{{viewurl}}" data-id="{{id}}">
{{rawname}}
</a>
<a href="#" class="tagnameedit visibleifjs" data-id="{{id}}" title="{{#str}}editname,core_tag{{/str}}">
{{#pix}}t/editstring,core,{{#str}}editname,core_tag{{/str}}{{/pix}}
</a>
</span>
<span class="edittagname">
<span class="editinstructions" id="id_editinstructions_{{id}}">{{#str}}edittitleinstructions,core{{/str}}</span>
<label class="accesshide" for="tagquickname{{id}}">{{#str}}newnamefor,core_tag,{{rawname}}{{/str}}</label>
<input id="tagquickname{{id}}" type="text" value="{{rawname}}" data-id="{{id}}" aria-describedby="id_editinstructions_{{id}}"/>
</span>

View File

@ -153,7 +153,7 @@ Feature: Users can edit tags to add description or rename
And I set the field "New name for tag Cat" to "Kitten"
And I press key "13" in the field "New name for tag Cat"
Then I should not see "Cat"
And "New name for tag" "field" should not be visible
And "New name for tag" "field" should not exist
And I wait until "Kitten" "link" exists
And I follow "Default collection"
And I should see "Kitten"
@ -163,8 +163,8 @@ Feature: Users can edit tags to add description or rename
And I set the field "New name for tag Turtle" to "DOG"
And I press key "13" in the field "New name for tag Turtle"
And I should see "Tag names already being used"
And I press "Ok"
And "New name for tag" "field" should not be visible
And I press "Close"
And "New name for tag" "field" should not exist
And I should see "Turtle"
And I should see "Dog"
And I should not see "DOG"
@ -176,7 +176,7 @@ Feature: Users can edit tags to add description or rename
And I click on "Edit tag name" "link" in the "Dog" "table_row"
And I set the field "New name for tag Dog" to "Penguin"
And I press key "27" in the field "New name for tag Dog"
And "New name for tag" "field" should not be visible
And "New name for tag" "field" should not exist
And I should see "Turtle"
And I should not see "Penguin"
And I follow "Default collection"

View File

@ -147,4 +147,38 @@ class core_tag_external_testcase extends externallib_advanced_testcase {
$this->assertEquals($tag->id, $result['warnings'][0]['item']);
$this->assertEquals('namesalreadybeeingused', $result['warnings'][0]['warningcode']);
}
/**
* Test update_inplace_editable()
*/
public function test_update_inplace_editable() {
global $CFG, $DB, $PAGE;
require_once($CFG->dirroot . '/lib/external/externallib.php');
$this->resetAfterTest(true);
$tag = $this->getDataGenerator()->create_tag();
// Call service for core_tag component without necessary permissions.
try {
core_external::update_inplace_editable('core_tag', 'tagname', $tag->id, 'new tag name');
$this->fail('Exception expected');
} catch (moodle_exception $e) {
$this->assertEquals('Sorry, but you do not currently have permissions to do that (Manage all tags)',
$e->getMessage());
}
// Change to admin user and make sure that tag name can be updated using web service update_inplace_editable().
$this->setAdminUser();
$res = core_external::update_inplace_editable('core_tag', 'tagname', $tag->id, 'New tag name');
$res = external_api::clean_returnvalue(core_external::update_inplace_editable_returns(), $res);
$this->assertEquals('New tag name', $res['value']);
$this->assertEquals('New tag name', $DB->get_field('tag', 'rawname', array('id' => $tag->id)));
// Call callback core_tag_inplace_editable() directly.
$tmpl = component_callback('core_tag', 'inplace_editable', array('tagname', $tag->id, 'Rename me again'));
$this->assertInstanceOf('core\output\inplace_editable', $tmpl);
$res = $tmpl->export_for_template($PAGE->get_renderer('core'));
$this->assertEquals('Rename me again', $res['value']);
$this->assertEquals('Rename me again', $DB->get_field('tag', 'rawname', array('id' => $tag->id)));
}
}

View File

@ -751,14 +751,7 @@ span.flagged-tag a,
tr.flagged-tag a {color:#FF0000;}
.tag-management-table td,
.tag-management-table th {vertical-align: middle;padding: 4px;}
.tag-management-table tr td.tageditingon .displaytagname,
.tag-management-table td .edittagname {display: none;}
.tag-management-table tr td.tageditingon .edittagname {display: inherit; position: relative;}
.tag-management-table tr td.tageditingon .edittagname .editinstructions {margin-right: -300px; margin-left: 0;}
.tag-management-table tr td .tagnameedit img {opacity: 0.2;}
.tag-management-table tr:hover td .tagnameedit img,
.tag-management-table tr td .tagnameedit:focus img {opacity: 1;}
.tag-management-table tr:hover td.tageditingon .tagnameedit img {opacity: 0.2;}
.tag-management-table span.inplaceeditable.inplaceeditingon input {width: 150px;}
.tag_feed .media, .tag_feed .media-body {overflow: hidden;}
.tag_feed.media-list .media .itemimage {float: left;}
.dir-rtl .tag_feed.media-list .media .itemimage {float: right;}
@ -2330,3 +2323,30 @@ body.lockscroll {
.ie10 .yui3-calendar-header-label {
display: inline-block;
}
span.inplaceeditable.inplaceeditingon {
position: relative;
}
span.inplaceeditable.inplaceeditingon span.editinstructions {
margin-top: -30px;
font-weight: normal;
margin-right: -300px;
margin-left: 0;
}
.dir-rtl span.inplaceeditable.inplaceeditingon span.editinstructions {
margin-left: -300px;
margin-right: 0;
}
span.inplaceeditable .quickeditlink img {
opacity: 0.2;
}
span.inplaceeditable:hover .quickeditlink img,
span.inplaceeditable .quickeditlink:focus img {
opacity: 1;
}
span.inplaceeditable.inplaceeditingon input {
width: 330px;
height: 16px;
vertical-align: text-bottom;
margin-bottom: 0;
}

View File

@ -826,27 +826,8 @@ tr.flagged-tag a {
vertical-align: middle;
padding: 4px;
}
.tag-management-table tr td.tageditingon .displaytagname,
.tag-management-table td .edittagname {
display: none;
}
.tag-management-table tr td.tageditingon .edittagname {
display: inherit;
position: relative;
}
.tag-management-table tr td.tageditingon .edittagname .editinstructions {
margin-right: -300px;
margin-left: 0;
}
.tag-management-table tr td .tagnameedit img {
opacity: 0.2;
}
.tag-management-table tr:hover td .tagnameedit img,
.tag-management-table tr td .tagnameedit:focus img {
opacity: 1;
}
.tag-management-table tr:hover td.tageditingon .tagnameedit img {
opacity: 0.2;
.tag-management-table span.inplaceeditable.inplaceeditingon input {
width: 150px;
}
.path-tag .tag-relatedtags {
padding-top: 10px;
@ -2445,3 +2426,33 @@ dd:after {
.nav-tabs > .active > a[href]:focus {
cursor: pointer;
}
span.inplaceeditable.inplaceeditingon {
position: relative;
}
span.inplaceeditable.inplaceeditingon span.editinstructions {
margin-top: -30px;
font-weight: normal;
margin-right: -300px;
margin-left: 0;
}
.dir-rtl span.inplaceeditable.inplaceeditingon span.editinstructions {
margin-left: -300px;
margin-right: 0;
}
span.inplaceeditable .quickeditlink img {
opacity: 0.2;
}
span.inplaceeditable:hover .quickeditlink img,
span.inplaceeditable .quickeditlink:focus img {
opacity: 1;
}
span.inplaceeditable.inplaceeditingon input {
width: 330px;
height: 16px;
vertical-align: text-bottom;
margin-bottom: 0;
}
h3.sectionname span.inplaceeditable.inplaceeditingon span.editinstructions {
margin-top: -20px;
}

File diff suppressed because one or more lines are too long

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2016021100.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2016021500.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.