Merge branch 'MDL-64821-master-2' of https://github.com/ryanwyllie/moodle

This commit is contained in:
Jake Dallimore 2019-09-26 10:45:44 +08:00
commit c3122dfcf5
50 changed files with 2204 additions and 183 deletions

View file

@ -147,7 +147,7 @@ class stored_file_exporter extends \core\external\exporter {
$filenameshort .= substr($filename, -4); $filenameshort .= substr($filename, -4);
} }
$icon = $this->file->is_directory() ? file_folder_icon() : file_file_icon($this->file); $icon = $this->file->is_directory() ? file_folder_icon(128) : file_file_icon($this->file, 128);
$url = moodle_url::make_pluginfile_url( $url = moodle_url::make_pluginfile_url(
$this->file->get_contextid(), $this->file->get_contextid(),

View file

@ -2241,6 +2241,7 @@ class core_renderer extends renderer_base {
if ($rating->user_can_view_aggregate()) { if ($rating->user_can_view_aggregate()) {
$aggregatelabel = $ratingmanager->get_aggregate_label($rating->settings->aggregationmethod); $aggregatelabel = $ratingmanager->get_aggregate_label($rating->settings->aggregationmethod);
$aggregatelabel = html_writer::tag('span', $aggregatelabel, array('class'=>'rating-aggregate-label'));
$aggregatestr = $rating->get_aggregate_string(); $aggregatestr = $rating->get_aggregate_string();
$aggregatehtml = html_writer::tag('span', $aggregatestr, array('id' => 'ratingaggregate'.$rating->itemid, 'class' => 'ratingaggregate')).' '; $aggregatehtml = html_writer::tag('span', $aggregatestr, array('id' => 'ratingaggregate'.$rating->itemid, 'class' => 'ratingaggregate')).' ';
@ -2251,17 +2252,16 @@ class core_renderer extends renderer_base {
} }
$aggregatehtml .= html_writer::tag('span', $countstr, array('id'=>"ratingcount{$rating->itemid}", 'class' => 'ratingcount')).' '; $aggregatehtml .= html_writer::tag('span', $countstr, array('id'=>"ratingcount{$rating->itemid}", 'class' => 'ratingcount')).' ';
$ratinghtml .= html_writer::tag('span', $aggregatelabel, array('class'=>'rating-aggregate-label'));
if ($rating->settings->permissions->viewall && $rating->settings->pluginpermissions->viewall) { if ($rating->settings->permissions->viewall && $rating->settings->pluginpermissions->viewall) {
$nonpopuplink = $rating->get_view_ratings_url(); $nonpopuplink = $rating->get_view_ratings_url();
$popuplink = $rating->get_view_ratings_url(true); $popuplink = $rating->get_view_ratings_url(true);
$action = new popup_action('click', $popuplink, 'ratings', array('height' => 400, 'width' => 600)); $action = new popup_action('click', $popuplink, 'ratings', array('height' => 400, 'width' => 600));
$ratinghtml .= $this->action_link($nonpopuplink, $aggregatehtml, $action); $aggregatehtml = $this->action_link($nonpopuplink, $aggregatehtml, $action);
} else {
$ratinghtml .= $aggregatehtml;
} }
$ratinghtml .= html_writer::tag('span', $aggregatelabel . $aggregatehtml, array('class' => 'rating-aggregate-container'));
} }
$formstart = null; $formstart = null;

View file

@ -1,2 +1,2 @@
define ("mod_forum/discussion",["jquery","core/custom_interaction_events","mod_forum/selectors"],function(a,b,c){var d=function(a){var b=a.prev(c.post.post);if(b.length){var d=b.find(c.post.post).last();if(d.length){d.focus()}else{b.focus()}}else{a.parents(c.post.post).first().focus()}},e=function(b){var d=b.find(c.post.post).first();if(d.length){d.focus()}else{var e=b.next(c.post.post);if(e.length){e.focus()}else{b.parents().toArray().forEach(function(b){var d=a(b).next(c.post.post);if(d.length){d.focus()}})}}},f=function(b){var d=a(b).closest(c.post.inpageReplyContent);return d.length?!0:!1},g=function(g){var h=g.find(c.post.post);h.each(function(b,d){var e=a(d).find(c.post.action),f=e.first();e.attr("tabindex","-1");f.attr("tabindex",0)});b.define(g,[b.events.up,b.events.down,b.events.next,b.events.previous,b.events.home,b.events.end]);g.on(b.events.up,function(b,e){var h=document.activeElement;if(f(h)){return}var i=a(h).closest(c.post.post);if(i.length){d(i)}else{g.find(c.post.post).first().focus()}e.originalEvent.preventDefault()});g.on(b.events.down,function(b,d){var h=document.activeElement;if(f(h)){return}var i=a(h).closest(c.post.post);if(i.length){e(i)}else{g.find(c.post.post).first().focus()}d.originalEvent.preventDefault()});g.on(b.events.home,function(a,b){if(f(document.activeElement)){return}g.find(c.post.post).first().focus();b.originalEvent.preventDefault()});g.on(b.events.end,function(a,b){if(f(document.activeElement)){return}g.find(c.post.post).last().focus();b.originalEvent.preventDefault()});g.on(b.events.next,c.post.action,function(b,d){var e=a(b.target),f=e.closest(c.post.actionsContainer),g=f.find(c.post.action),h=e.next(c.post.action);g.attr("tabindex","-1");if(!h.length){h=g.first()}h.attr("tabindex",0);h.focus();d.originalEvent.preventDefault()});g.on(b.events.previous,c.post.action,function(b,d){var e=a(b.target),f=e.closest(c.post.actionsContainer),g=f.find(c.post.action),h=e.prev(c.post.action);g.attr("tabindex","-1");if(!h.length){h=g.last()}h.attr("tabindex",0);h.focus();d.originalEvent.preventDefault()});g.on(b.events.home,c.post.action,function(b,d){var e=a(b.target),f=e.closest(c.post.actionsContainer),g=f.find(c.post.action),h=g.first();g.attr("tabindex","-1");h.attr("tabindex",0);h.focus();b.stopPropagation();d.originalEvent.preventDefault()});g.on(b.events.end,c.post.action,function(b,d){var e=a(b.target),f=e.closest(c.post.actionsContainer),g=f.find(c.post.action),h=g.last();g.attr("tabindex","-1");h.attr("tabindex",0);h.focus();b.stopPropagation();d.originalEvent.preventDefault()})};return{init:function init(a){g(a)}}}); define ("mod_forum/discussion",["jquery","core/custom_interaction_events","mod_forum/selectors"],function(a,b,c){var d=function(a){var b=a.prev(c.post.post);if(b.length){var d=b.find(c.post.post).last();if(d.length){d.focus()}else{b.focus()}}else{a.parents(c.post.post).first().focus()}},e=function(b){var d=b.find(c.post.post).first();if(d.length){d.focus()}else{var e=b.next(c.post.post);if(e.length){e.focus()}else{for(var f=b.parents(c.post.post).toArray(),g=0,h;g<f.length;g++){h=a(f[g]).next(c.post.post);if(h.length){h.focus();break}}}}},f=function(b){var d=a(b).closest(c.post.inpageReplyContent);return d.length?!0:!1},g=function(g){var h=g.find(c.post.post);h.each(function(b,d){var e=a(d).find(c.post.action),f=e.first();e.attr("tabindex","-1");f.attr("tabindex",0)});b.define(g,[b.events.up,b.events.down,b.events.next,b.events.previous,b.events.home,b.events.end]);g.on(b.events.up,function(b,e){var h=document.activeElement;if(f(h)){return}var i=a(h).closest(c.post.post);if(i.length){d(i)}else{g.find(c.post.post).first().focus()}e.originalEvent.preventDefault()});g.on(b.events.down,function(b,d){var h=document.activeElement;if(f(h)){return}var i=a(h).closest(c.post.post);if(i.length){e(i)}else{g.find(c.post.post).first().focus()}d.originalEvent.preventDefault()});g.on(b.events.home,function(a,b){if(f(document.activeElement)){return}g.find(c.post.post).first().focus();b.originalEvent.preventDefault()});g.on(b.events.end,function(a,b){if(f(document.activeElement)){return}g.find(c.post.post).last().focus();b.originalEvent.preventDefault()});g.on(b.events.next,c.post.action,function(b,d){var e=a(b.target),f=e.closest(c.post.actionsContainer),g=f.find(c.post.action),h=e.next(c.post.action);g.attr("tabindex","-1");if(!h.length){h=g.first()}h.attr("tabindex",0);h.focus();d.originalEvent.preventDefault()});g.on(b.events.previous,c.post.action,function(b,d){var e=a(b.target),f=e.closest(c.post.actionsContainer),g=f.find(c.post.action),h=e.prev(c.post.action);g.attr("tabindex","-1");if(!h.length){h=g.last()}h.attr("tabindex",0);h.focus();d.originalEvent.preventDefault()});g.on(b.events.home,c.post.action,function(b,d){var e=a(b.target),f=e.closest(c.post.actionsContainer),g=f.find(c.post.action),h=g.first();g.attr("tabindex","-1");h.attr("tabindex",0);h.focus();b.stopPropagation();d.originalEvent.preventDefault()});g.on(b.events.end,c.post.action,function(b,d){var e=a(b.target),f=e.closest(c.post.actionsContainer),g=f.find(c.post.action),h=g.last();g.attr("tabindex","-1");h.attr("tabindex",0);h.focus();b.stopPropagation();d.originalEvent.preventDefault()})};return{init:function init(a){g(a)}}});
//# sourceMappingURL=discussion.min.js.map //# sourceMappingURL=discussion.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
define ("mod_forum/inpage_reply",["jquery","core/templates","core/notification","mod_forum/repository","mod_forum/selectors"],function(a,b,c,d,f){var g={THREADED:2,NESTED:3,FLAT_OLDEST_FIRST:1,FLAT_NEWEST_FIRST:-1},h={MOODLE:0},i=function(a){var b=a.find(f.post.inpageSubmitBtnText),c=a.find(f.post.loadingIconContainer),d=a.outerWidth();a.css("width",d);b.addClass("hidden");c.removeClass("hidden")},j=function(a){var b=a.find(f.post.inpageSubmitBtnText),c=a.find(f.post.loadingIconContainer);a.css("width","");b.removeClass("hidden");c.addClass("hidden")},k=function(k){k.on("click",f.post.inpageSubmitBtn,function(l){l.preventDefault();var e=a(l.currentTarget),m=e.parent().find(f.post.inpageReplyButton),n=e.parents(f.post.inpageReplyForm).get(0),o=n.elements.post.value.trim(),p=h.MOODLE,q=n.elements.reply.value,r=n.elements.subject.value,s=e.parents(f.post.forumContent),t=n.elements.privatereply!=void 0?n.elements.privatereply.checked:!1,u=parseInt(k.find(f.post.modeSelect).get(0).value),v;if(o.length){i(e);m.prop("disabled",!0);d.addDiscussionPost(q,r,o,p,t,!0).then(function(a){var b=a.messages.reduce(function(a,b){if("success"==b.type){a+="<p>"+b.message+"</p>"}return a},"");c.addNotification({message:b,type:"success"});return a}).then(function(a){n.reset();var c=a.post;v=c.id;switch(u){case g.THREADED:return b.render("mod_forum/forum_discussion_threaded_post",c);case g.NESTED:return b.render("mod_forum/forum_discussion_nested_post",c);default:return b.render("mod_forum/forum_discussion_post",c);}}).then(function(a,c){var d;if(u==g.FLAT_OLDEST_FIRST||u==g.FLAT_NEWEST_FIRST){d=s.parents(f.post.repliesContainer).children().get(0)}if(d==void 0){d=s.siblings(f.post.repliesContainer).children().get(0)}if(u==g.FLAT_NEWEST_FIRST){return b.prependNodeContents(d,a,c)}else{return b.appendNodeContents(d,a,c)}}).then(function(){j(e);m.prop("disabled",!1);return s.find(f.post.inpageReplyContent).hide()}).then(function(){location.href="#p"+v}).catch(function(a){j(e);m.prop("disabled",!1);return c.exception(a)})}})};return{init:function init(a){k(a)},CONTENT_FORMATS:h}}); define ("mod_forum/inpage_reply",["jquery","core/templates","core/notification","mod_forum/repository","mod_forum/selectors"],function(a,b,c,d,f){var g={MODERN:4,THREADED:2,NESTED:3,FLAT_OLDEST_FIRST:1,FLAT_NEWEST_FIRST:-1},h={POST_CREATED:"mod_forum-post-created"},i={MOODLE:0},j=function(a){var b=a.find(f.post.inpageSubmitBtnText),c=a.find(f.post.loadingIconContainer),d=a.outerWidth();a.css("width",d);b.addClass("hidden");c.removeClass("hidden")},k=function(a){var b=a.find(f.post.inpageSubmitBtnText),c=a.find(f.post.loadingIconContainer);a.css("width","");b.removeClass("hidden");c.addClass("hidden")},l=function(l){l.on("click",f.post.inpageSubmitBtn,function(m){m.preventDefault();var e=a(m.currentTarget),n=e.parent().find(f.post.inpageReplyButton),o=e.parents(f.post.inpageReplyForm).get(0),p=o.elements.post.value.trim(),q=i.MOODLE,r=o.elements.reply.value,s=o.elements.subject.value,t=e.closest(f.post.post),u=o.elements.privatereply!=void 0?o.elements.privatereply.checked:!1,v=l.find(f.post.modeSelect),w=v.length?parseInt(v.get(0).value):null,x;if(p.length){j(e);n.prop("disabled",!0);d.addDiscussionPost(r,s,p,q,u,!0).then(function(a){var b=a.messages.reduce(function(a,b){if("success"==b.type){a+="<p>"+b.message+"</p>"}return a},"");c.addNotification({message:b,type:"success"});return a}).then(function(a){o.reset();var c=a.post;x=c.id;switch(w){case g.MODERN:var d=c.capabilities;c.showactionmenu=d.controlreadstatus||d.edit||d.split||d.delete||d.export||c.urls.viewparent;return b.render("mod_forum/forum_discussion_modern_post_reply",c);case g.THREADED:return b.render("mod_forum/forum_discussion_threaded_post",c);case g.NESTED:return b.render("mod_forum/forum_discussion_nested_post",c);default:return b.render("mod_forum/forum_discussion_post",c);}}).then(function(a,c){var d=t.find(f.post.repliesContainer).first();if(w==g.FLAT_NEWEST_FIRST){return b.prependNodeContents(d,a,c)}else{return b.appendNodeContents(d,a,c)}}).then(function(){e.trigger(h.POST_CREATED,x);k(e);n.prop("disabled",!1);return t.find(f.post.inpageReplyContent).hide()}).then(function(){location.href="#p"+x}).catch(function(a){k(e);n.prop("disabled",!1);return c.exception(a)})}})};return{init:function init(a){l(a)},CONTENT_FORMATS:i,EVENTS:h}});
//# sourceMappingURL=inpage_reply.min.js.map //# sourceMappingURL=inpage_reply.min.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
define ("mod_forum/selectors",[],function(){return{subscription:{toggle:"[data-type='subscription-toggle'][data-action='toggle']"},summary:{actions:"[data-container='discussion-summary-actions']"},post:{post:"[data-region=\"post\"]",action:"[data-region=\"post-action\"]",actionsContainer:"[data-region=\"post-actions-container\"]",forumCoreContent:"[data-region-content='forum-post-core']",forumContent:"[data-content='forum-post']",forumSubject:"[data-region-content='forum-post-core-subject']",inpageReplyButton:"button",inpageReplyLink:"[data-action='collapsible-link']",inpageReplyContent:"[data-content='inpage-reply-content']",inpageReplyForm:"form[data-content='inpage-reply-form']",inpageSubmitBtn:"[data-action='forum-inpage-submit']",inpageSubmitBtnText:"[data-region='submit-text']",loadingIconContainer:"[data-region='loading-icon-container']",repliesContainer:"[data-region='replies-container']",modeSelect:"select[name='mode']"},lock:{toggle:"[data-action='toggle'][data-type='lock-toggle']",icon:"[data-region='locked-icon']"},favourite:{toggle:"[data-type='favorite-toggle'][data-action='toggle']"},pin:{toggle:"[data-type='pin-toggle'][data-action='toggle']"}}}); define ("mod_forum/selectors",[],function(){return{subscription:{toggle:"[data-type='subscription-toggle'][data-action='toggle']"},summary:{actions:"[data-container='discussion-summary-actions']"},post:{post:"[data-region=\"post\"]",action:"[data-region=\"post-action\"]",actionsContainer:"[data-region=\"post-actions-container\"]",authorName:"[data-region=\"author-name\"]",forumCoreContent:"[data-region-content='forum-post-core']",forumContent:"[data-content='forum-post']",forumSubject:"[data-region-content='forum-post-core-subject']",inpageReplyButton:"button",inpageReplyLink:"[data-action='collapsible-link']",inpageReplyCancelButton:"[data-action='cancel-inpage-reply']",inpageReplyCreateButton:"[data-action='create-inpage-reply']",inpageReplyContainer:"[data-region=\"inpage-reply-container\"]",inpageReplyContent:"[data-content='inpage-reply-content']",inpageReplyForm:"form[data-content='inpage-reply-form']",inpageSubmitBtn:"[data-action='forum-inpage-submit']",inpageSubmitBtnText:"[data-region='submit-text']",loadingIconContainer:"[data-region='loading-icon-container']",repliesContainer:"[data-region='replies-container']",replyCount:"[data-region=\"reply-count\"]",modeSelect:"select[name='mode']",showReplies:"[data-action=\"show-replies\"]",hideReplies:"[data-action=\"hide-replies\"]",repliesVisibilityToggleContainer:"[data-region=\"replies-visibility-toggle-container\"]"},lock:{toggle:"[data-action='toggle'][data-type='lock-toggle']",icon:"[data-region='locked-icon']"},favourite:{toggle:"[data-type='favorite-toggle'][data-action='toggle']"},pin:{toggle:"[data-type='pin-toggle'][data-action='toggle']"},discussion:{tools:"[data-container=\"discussion-tools\"]"}}});
//# sourceMappingURL=selectors.min.js.map //# sourceMappingURL=selectors.min.js.map

View file

@ -1 +1 @@
{"version":3,"sources":["../src/selectors.js"],"names":["define","subscription","toggle","summary","actions","post","action","actionsContainer","forumCoreContent","forumContent","forumSubject","inpageReplyButton","inpageReplyLink","inpageReplyContent","inpageReplyForm","inpageSubmitBtn","inpageSubmitBtnText","loadingIconContainer","repliesContainer","modeSelect","lock","icon","favourite","pin"],"mappings":"AAuBAA,OAAM,uBAAC,EAAD,CAAK,UAAW,CAClB,MAAO,CACHC,YAAY,CAAE,CACVC,MAAM,CAAE,yDADE,CADX,CAIHC,OAAO,CAAE,CACLC,OAAO,CAAE,+CADJ,CAJN,CAOHC,IAAI,CAAE,CACFA,IAAI,CAAE,wBADJ,CAEFC,MAAM,CAAE,+BAFN,CAGFC,gBAAgB,CAAE,0CAHhB,CAIFC,gBAAgB,CAAE,yCAJhB,CAKFC,YAAY,CAAE,6BALZ,CAMFC,YAAY,CAAE,iDANZ,CAOFC,iBAAiB,CAAE,QAPjB,CAQFC,eAAe,CAAE,kCARf,CASFC,kBAAkB,CAAE,uCATlB,CAUFC,eAAe,CAAE,wCAVf,CAWFC,eAAe,CAAE,qCAXf,CAYFC,mBAAmB,CAAE,6BAZnB,CAaFC,oBAAoB,CAAE,wCAbpB,CAcFC,gBAAgB,CAAE,mCAdhB,CAeFC,UAAU,CAAE,qBAfV,CAPH,CAwBHC,IAAI,CAAE,CACFlB,MAAM,CAAE,iDADN,CAEFmB,IAAI,CAAE,6BAFJ,CAxBH,CA4BHC,SAAS,CAAE,CACPpB,MAAM,CAAE,qDADD,CA5BR,CA+BHqB,GAAG,CAAE,CACDrB,MAAM,CAAE,gDADP,CA/BF,CAmCV,CApCK,CAAN","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Common CSS selectors for the forum UI.\n *\n * @module mod_forum/selectors\n * @package mod_forum\n * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([], function() {\n return {\n subscription: {\n toggle: \"[data-type='subscription-toggle'][data-action='toggle']\",\n },\n summary: {\n actions: \"[data-container='discussion-summary-actions']\"\n },\n post: {\n post: '[data-region=\"post\"]',\n action: '[data-region=\"post-action\"]',\n actionsContainer: '[data-region=\"post-actions-container\"]',\n forumCoreContent: \"[data-region-content='forum-post-core']\",\n forumContent: \"[data-content='forum-post']\",\n forumSubject: \"[data-region-content='forum-post-core-subject']\",\n inpageReplyButton: \"button\",\n inpageReplyLink: \"[data-action='collapsible-link']\",\n inpageReplyContent: \"[data-content='inpage-reply-content']\",\n inpageReplyForm: \"form[data-content='inpage-reply-form']\",\n inpageSubmitBtn: \"[data-action='forum-inpage-submit']\",\n inpageSubmitBtnText: \"[data-region='submit-text']\",\n loadingIconContainer: \"[data-region='loading-icon-container']\",\n repliesContainer: \"[data-region='replies-container']\",\n modeSelect: \"select[name='mode']\"\n },\n lock: {\n toggle: \"[data-action='toggle'][data-type='lock-toggle']\",\n icon: \"[data-region='locked-icon']\"\n },\n favourite: {\n toggle: \"[data-type='favorite-toggle'][data-action='toggle']\",\n },\n pin: {\n toggle: \"[data-type='pin-toggle'][data-action='toggle']\",\n },\n };\n});\n"],"file":"selectors.min.js"} {"version":3,"sources":["../src/selectors.js"],"names":["define","subscription","toggle","summary","actions","post","action","actionsContainer","authorName","forumCoreContent","forumContent","forumSubject","inpageReplyButton","inpageReplyLink","inpageReplyCancelButton","inpageReplyCreateButton","inpageReplyContainer","inpageReplyContent","inpageReplyForm","inpageSubmitBtn","inpageSubmitBtnText","loadingIconContainer","repliesContainer","replyCount","modeSelect","showReplies","hideReplies","repliesVisibilityToggleContainer","lock","icon","favourite","pin","discussion","tools"],"mappings":"AAuBAA,OAAM,uBAAC,EAAD,CAAK,UAAW,CAClB,MAAO,CACHC,YAAY,CAAE,CACVC,MAAM,CAAE,yDADE,CADX,CAIHC,OAAO,CAAE,CACLC,OAAO,CAAE,+CADJ,CAJN,CAOHC,IAAI,CAAE,CACFA,IAAI,CAAE,wBADJ,CAEFC,MAAM,CAAE,+BAFN,CAGFC,gBAAgB,CAAE,0CAHhB,CAIFC,UAAU,CAAE,+BAJV,CAKFC,gBAAgB,CAAE,yCALhB,CAMFC,YAAY,CAAE,6BANZ,CAOFC,YAAY,CAAE,iDAPZ,CAQFC,iBAAiB,CAAE,QARjB,CASFC,eAAe,CAAE,kCATf,CAUFC,uBAAuB,CAAE,qCAVvB,CAWFC,uBAAuB,CAAE,qCAXvB,CAYFC,oBAAoB,CAAE,0CAZpB,CAaFC,kBAAkB,CAAE,uCAblB,CAcFC,eAAe,CAAE,wCAdf,CAeFC,eAAe,CAAE,qCAff,CAgBFC,mBAAmB,CAAE,6BAhBnB,CAiBFC,oBAAoB,CAAE,wCAjBpB,CAkBFC,gBAAgB,CAAE,mCAlBhB,CAmBFC,UAAU,CAAE,+BAnBV,CAoBFC,UAAU,CAAE,qBApBV,CAqBFC,WAAW,CAAE,gCArBX,CAsBFC,WAAW,CAAE,gCAtBX,CAuBFC,gCAAgC,CAAE,uDAvBhC,CAPH,CAgCHC,IAAI,CAAE,CACF1B,MAAM,CAAE,iDADN,CAEF2B,IAAI,CAAE,6BAFJ,CAhCH,CAoCHC,SAAS,CAAE,CACP5B,MAAM,CAAE,qDADD,CApCR,CAuCH6B,GAAG,CAAE,CACD7B,MAAM,CAAE,gDADP,CAvCF,CA0CH8B,UAAU,CAAE,CACRC,KAAK,CAAE,uCADC,CA1CT,CA8CV,CA/CK,CAAN","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Common CSS selectors for the forum UI.\n *\n * @module mod_forum/selectors\n * @package mod_forum\n * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([], function() {\n return {\n subscription: {\n toggle: \"[data-type='subscription-toggle'][data-action='toggle']\",\n },\n summary: {\n actions: \"[data-container='discussion-summary-actions']\"\n },\n post: {\n post: '[data-region=\"post\"]',\n action: '[data-region=\"post-action\"]',\n actionsContainer: '[data-region=\"post-actions-container\"]',\n authorName: '[data-region=\"author-name\"]',\n forumCoreContent: \"[data-region-content='forum-post-core']\",\n forumContent: \"[data-content='forum-post']\",\n forumSubject: \"[data-region-content='forum-post-core-subject']\",\n inpageReplyButton: \"button\",\n inpageReplyLink: \"[data-action='collapsible-link']\",\n inpageReplyCancelButton: \"[data-action='cancel-inpage-reply']\",\n inpageReplyCreateButton: \"[data-action='create-inpage-reply']\",\n inpageReplyContainer: '[data-region=\"inpage-reply-container\"]',\n inpageReplyContent: \"[data-content='inpage-reply-content']\",\n inpageReplyForm: \"form[data-content='inpage-reply-form']\",\n inpageSubmitBtn: \"[data-action='forum-inpage-submit']\",\n inpageSubmitBtnText: \"[data-region='submit-text']\",\n loadingIconContainer: \"[data-region='loading-icon-container']\",\n repliesContainer: \"[data-region='replies-container']\",\n replyCount: '[data-region=\"reply-count\"]',\n modeSelect: \"select[name='mode']\",\n showReplies: '[data-action=\"show-replies\"]',\n hideReplies: '[data-action=\"hide-replies\"]',\n repliesVisibilityToggleContainer: '[data-region=\"replies-visibility-toggle-container\"]'\n },\n lock: {\n toggle: \"[data-action='toggle'][data-type='lock-toggle']\",\n icon: \"[data-region='locked-icon']\"\n },\n favourite: {\n toggle: \"[data-type='favorite-toggle'][data-action='toggle']\",\n },\n pin: {\n toggle: \"[data-type='pin-toggle'][data-action='toggle']\",\n },\n discussion: {\n tools: '[data-container=\"discussion-tools\"]'\n }\n };\n});\n"],"file":"selectors.min.js"}

View file

@ -88,14 +88,16 @@ function(
// No siblings either. That means we're the lowest level reply in a thread // No siblings either. That means we're the lowest level reply in a thread
// so we need to walk back up the tree of posts and find an ancestor post that // so we need to walk back up the tree of posts and find an ancestor post that
// has a sibling post we can focus. // has a sibling post we can focus.
currentPost.parents().toArray().forEach(function(parent) { var parentPosts = currentPost.parents(Selectors.post.post).toArray();
var ancestorSiblingPost = $(parent).next(Selectors.post.post);
for (var i = 0; i < parentPosts.length; i++) {
var ancestorSiblingPost = $(parentPosts[i]).next(Selectors.post.post);
if (ancestorSiblingPost.length) { if (ancestorSiblingPost.length) {
ancestorSiblingPost.focus(); ancestorSiblingPost.focus();
return; break;
} }
}); }
} }
} }
}; };

View file

@ -0,0 +1,415 @@
// 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/>.
/**
* Module for viewing a discussion in modern view.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import $ from 'jquery';
import AutoRows from 'core/auto_rows';
import CustomEvents from 'core/custom_interaction_events';
import Notification from 'core/notification';
import Templates from 'core/templates';
import Discussion from 'mod_forum/discussion';
import InPageReply from 'mod_forum/inpage_reply';
import LockToggle from 'mod_forum/lock_toggle';
import FavouriteToggle from 'mod_forum/favourite_toggle';
import Pin from 'mod_forum/pin_toggle';
import Selectors from 'mod_forum/selectors';
const ANIMATION_DURATION = 150;
/**
* Get the closest post container element from the given element.
*
* @param {Object} element jQuery element to search from
* @return {Object} jQuery element
*/
const getPostContainer = (element) => {
return element.closest(Selectors.post.post);
};
/**
* Get the closest post container element from the given element.
*
* @param {Object} element jQuery element to search from
* @param {Number} id Id of the post to find.
* @return {Object} jQuery element
*/
const getPostContainerById = (element, id) => {
return element.find(`${Selectors.post.post}[data-post-id=${id}]`);
};
/**
* Get the parent post container elements from the given element.
*
* @param {Object} element jQuery element to search from
* @return {Object} jQuery element
*/
const getParentPostContainers = (element) => {
return element.parents(Selectors.post.post);
};
/**
* Get the post content container element from the post container element.
*
* @param {Object} postContainer jQuery element for the post container
* @return {Object} jQuery element
*/
const getPostContentContainer = (postContainer) => {
return postContainer.children().not(Selectors.post.repliesContainer).find(Selectors.post.forumCoreContent);
};
/**
* Get the in page reply container element from the post container element.
*
* @param {Object} postContainer jQuery element for the post container
* @return {Object} jQuery element
*/
const getInPageReplyContainer = (postContainer) => {
return postContainer.children().filter(Selectors.post.inpageReplyContainer);
};
/**
* Get the in page reply form element from the post container element.
*
* @param {Object} postContainer jQuery element for the post container
* @return {Object} jQuery element
*/
const getInPageReplyForm = (postContainer) => {
return getInPageReplyContainer(postContainer).find(Selectors.post.inpageReplyContent);
};
/**
* Get the in page reply create (reply) button element from the post container element.
*
* @param {Object} postContainer jQuery element for the post container
* @return {Object} jQuery element
*/
const getInPageReplyCreateButton = (postContainer) => {
return getPostContentContainer(postContainer).find(Selectors.post.inpageReplyCreateButton);
};
/**
* Get the replies visibility toggle container (show/hide replies button container) element
* from the post container element.
*
* @param {Object} postContainer jQuery element for the post container
* @return {Object} jQuery element
*/
const getRepliesVisibilityToggleContainer = (postContainer) => {
return postContainer.children(Selectors.post.repliesVisibilityToggleContainer);
};
/**
* Get the replies container element from the post container element.
*
* @param {Object} postContainer jQuery element for the post container
* @return {Object} jQuery element
*/
const getRepliesContainer = (postContainer) => {
return postContainer.children(Selectors.post.repliesContainer);
};
/**
* Check if the post has any replies.
*
* @param {Object} postContainer jQuery element for the post container
* @return {Bool}
*/
const hasReplies = (postContainer) => {
return getRepliesContainer(postContainer).children().length > 0;
};
/**
* Get the show replies button element from the replies visibility toggle container element.
*
* @param {Object} replyVisibilityToggleContainer jQuery element for the toggle container
* @return {Object} jQuery element
*/
const getShowRepliesButton = (replyVisibilityToggleContainer) => {
return replyVisibilityToggleContainer.find(Selectors.post.showReplies);
};
/**
* Get the hide replies button element from the replies visibility toggle container element.
*
* @param {Object} replyVisibilityToggleContainer jQuery element for the toggle container
* @return {Object} jQuery element
*/
const getHideRepliesButton = (replyVisibilityToggleContainer) => {
return replyVisibilityToggleContainer.find(Selectors.post.hideReplies);
};
/**
* Check if the replies are visible.
*
* @param {Object} postContainer jQuery element for the post container
* @return {Bool}
*/
const repliesVisible = (postContainer) => {
const repliesContainer = getRepliesContainer(postContainer);
return repliesContainer.is(':visible');
};
/**
* Show the post replies.
*
* @param {Object} postContainer jQuery element for the post container
* @param {Number|null} postIdToSee Id of the post to scroll into view (if any)
*/
const showReplies = (postContainer, postIdToSee = null) => {
const repliesContainer = getRepliesContainer(postContainer);
const replyVisibilityToggleContainer = getRepliesVisibilityToggleContainer(postContainer);
const showButton = getShowRepliesButton(replyVisibilityToggleContainer);
const hideButton = getHideRepliesButton(replyVisibilityToggleContainer);
showButton.addClass('hidden');
hideButton.removeClass('hidden');
repliesContainer.slideDown({
duration: ANIMATION_DURATION,
queue: false,
complete: () => {
if (postIdToSee) {
const postContainerToSee = getPostContainerById(repliesContainer, postIdToSee);
if (postContainerToSee.length) {
postContainerToSee[0].scrollIntoView();
}
}
}
}).css('display', 'none').fadeIn(ANIMATION_DURATION);
};
/**
* Hide the post replies.
*
* @param {Object} postContainer jQuery element for the post container
*/
const hideReplies = (postContainer) => {
const repliesContainer = getRepliesContainer(postContainer);
const replyVisibilityToggleContainer = getRepliesVisibilityToggleContainer(postContainer);
const showButton = getShowRepliesButton(replyVisibilityToggleContainer);
const hideButton = getHideRepliesButton(replyVisibilityToggleContainer);
showButton.removeClass('hidden');
hideButton.addClass('hidden');
repliesContainer.slideUp({
duration: ANIMATION_DURATION,
queue: false
}).fadeOut(ANIMATION_DURATION);
};
/** Variable to hold the showInPageReplyForm function after it's built. */
let showInPageReplyForm = null;
/**
* Build the showInPageReplyForm function with the given additional template context.
*
* @param {Object} additionalTemplateContext Additional render context for the in page reply template.
* @return {Function}
*/
const buildShowInPageReplyFormFunction = (additionalTemplateContext) => {
/**
* Show the in page reply form in the given in page reply container. The form
* display will be animated.
*
* @param {Object} postContainer jQuery element for the post container
*/
return async (postContainer) => {
const inPageReplyContainer = getInPageReplyContainer(postContainer);
const repliesVisibilityToggleContainer = getRepliesVisibilityToggleContainer(postContainer);
const inPageReplyCreateButton = getInPageReplyCreateButton(postContainer);
if (!hasInPageReplyForm(inPageReplyContainer)) {
try {
const html = await renderInPageReplyTemplate(additionalTemplateContext, inPageReplyCreateButton, postContainer);
Templates.appendNodeContents(inPageReplyContainer, html, '');
} catch (e) {
Notification.exception(e);
}
}
inPageReplyCreateButton.fadeOut(ANIMATION_DURATION, () => {
const inPageReplyForm = getInPageReplyForm(postContainer);
inPageReplyForm.slideDown({
duration: ANIMATION_DURATION,
queue: false,
complete: () => {
inPageReplyForm.find('textarea').focus();
}
}).css('display', 'none').fadeIn(ANIMATION_DURATION);
if (repliesVisibilityToggleContainer.length && hasReplies(postContainer)) {
repliesVisibilityToggleContainer.fadeIn(ANIMATION_DURATION);
hideReplies(postContainer);
}
});
};
};
/**
* Hide the in page reply form in the given in page reply container. The form
* display will be animated.
*
* @param {Object} postContainer jQuery element for the post container
* @param {Number|null} postIdToSee Id of the post to scroll into view (if any)
*/
const hideInPageReplyForm = (postContainer, postIdToSee = null) => {
const inPageReplyForm = getInPageReplyForm(postContainer);
const inPageReplyCreateButton = getInPageReplyCreateButton(postContainer);
const repliesVisibilityToggleContainer = getRepliesVisibilityToggleContainer(postContainer);
if (repliesVisibilityToggleContainer.length && hasReplies(postContainer)) {
repliesVisibilityToggleContainer.fadeOut(ANIMATION_DURATION);
if (!repliesVisible(postContainer)) {
showReplies(postContainer, postIdToSee);
}
}
inPageReplyForm.slideUp({
duration: ANIMATION_DURATION,
queue: false,
complete: () => {
inPageReplyCreateButton.fadeIn(ANIMATION_DURATION);
}
}).fadeOut(200);
};
/**
* Check if the in page reply container contains the in page reply form.
*
* @param {Object} inPageReplyContainer jQuery element for the in page reply container
* @return {Bool}
*/
const hasInPageReplyForm = (inPageReplyContainer) => {
return inPageReplyContainer.find(Selectors.post.inpageReplyContent).length > 0;
};
/**
* Render the template to generate the in page reply form HTML.
*
* @param {Object} additionalTemplateContext Additional render context for the in page reply template
* @param {Object} button jQuery element for the reply button that was clicked
* @param {Object} postContainer jQuery element for the post container
* @return {Object} jQuery promise
*/
const renderInPageReplyTemplate = (additionalTemplateContext, button, postContainer) => {
const postContentContainer = getPostContentContainer(postContainer);
const currentSubject = postContentContainer.find(Selectors.post.forumSubject).text();
const currentAuthorName = postContentContainer.find(Selectors.post.authorName).text();
const context = {
postid: postContainer.data('post-id'),
"reply_url": button.attr('data-href'),
sesskey: M.cfg.sesskey,
parentsubject: currentSubject,
parentauthorname: currentAuthorName,
canreplyprivately: button.data('can-reply-privately'),
postformat: InPageReply.CONTENT_FORMATS.MOODLE,
...additionalTemplateContext
};
return Templates.render('mod_forum/inpage_reply_modern', context);
};
/**
* Increment the total reply count in the show/hide replies buttons for the post.
*
* @param {Object} postContainer jQuery element for the post container
*/
const incrementTotalReplyCount = (postContainer) => {
getRepliesVisibilityToggleContainer(postContainer).find(Selectors.post.replyCount).each((index, element) => {
const currentCount = parseInt(element.innerText, 10);
element.innerText = currentCount + 1;
});
};
/**
* Create all of the event listeners for the discussion.
*
* @param {Object} root jQuery element for the discussion container
*/
const registerEventListeners = (root) => {
CustomEvents.define(root, [CustomEvents.events.activate]);
// Auto expanding text area for in page reply.
AutoRows.init(root);
// Reply button is clicked.
root.on(CustomEvents.events.activate, Selectors.post.inpageReplyCreateButton, (e, data) => {
data.originalEvent.preventDefault();
const postContainer = getPostContainer($(e.currentTarget));
showInPageReplyForm(postContainer);
});
// Cancel in page reply button.
root.on(CustomEvents.events.activate, Selectors.post.inpageReplyCancelButton, (e, data) => {
data.originalEvent.preventDefault();
const postContainer = getPostContainer($(e.currentTarget));
hideInPageReplyForm(postContainer);
});
// Show replies button clicked.
root.on(CustomEvents.events.activate, Selectors.post.showReplies, (e, data) => {
data.originalEvent.preventDefault();
const postContainer = getPostContainer($(e.target));
showReplies(postContainer);
});
// Hide replies button clicked.
root.on(CustomEvents.events.activate, Selectors.post.hideReplies, (e, data) => {
data.originalEvent.preventDefault();
const postContainer = getPostContainer($(e.target));
hideReplies(postContainer);
});
// Post created with in page reply.
root.on(InPageReply.EVENTS.POST_CREATED, Selectors.post.inpageSubmitBtn, (e, newPostId) => {
const currentTarget = $(e.currentTarget);
const postContainer = getPostContainer(currentTarget);
const postContainers = getParentPostContainers(currentTarget);
hideInPageReplyForm(postContainer, newPostId);
postContainers.each((index, container) => {
incrementTotalReplyCount($(container));
});
});
};
/**
* Initialise the javascript for the discussion in modern display mode.
*
* @param {Object} root jQuery element for the discussion container
* @param {Object} context Additional render context for the in page reply template
*/
export const init = (root, context) => {
// Build the showInPageReplyForm function with the additional render context.
showInPageReplyForm = buildShowInPageReplyFormFunction(context);
// Add discussion event listeners.
registerEventListeners(root);
// Initialise default discussion javascript (keyboard nav etc).
Discussion.init(root);
// Add in page reply javascript.
InPageReply.init(root);
// Initialise the settings menu javascript.
const discussionToolsContainer = root.find(Selectors.discussion.tools);
LockToggle.init(discussionToolsContainer);
FavouriteToggle.init(discussionToolsContainer);
Pin.init(discussionToolsContainer);
};

View file

@ -36,12 +36,17 @@ define([
) { ) {
var DISPLAYCONSTANTS = { var DISPLAYCONSTANTS = {
MODERN: 4,
THREADED: 2, THREADED: 2,
NESTED: 3, NESTED: 3,
FLAT_OLDEST_FIRST: 1, FLAT_OLDEST_FIRST: 1,
FLAT_NEWEST_FIRST: -1 FLAT_NEWEST_FIRST: -1
}; };
var EVENTS = {
POST_CREATED: 'mod_forum-post-created'
};
/** /**
* Moodle formats taken from the FORMAT_* constants declared in lib/weblib.php. * Moodle formats taken from the FORMAT_* constants declared in lib/weblib.php.
* @type {Object} * @type {Object}
@ -97,9 +102,10 @@ define([
var topreferredformat = true; var topreferredformat = true;
var postid = form.elements.reply.value; var postid = form.elements.reply.value;
var subject = form.elements.subject.value; var subject = form.elements.subject.value;
var currentRoot = submitButton.parents(Selectors.post.forumContent); var currentRoot = submitButton.closest(Selectors.post.post);
var isprivatereply = form.elements.privatereply != undefined ? form.elements.privatereply.checked : false; var isprivatereply = form.elements.privatereply != undefined ? form.elements.privatereply.checked : false;
var mode = parseInt(root.find(Selectors.post.modeSelect).get(0).value); var modeSelector = root.find(Selectors.post.modeSelect);
var mode = modeSelector.length ? parseInt(modeSelector.get(0).value) : null;
var newid; var newid;
if (message.length) { if (message.length) {
@ -125,7 +131,17 @@ define([
form.reset(); form.reset();
var post = context.post; var post = context.post;
newid = post.id; newid = post.id;
switch (mode) { switch (mode) {
case DISPLAYCONSTANTS.MODERN:
var capabilities = post.capabilities;
post.showactionmenu = capabilities.controlreadstatus ||
capabilities.edit ||
capabilities.split ||
capabilities.delete ||
capabilities.export ||
post.urls.viewparent;
return Templates.render('mod_forum/forum_discussion_modern_post_reply', post);
case DISPLAYCONSTANTS.THREADED: case DISPLAYCONSTANTS.THREADED:
return Templates.render('mod_forum/forum_discussion_threaded_post', post); return Templates.render('mod_forum/forum_discussion_threaded_post', post);
case DISPLAYCONSTANTS.NESTED: case DISPLAYCONSTANTS.NESTED:
@ -135,16 +151,7 @@ define([
} }
}) })
.then(function(html, js) { .then(function(html, js) {
var repliesnode; var repliesnode = currentRoot.find(Selectors.post.repliesContainer).first();
// Try and get the replies-container which can either be a sibling OR parent if it's flat
if (mode == DISPLAYCONSTANTS.FLAT_OLDEST_FIRST || mode == DISPLAYCONSTANTS.FLAT_NEWEST_FIRST) {
repliesnode = currentRoot.parents(Selectors.post.repliesContainer).children().get(0);
}
if (repliesnode == undefined) {
repliesnode = currentRoot.siblings(Selectors.post.repliesContainer).children().get(0);
}
if (mode == DISPLAYCONSTANTS.FLAT_NEWEST_FIRST) { if (mode == DISPLAYCONSTANTS.FLAT_NEWEST_FIRST) {
return Templates.prependNodeContents(repliesnode, html, js); return Templates.prependNodeContents(repliesnode, html, js);
@ -153,6 +160,7 @@ define([
} }
}) })
.then(function() { .then(function() {
submitButton.trigger(EVENTS.POST_CREATED, newid);
hideSubmitButtonLoadingIcon(submitButton); hideSubmitButtonLoadingIcon(submitButton);
allButtons.prop('disabled', false); allButtons.prop('disabled', false);
return currentRoot.find(Selectors.post.inpageReplyContent).hide(); return currentRoot.find(Selectors.post.inpageReplyContent).hide();
@ -174,6 +182,7 @@ define([
init: function(root) { init: function(root) {
registerEventListeners(root); registerEventListeners(root);
}, },
CONTENT_FORMATS: CONTENT_FORMATS CONTENT_FORMATS: CONTENT_FORMATS,
EVENTS: EVENTS
}; };
}); });

View file

@ -33,18 +33,26 @@ define([], function() {
post: '[data-region="post"]', post: '[data-region="post"]',
action: '[data-region="post-action"]', action: '[data-region="post-action"]',
actionsContainer: '[data-region="post-actions-container"]', actionsContainer: '[data-region="post-actions-container"]',
authorName: '[data-region="author-name"]',
forumCoreContent: "[data-region-content='forum-post-core']", forumCoreContent: "[data-region-content='forum-post-core']",
forumContent: "[data-content='forum-post']", forumContent: "[data-content='forum-post']",
forumSubject: "[data-region-content='forum-post-core-subject']", forumSubject: "[data-region-content='forum-post-core-subject']",
inpageReplyButton: "button", inpageReplyButton: "button",
inpageReplyLink: "[data-action='collapsible-link']", inpageReplyLink: "[data-action='collapsible-link']",
inpageReplyCancelButton: "[data-action='cancel-inpage-reply']",
inpageReplyCreateButton: "[data-action='create-inpage-reply']",
inpageReplyContainer: '[data-region="inpage-reply-container"]',
inpageReplyContent: "[data-content='inpage-reply-content']", inpageReplyContent: "[data-content='inpage-reply-content']",
inpageReplyForm: "form[data-content='inpage-reply-form']", inpageReplyForm: "form[data-content='inpage-reply-form']",
inpageSubmitBtn: "[data-action='forum-inpage-submit']", inpageSubmitBtn: "[data-action='forum-inpage-submit']",
inpageSubmitBtnText: "[data-region='submit-text']", inpageSubmitBtnText: "[data-region='submit-text']",
loadingIconContainer: "[data-region='loading-icon-container']", loadingIconContainer: "[data-region='loading-icon-container']",
repliesContainer: "[data-region='replies-container']", repliesContainer: "[data-region='replies-container']",
modeSelect: "select[name='mode']" replyCount: '[data-region="reply-count"]',
modeSelect: "select[name='mode']",
showReplies: '[data-action="show-replies"]',
hideReplies: '[data-action="hide-replies"]',
repliesVisibilityToggleContainer: '[data-region="replies-visibility-toggle-container"]'
}, },
lock: { lock: {
toggle: "[data-action='toggle'][data-type='lock-toggle']", toggle: "[data-action='toggle'][data-type='lock-toggle']",
@ -56,5 +64,8 @@ define([], function() {
pin: { pin: {
toggle: "[data-type='pin-toggle'][data-action='toggle']", toggle: "[data-type='pin-toggle'][data-action='toggle']",
}, },
discussion: {
tools: '[data-container="discussion-tools"]'
}
}; };
}); });

View file

@ -51,6 +51,7 @@ class author {
'lastname' => $author->get_last_name(), 'lastname' => $author->get_last_name(),
'fullname' => $author->get_full_name(), 'fullname' => $author->get_full_name(),
'email' => $author->get_email(), 'email' => $author->get_email(),
'deleted' => $author->is_deleted(),
'middlename' => $author->get_middle_name(), 'middlename' => $author->get_middle_name(),
'firstnamephonetic' => $author->get_first_name_phonetic(), 'firstnamephonetic' => $author->get_first_name_phonetic(),
'lastnamephonetic' => $author->get_last_name_phonetic(), 'lastnamephonetic' => $author->get_last_name_phonetic(),

View file

@ -45,6 +45,8 @@ class author {
private $fullname; private $fullname;
/** @var string $email Email */ /** @var string $email Email */
private $email; private $email;
/** @var bool $deleted Deleted */
private $deleted;
/** @var string $middlename Middle name */ /** @var string $middlename Middle name */
private $middlename; private $middlename;
/** @var string $firstnamephonetic Phonetic spelling of first name */ /** @var string $firstnamephonetic Phonetic spelling of first name */
@ -78,6 +80,7 @@ class author {
string $lastname, string $lastname,
string $fullname, string $fullname,
string $email, string $email,
bool $deleted,
string $middlename = null, string $middlename = null,
string $firstnamephonetic = null, string $firstnamephonetic = null,
string $lastnamephonetic = null, string $lastnamephonetic = null,
@ -90,6 +93,7 @@ class author {
$this->lastname = $lastname; $this->lastname = $lastname;
$this->fullname = $fullname; $this->fullname = $fullname;
$this->email = $email; $this->email = $email;
$this->deleted = $deleted;
$this->middlename = $middlename; $this->middlename = $middlename;
$this->firstnamephonetic = $firstnamephonetic; $this->firstnamephonetic = $firstnamephonetic;
$this->lastnamephonetic = $lastnamephonetic; $this->lastnamephonetic = $lastnamephonetic;
@ -151,6 +155,15 @@ class author {
return $this->email; return $this->email;
} }
/**
* Is the author deleted?
*
* @return bool
*/
public function is_deleted() : bool {
return !empty($this->deleted);
}
/** /**
* Return the middle name. * Return the middle name.
* *

View file

@ -91,6 +91,12 @@ class author extends exporter {
'default' => null, 'default' => null,
'null' => NULL_ALLOWED 'null' => NULL_ALLOWED
], ],
'isdeleted' => [
'type' => PARAM_BOOL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'groups' => [ 'groups' => [
'multiple' => true, 'multiple' => true,
'optional' => true, 'optional' => true,
@ -143,41 +149,56 @@ class author extends exporter {
$context = $this->related['context']; $context = $this->related['context'];
if ($this->canview) { if ($this->canview) {
$groups = array_map(function($group) use ($urlfactory, $context) { if ($author->is_deleted()) {
$imageurl = null;
$groupurl = null;
if (!$group->hidepicture) {
$imageurl = get_group_picture_url($group, $group->courseid, true);
}
if (course_can_view_participants($context)) {
$groupurl = $urlfactory->get_author_group_url($group);
}
return [ return [
'id' => $group->id, 'id' => $author->get_id(),
'name' => $group->name, 'fullname' => get_string('deleteduser', 'mod_forum'),
'isdeleted' => true,
'groups' => [],
'urls' => [ 'urls' => [
'image' => $imageurl ? $imageurl->out(false) : null, 'profile' => ($urlfactory->get_author_profile_url($author))->out(false),
'group' => $groupurl ? $groupurl->out(false) : null 'profileimage' => ($urlfactory->get_author_profile_image_url($author, $authorcontextid))->out(false)
] ]
]; ];
}, $this->authorgroups); } else {
$groups = array_map(function($group) use ($urlfactory, $context) {
$imageurl = null;
$groupurl = null;
if (!$group->hidepicture) {
$imageurl = get_group_picture_url($group, $group->courseid, true);
}
if (course_can_view_participants($context)) {
$groupurl = $urlfactory->get_author_group_url($group);
}
return [ return [
'id' => $author->get_id(), 'id' => $group->id,
'fullname' => $author->get_full_name(), 'name' => $group->name,
'groups' => $groups, 'urls' => [
'urls' => [ 'image' => $imageurl ? $imageurl->out(false) : null,
'profile' => ($urlfactory->get_author_profile_url($author))->out(false), 'group' => $groupurl ? $groupurl->out(false) : null
'profileimage' => ($urlfactory->get_author_profile_image_url($author, $authorcontextid))->out(false)
] ]
]; ];
}, $this->authorgroups);
return [
'id' => $author->get_id(),
'fullname' => $author->get_full_name(),
'isdeleted' => false,
'groups' => $groups,
'urls' => [
'profile' => ($urlfactory->get_author_profile_url($author))->out(false),
'profileimage' => ($urlfactory->get_author_profile_image_url($author, $authorcontextid))->out(false)
]
];
}
} else { } else {
// The author should be anonymised. // The author should be anonymised.
return [ return [
'id' => null, 'id' => null,
'fullname' => get_string('forumauthorhidden', 'mod_forum'), 'fullname' => get_string('forumauthorhidden', 'mod_forum'),
'isdeleted' => null,
'groups' => [], 'groups' => [],
'urls' => [ 'urls' => [
'profile' => null, 'profile' => null,

View file

@ -386,7 +386,7 @@ class post extends exporter {
$author, $author,
$authorcontextid, $authorcontextid,
$authorgroups, $authorgroups,
($canview && !$isdeleted), $canview,
$this->related $this->related
); );
$exportedauthor = $authorexporter->export($output); $exportedauthor = $authorexporter->export($output);
@ -402,10 +402,6 @@ class post extends exporter {
$subject = $isdeleted ? get_string('forumsubjectdeleted', 'forum') : get_string('forumsubjecthidden', 'forum'); $subject = $isdeleted ? get_string('forumsubjectdeleted', 'forum') : get_string('forumsubjecthidden', 'forum');
$message = $isdeleted ? get_string('forumbodydeleted', 'forum') : get_string('forumbodyhidden', 'forum'); $message = $isdeleted ? get_string('forumbodydeleted', 'forum') : get_string('forumbodyhidden', 'forum');
$timecreated = null; $timecreated = null;
if ($isdeleted) {
$exportedauthor->fullname = null;
}
} }
$replysubject = $subject; $replysubject = $subject;

View file

@ -172,6 +172,7 @@ class entity {
$record->lastname, $record->lastname,
fullname($record), fullname($record),
$record->email, $record->email,
$record->deleted,
$record->middlename, $record->middlename,
$record->firstnamephonetic, $record->firstnamephonetic,
$record->lastnamephonetic, $record->lastnamephonetic,

View file

@ -134,6 +134,8 @@ class renderer {
$this->legacydatamapperfactory, $this->legacydatamapperfactory,
$this->exporterfactory, $this->exporterfactory,
$this->vaultfactory, $this->vaultfactory,
$this->urlfactory,
$this->entityfactory,
$capabilitymanager, $capabilitymanager,
$ratingmanager, $ratingmanager,
$this->entityfactory->get_exported_posts_sorter(), $this->entityfactory->get_exported_posts_sorter(),
@ -141,7 +143,7 @@ class renderer {
$notifications, $notifications,
function($discussion, $user, $forum) { function($discussion, $user, $forum) {
$exportbuilder = $this->builderfactory->get_exported_discussion_builder(); $exportbuilder = $this->builderfactory->get_exported_discussion_builder();
return $exportedposts = $exportbuilder->build( return $exportbuilder->build(
$user, $user,
$forum, $forum,
$discussion $discussion
@ -180,6 +182,9 @@ class renderer {
case FORUM_MODE_NESTED: case FORUM_MODE_NESTED:
$template = 'mod_forum/forum_discussion_nested_posts'; $template = 'mod_forum/forum_discussion_nested_posts';
break; break;
case FORUM_MODE_MODERN:
$template = 'mod_forum/forum_discussion_modern_posts';
break;
default; default;
$template = 'mod_forum/forum_discussion_posts'; $template = 'mod_forum/forum_discussion_posts';
break; break;
@ -192,12 +197,17 @@ class renderer {
// Post process the exported posts for our template. This function will add the "replies" // Post process the exported posts for our template. This function will add the "replies"
// and "hasreplies" properties to the exported posts. It will also sort them into the // and "hasreplies" properties to the exported posts. It will also sort them into the
// reply tree structure if the display mode requires it. // reply tree structure if the display mode requires it.
function($exportedposts, $forums) use ($displaymode, $readonly, $exportedpostssorter) { function($exportedposts, $forums, $discussions) use ($displaymode, $readonly, $exportedpostssorter) {
$forum = array_shift($forums); $forum = array_shift($forums);
$seenfirstunread = false; $seenfirstunread = false;
$postcount = count($exportedposts); $postcount = count($exportedposts);
$discussionsbyid = array_reduce($discussions, function($carry, $discussion) {
$carry[$discussion->get_id()] = $discussion;
return $carry;
}, []);
$exportedposts = array_map( $exportedposts = array_map(
function($exportedpost) use ($forum, $readonly, $seenfirstunread) { function($exportedpost) use ($forum, $discussionsbyid, $readonly, $seenfirstunread, $displaymode) {
$discussion = $discussionsbyid[$exportedpost->discussionid] ?? null;
if ($forum->get_type() == 'single' && !$exportedpost->hasparent) { if ($forum->get_type() == 'single' && !$exportedpost->hasparent) {
// Remove the author from any posts that don't have a parent. // Remove the author from any posts that don't have a parent.
unset($exportedpost->author); unset($exportedpost->author);
@ -209,6 +219,7 @@ class renderer {
$exportedpost->hasreplycount = false; $exportedpost->hasreplycount = false;
$exportedpost->hasreplies = false; $exportedpost->hasreplies = false;
$exportedpost->replies = []; $exportedpost->replies = [];
$exportedpost->discussionlocked = $discussion ? $discussion->is_locked() : null;
$exportedpost->isfirstunread = false; $exportedpost->isfirstunread = false;
if (!$seenfirstunread && $exportedpost->unread) { if (!$seenfirstunread && $exportedpost->unread) {
@ -216,31 +227,59 @@ class renderer {
$seenfirstunread = true; $seenfirstunread = true;
} }
if ($displaymode === FORUM_MODE_MODERN) {
$exportedpost->showactionmenu = $exportedpost->capabilities['controlreadstatus'] ||
$exportedpost->capabilities['edit'] ||
$exportedpost->capabilities['split'] ||
$exportedpost->capabilities['delete'] ||
$exportedpost->capabilities['export'] ||
!empty($exportedpost->urls['viewparent']);
}
return $exportedpost; return $exportedpost;
}, },
$exportedposts $exportedposts
); );
if ($displaymode === FORUM_MODE_NESTED || $displaymode === FORUM_MODE_THREADED) { if (
$displaymode === FORUM_MODE_NESTED ||
$displaymode === FORUM_MODE_THREADED ||
$displaymode === FORUM_MODE_MODERN
) {
$sortedposts = $exportedpostssorter->sort_into_children($exportedposts); $sortedposts = $exportedpostssorter->sort_into_children($exportedposts);
$sortintoreplies = function($nestedposts) use (&$sortintoreplies) { $sortintoreplies = function($nestedposts) use (&$sortintoreplies) {
return array_map(function($postdata) use (&$sortintoreplies) { return array_map(function($postdata) use (&$sortintoreplies) {
[$post, $replies] = $postdata; [$post, $replies] = $postdata;
$sortedreplies = $sortintoreplies($replies); $totalreplycount = 0;
// Set the parent author name on the replies. This is used for screen
// readers to help them identify the structure of the discussion. if (empty($replies)) {
$sortedreplies = array_map(function($reply) use ($post) { $post->replies = [];
if (isset($post->author)) { $post->hasreplies = false;
$reply->parentauthorname = $post->author->fullname; } else {
} else { $sortedreplies = $sortintoreplies($replies);
// The only time the author won't be set is for a single discussion // Set the parent author name on the replies. This is used for screen
// forum. See above for where it gets unset. // readers to help them identify the structure of the discussion.
$reply->parentauthorname = get_string('firstpost', 'mod_forum'); $sortedreplies = array_map(function($reply) use ($post) {
} if (isset($post->author)) {
return $reply; $reply->parentauthorname = $post->author->fullname;
}, $sortedreplies); } else {
$post->replies = $sortedreplies; // The only time the author won't be set is for a single discussion
$post->hasreplies = !empty($post->replies); // forum. See above for where it gets unset.
$reply->parentauthorname = get_string('firstpost', 'mod_forum');
}
return $reply;
}, $sortedreplies);
$totalreplycount = array_reduce($sortedreplies, function($carry, $reply) {
return $carry + 1 + $reply->totalreplycount;
}, $totalreplycount);
$post->replies = $sortedreplies;
$post->hasreplies = true;
}
$post->totalreplycount = $totalreplycount;
return $post; return $post;
}, $nestedposts); }, $nestedposts);
}; };
@ -583,6 +622,8 @@ class renderer {
$this->legacydatamapperfactory, $this->legacydatamapperfactory,
$this->exporterfactory, $this->exporterfactory,
$this->vaultfactory, $this->vaultfactory,
$this->urlfactory,
$this->entityfactory,
$capabilitymanager, $capabilitymanager,
$ratingmanager, $ratingmanager,
$this->entityfactory->get_exported_posts_sorter(), $this->entityfactory->get_exported_posts_sorter(),

View file

@ -30,8 +30,10 @@ use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\post as post_entity; use mod_forum\local\entities\post as post_entity;
use mod_forum\local\entities\sorter as sorter_entity; use mod_forum\local\entities\sorter as sorter_entity;
use mod_forum\local\factories\entity as entity_factory;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory; use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\exporter as exporter_factory; use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\url as url_factory;
use mod_forum\local\factories\vault as vault_factory; use mod_forum\local\factories\vault as vault_factory;
use mod_forum\local\managers\capability as capability_manager; use mod_forum\local\managers\capability as capability_manager;
use mod_forum\local\renderers\posts as posts_renderer; use mod_forum\local\renderers\posts as posts_renderer;
@ -80,6 +82,10 @@ class discussion {
private $exporterfactory; private $exporterfactory;
/** @var vault_factory $vaultfactory Vault factory */ /** @var vault_factory $vaultfactory Vault factory */
private $vaultfactory; private $vaultfactory;
/** @var url_factory $urlfactory URL factory */
private $urlfactory;
/** @var entity_factory $entityfactory Entity factory */
private $entityfactory;
/** @var capability_manager $capabilitymanager Capability manager */ /** @var capability_manager $capabilitymanager Capability manager */
private $capabilitymanager; private $capabilitymanager;
/** @var rating_manager $ratingmanager Rating manager */ /** @var rating_manager $ratingmanager Rating manager */
@ -105,11 +111,14 @@ class discussion {
* @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory * @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory
* @param exporter_factory $exporterfactory Exporter factory * @param exporter_factory $exporterfactory Exporter factory
* @param vault_factory $vaultfactory Vault factory * @param vault_factory $vaultfactory Vault factory
* @param url_factory $urlfactory URL factory
* @param entity_factory $entityfactory Entity factory
* @param capability_manager $capabilitymanager Capability manager * @param capability_manager $capabilitymanager Capability manager
* @param rating_manager $ratingmanager Rating manager * @param rating_manager $ratingmanager Rating manager
* @param sorter_entity $exportedpostsorter Sorter for the exported posts * @param sorter_entity $exportedpostsorter Sorter for the exported posts
* @param moodle_url $baseurl The base URL for the discussion * @param moodle_url $baseurl The base URL for the discussion
* @param array $notifications List of HTML notifications to display * @param array $notifications List of HTML notifications to display
* @param callable|null $postprocessfortemplate Post processing for template callback
*/ */
public function __construct( public function __construct(
forum_entity $forum, forum_entity $forum,
@ -121,6 +130,8 @@ class discussion {
legacy_data_mapper_factory $legacydatamapperfactory, legacy_data_mapper_factory $legacydatamapperfactory,
exporter_factory $exporterfactory, exporter_factory $exporterfactory,
vault_factory $vaultfactory, vault_factory $vaultfactory,
url_factory $urlfactory,
entity_factory $entityfactory,
capability_manager $capabilitymanager, capability_manager $capabilitymanager,
rating_manager $ratingmanager, rating_manager $ratingmanager,
sorter_entity $exportedpostsorter, sorter_entity $exportedpostsorter,
@ -138,6 +149,8 @@ class discussion {
$this->legacydatamapperfactory = $legacydatamapperfactory; $this->legacydatamapperfactory = $legacydatamapperfactory;
$this->exporterfactory = $exporterfactory; $this->exporterfactory = $exporterfactory;
$this->vaultfactory = $vaultfactory; $this->vaultfactory = $vaultfactory;
$this->urlfactory = $urlfactory;
$this->entityfactory = $entityfactory;
$this->capabilitymanager = $capabilitymanager; $this->capabilitymanager = $capabilitymanager;
$this->ratingmanager = $ratingmanager; $this->ratingmanager = $ratingmanager;
$this->notifications = $notifications; $this->notifications = $notifications;
@ -169,6 +182,8 @@ class discussion {
$displaymode = $this->displaymode; $displaymode = $this->displaymode;
$capabilitymanager = $this->capabilitymanager; $capabilitymanager = $this->capabilitymanager;
$urlfactory = $this->urlfactory;
$entityfactory = $this->entityfactory;
// Make sure we can render. // Make sure we can render.
if (!$capabilitymanager->can_view_discussions($user)) { if (!$capabilitymanager->can_view_discussions($user)) {
@ -212,7 +227,22 @@ class discussion {
$exporteddiscussion['html']['movediscussion'] = $this->get_move_discussion_html(); $exporteddiscussion['html']['movediscussion'] = $this->get_move_discussion_html();
} }
return $this->renderer->render_from_template('mod_forum/forum_discussion', $exporteddiscussion); if (!empty($user->id)) {
$loggedinuser = $entityfactory->get_author_from_stdClass($user);
$exporteddiscussion['loggedinuser'] = [
'firstname' => $loggedinuser->get_first_name(),
'fullname' => $loggedinuser->get_full_name(),
'profileimageurl' => ($urlfactory->get_author_profile_image_url($loggedinuser, null))->out(false)
];
}
if ($this->displaymode === FORUM_MODE_MODERN) {
$template = 'mod_forum/forum_discussion_modern';
} else {
$template = 'mod_forum/forum_discussion';
}
return $this->renderer->render_from_template($template, $exporteddiscussion);
} }
/** /**

View file

@ -120,8 +120,8 @@ class discussion_list extends db_table_vault {
// - Most recent editor. // - Most recent editor.
$thistable = new dml_table(self::TABLE, $alias, $alias); $thistable = new dml_table(self::TABLE, $alias, $alias);
$posttable = new dml_table('forum_posts', 'fp', 'p_'); $posttable = new dml_table('forum_posts', 'fp', 'p_');
$firstauthorfields = \user_picture::fields('fa', null, self::FIRST_AUTHOR_ID_ALIAS, self::FIRST_AUTHOR_ALIAS); $firstauthorfields = \user_picture::fields('fa', ['deleted'], self::FIRST_AUTHOR_ID_ALIAS, self::FIRST_AUTHOR_ALIAS);
$latestuserfields = \user_picture::fields('la', null, self::LATEST_AUTHOR_ID_ALIAS, self::LATEST_AUTHOR_ALIAS); $latestuserfields = \user_picture::fields('la', ['deleted'], self::LATEST_AUTHOR_ID_ALIAS, self::LATEST_AUTHOR_ALIAS);
$fields = implode(', ', [ $fields = implode(', ', [
$thistable->get_field_select(), $thistable->get_field_select(),

View file

@ -65,7 +65,7 @@ class extract_user {
$alias = $this->alias; $alias = $this->alias;
return array_map(function($record) use ($idalias, $alias) { return array_map(function($record) use ($idalias, $alias) {
return user_picture::unalias($record, null, $idalias, $alias); return user_picture::unalias($record, ['deleted'], $idalias, $alias);
}, $records); }, $records);
} }
} }

View file

@ -291,13 +291,20 @@ if ($node && $post->get_id() != $discussion->get_first_post_id()) {
$node->add(format_string($post->get_subject()), $PAGE->url); $node->add(format_string($post->get_subject()), $PAGE->url);
} }
$ismoderndisplaymode = $displaymode == FORUM_MODE_MODERN;
$PAGE->set_title("$course->shortname: " . format_string($discussion->get_name())); $PAGE->set_title("$course->shortname: " . format_string($discussion->get_name()));
$PAGE->set_heading($course->fullname); $PAGE->set_heading($course->fullname);
$PAGE->set_button(forum_search_form($course)); if ($ismoderndisplaymode) {
$PAGE->add_body_class('modern-display-mode reset-style');
} else {
$PAGE->set_button(forum_search_form($course));
}
echo $OUTPUT->header(); echo $OUTPUT->header();
echo $OUTPUT->heading(format_string($forum->get_name()), 2); if (!$ismoderndisplaymode) {
echo $OUTPUT->heading(format_string($discussion->get_name()), 3, 'discussionname'); echo $OUTPUT->heading(format_string($forum->get_name()), 2);
echo $OUTPUT->heading(format_string($discussion->get_name()), 3, 'discussionname');
}
$rendererfactory = mod_forum\local\container::get_renderer_factory(); $rendererfactory = mod_forum\local\container::get_renderer_factory();
$discussionrenderer = $rendererfactory->get_discussion_renderer($forum, $discussion, $displaymode); $discussionrenderer = $rendererfactory->get_discussion_renderer($forum, $discussion, $displaymode);

View file

@ -24,6 +24,7 @@
*/ */
$string['activityoverview'] = 'There are new forum posts'; $string['activityoverview'] = 'There are new forum posts';
$string['actionsforpost'] = 'Actions for post';
$string['addanewdiscussion'] = 'Add a new discussion topic'; $string['addanewdiscussion'] = 'Add a new discussion topic';
$string['addanewquestion'] = 'Add a new question'; $string['addanewquestion'] = 'Add a new question';
$string['addanewtopic'] = 'Add a new topic'; $string['addanewtopic'] = 'Add a new topic';
@ -43,10 +44,13 @@ $string['areaattachment'] = 'Attachments';
$string['areapost'] = 'Messages'; $string['areapost'] = 'Messages';
$string['attachment'] = 'Attachment'; $string['attachment'] = 'Attachment';
$string['attachmentname'] = 'Attachment {$a}'; $string['attachmentname'] = 'Attachment {$a}';
$string['attachmentnameandfilesize'] = '{$a->name} ({$a->size})';
$string['attachment_help'] = 'You can optionally attach one or more files to a forum post. If you attach an image, it will be displayed after the message.'; $string['attachment_help'] = 'You can optionally attach one or more files to a forum post. If you attach an image, it will be displayed after the message.';
$string['attachmentnopost'] = 'You cannot export attachments without a post id'; $string['attachmentnopost'] = 'You cannot export attachments without a post id';
$string['attachments'] = 'Attachments'; $string['attachments'] = 'Attachments';
$string['attachmentswordcount'] = 'Attachments and word count'; $string['attachmentswordcount'] = 'Attachments and word count';
$string['authorreplyingprivatelytoauthor'] = '{$a->respondant} replying privately to {$a->author}';
$string['authorreplyingtoauthor'] = '{$a->respondant} replying to {$a->author}';
$string['availability'] = 'Availability'; $string['availability'] = 'Availability';
$string['blockafter'] = 'Post threshold for blocking'; $string['blockafter'] = 'Post threshold for blocking';
$string['blockafter_help'] = 'This setting specifies the maximum number of posts which a user can post in the given time period. Users with the capability mod/forum:postwithoutthrottling are exempt from post limits.'; $string['blockafter_help'] = 'This setting specifies the maximum number of posts which a user can post in the given time period. Users with the capability mod/forum:postwithoutthrottling are exempt from post limits.';
@ -57,6 +61,7 @@ $string['blogforum'] = 'Standard forum displayed in a blog-like format';
$string['bynameondate'] = 'by {$a->name} - {$a->date}'; $string['bynameondate'] = 'by {$a->name} - {$a->date}';
$string['cachedef_forum_is_tracked'] = 'Forum tracking status for user'; $string['cachedef_forum_is_tracked'] = 'Forum tracking status for user';
$string['calendardue'] = '{$a} is due'; $string['calendardue'] = '{$a} is due';
$string['cancelreply'] = 'Cancel reply';
$string['cannotadd'] = 'Could not add the discussion for this forum'; $string['cannotadd'] = 'Could not add the discussion for this forum';
$string['cannotadddiscussion'] = 'Adding discussions to this forum requires group membership.'; $string['cannotadddiscussion'] = 'Adding discussions to this forum requires group membership.';
$string['cannotadddiscussionall'] = 'You do not have permission to add a new discussion topic for all participants.'; $string['cannotadddiscussionall'] = 'You do not have permission to add a new discussion topic for all participants.';
@ -144,6 +149,7 @@ $string['delete'] = 'Delete';
$string['deleteddiscussion'] = 'The discussion topic has been deleted'; $string['deleteddiscussion'] = 'The discussion topic has been deleted';
$string['deletedpost'] = 'The post has been deleted'; $string['deletedpost'] = 'The post has been deleted';
$string['deletedposts'] = 'Those posts have been deleted'; $string['deletedposts'] = 'Those posts have been deleted';
$string['deleteduser'] = 'Deleted user';
$string['deletesure'] = 'Are you sure you want to delete this post?'; $string['deletesure'] = 'Are you sure you want to delete this post?';
$string['deletesureplural'] = 'Are you sure you want to delete this post and all replies? ({$a} posts)'; $string['deletesureplural'] = 'Are you sure you want to delete this post and all replies? ({$a} posts)';
$string['digestmailheader'] = 'This is your daily digest of new posts from the {$a->sitename} forums. To change your default forum email preferences, go to {$a->userprefs}.'; $string['digestmailheader'] = 'This is your daily digest of new posts from the {$a->sitename} forums. To change your default forum email preferences, go to {$a->userprefs}.';
@ -311,6 +317,7 @@ $string['forum:viewsubscribers'] = 'View subscribers';
$string['generalforum'] = 'Standard forum for general use'; $string['generalforum'] = 'Standard forum for general use';
$string['generalforums'] = 'General forums'; $string['generalforums'] = 'General forums';
$string['hiddenforumpost'] = 'Hidden forum post'; $string['hiddenforumpost'] = 'Hidden forum post';
$string['hidepreviousrepliescount'] = 'Hide previous replies ({$a})';
$string['indicator:cognitivedepth'] = 'Forum cognitive'; $string['indicator:cognitivedepth'] = 'Forum cognitive';
$string['indicator:cognitivedepth_help'] = 'This indicator is based on the cognitive depth reached by the student in a Forum activity.'; $string['indicator:cognitivedepth_help'] = 'This indicator is based on the cognitive depth reached by the student in a Forum activity.';
$string['indicator:socialbreadth'] = 'Forum social'; $string['indicator:socialbreadth'] = 'Forum social';
@ -370,6 +377,7 @@ $string['messageprovider:posts'] = 'Subscribed forum posts';
$string['missingsearchterms'] = 'The following search terms occur only in the HTML markup of this message:'; $string['missingsearchterms'] = 'The following search terms occur only in the HTML markup of this message:';
$string['modeflatnewestfirst'] = 'Display replies flat, with newest first'; $string['modeflatnewestfirst'] = 'Display replies flat, with newest first';
$string['modeflatoldestfirst'] = 'Display replies flat, with oldest first'; $string['modeflatoldestfirst'] = 'Display replies flat, with oldest first';
$string['modemodern'] = 'Display replies in modern form';
$string['modenested'] = 'Display replies in nested form'; $string['modenested'] = 'Display replies in nested form';
$string['modethreaded'] = 'Display replies in threaded form'; $string['modethreaded'] = 'Display replies in threaded form';
$string['modulename'] = 'Forum'; $string['modulename'] = 'Forum';
@ -555,6 +563,8 @@ $string['replies'] = 'Replies';
$string['repliesmany'] = '{$a} replies so far'; $string['repliesmany'] = '{$a} replies so far';
$string['repliesone'] = '{$a} reply so far'; $string['repliesone'] = '{$a} reply so far';
$string['reply'] = 'Reply'; $string['reply'] = 'Reply';
$string['replyauthorself'] = '{$a} (you)';
$string['replyingtoauthor'] = 'Replying to {$a}...';
$string['replyplaceholder'] = 'Write your reply...'; $string['replyplaceholder'] = 'Write your reply...';
$string['replyforum'] = 'Reply to forum'; $string['replyforum'] = 'Reply to forum';
$string['replytopostbyemail'] = 'You can reply to this via email.'; $string['replytopostbyemail'] = 'You can reply to this via email.';
@ -595,6 +605,7 @@ $string['seeallposts'] = 'See all posts made by this user';
$string['settings'] = 'Settings'; $string['settings'] = 'Settings';
$string['shortpost'] = 'Short post'; $string['shortpost'] = 'Short post';
$string['showingcountoftotaldiscussions'] = 'Showing {$a->count} of {$a->total} discussions'; $string['showingcountoftotaldiscussions'] = 'Showing {$a->count} of {$a->total} discussions';
$string['showpreviousrepliescount'] = 'Show previous replies ({$a})';
$string['showsubscribers'] = 'Show/edit current subscribers'; $string['showsubscribers'] = 'Show/edit current subscribers';
$string['singleforum'] = 'A single simple discussion'; $string['singleforum'] = 'A single simple discussion';
$string['smallmessage'] = '{$a->user} posted in {$a->forumname}'; $string['smallmessage'] = '{$a->user} posted in {$a->forumname}';
@ -649,6 +660,7 @@ If set to optional, participants can choose whether to turn tracking on or off v
If \'Allow forced read tracking\' is enabled in the site administration, then a further option is available - forced. This means that tracking is always on, regardless of users\' forum preferences.'; If \'Allow forced read tracking\' is enabled in the site administration, then a further option is available - forced. This means that tracking is always on, regardless of users\' forum preferences.';
$string['unlockdiscussion'] = 'Unlock this discussion'; $string['unlockdiscussion'] = 'Unlock this discussion';
$string['unread'] = 'Unread'; $string['unread'] = 'Unread';
$string['unreadpost'] = 'Unread post';
$string['unreadposts'] = 'Unread posts'; $string['unreadposts'] = 'Unread posts';
$string['unreadpostsnumber'] = '{$a} unread posts'; $string['unreadpostsnumber'] = '{$a} unread posts';
$string['unreadpostsone'] = '1 unread post'; $string['unreadpostsone'] = '1 unread post';

View file

@ -32,6 +32,7 @@ define('FORUM_MODE_FLATOLDEST', 1);
define('FORUM_MODE_FLATNEWEST', -1); define('FORUM_MODE_FLATNEWEST', -1);
define('FORUM_MODE_THREADED', 2); define('FORUM_MODE_THREADED', 2);
define('FORUM_MODE_NESTED', 3); define('FORUM_MODE_NESTED', 3);
define('FORUM_MODE_MODERN', 4);
define('FORUM_CHOOSESUBSCRIBE', 0); define('FORUM_CHOOSESUBSCRIBE', 0);
define('FORUM_FORCESUBSCRIBE', 1); define('FORUM_FORCESUBSCRIBE', 1);
@ -5184,7 +5185,8 @@ function forum_get_layout_modes() {
return array (FORUM_MODE_FLATOLDEST => get_string('modeflatoldestfirst', 'forum'), return array (FORUM_MODE_FLATOLDEST => get_string('modeflatoldestfirst', 'forum'),
FORUM_MODE_FLATNEWEST => get_string('modeflatnewestfirst', 'forum'), FORUM_MODE_FLATNEWEST => get_string('modeflatnewestfirst', 'forum'),
FORUM_MODE_THREADED => get_string('modethreaded', 'forum'), FORUM_MODE_THREADED => get_string('modethreaded', 'forum'),
FORUM_MODE_NESTED => get_string('modenested', 'forum')); FORUM_MODE_NESTED => get_string('modenested', 'forum'),
FORUM_MODE_MODERN => get_string('modemodern', 'forum'));
} }
/** /**

View file

@ -25,7 +25,7 @@
} }
}} }}
<div class="ml-auto dropdown"> <div class="ml-auto dropdown">
<button class="{{^settings.excludetext}}dropdown-toggle{{/settings.excludetext}} m-t-0 p-t-0 btn btn-link btn-icon" <button class="{{^settings.excludetext}}dropdown-toggle{{/settings.excludetext}} m-t-0 p-t-0 btn btn-link"
type="button" type="button"
role="button" role="button"
data-toggle="dropdown" data-toggle="dropdown"

View file

@ -0,0 +1,70 @@
{{!
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 mod_forum/forum_discussion_modern
Template for displaying a single forum discussion.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
<div id="discussion-container-{{uniqid}}" data-content="forum-discussion">
{{#html}}
<div class="mb-5">
<div class="d-flex flex-wrap">
<div>{{{modeselectorform}}}</div>
<div class="ml-auto d-flex align-items-middle" data-container="discussion-tools">
<div>{{{subscribe}}}</div>
<div class="pl-1">{{> mod_forum/forum_action_menu}}</div>
</div>
</div>
<div class="d-flex mt-2">
<div>{{{movediscussion}}}</div>
<div {{#movediscussion}}class="ml-2"{{/movediscussion}}>{{{exportdiscussion}}}</div>
</div>
</div>
{{/html}}
{{#notifications}}
{{> core/notification}}
{{/notifications}}
{{{html.posts}}}
</div>
{{#js}}
require(['jquery', 'mod_forum/discussion_modern'], function($, Discussion) {
var root = $('#discussion-container-{{uniqid}}');
Discussion.init(root, {
{{#loggedinuser}}
loggedinuser: {
profileimageurl: '{{{profileimageurl}}}',
fullname: '{{fullname}}',
firstname: '{{firstname}}'
}
{{/loggedinuser}}
});
});
{{/js}}

View file

@ -0,0 +1,362 @@
{{!
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 mod_forum/forum_discussion_modern_first_post
Template to render a single post from a discussion.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
<article
id="p{{id}}"
class="forum-post-container"
data-post-id="{{id}}"
data-region="post"
data-target="{{id}}-target"
tabindex="0"
aria-labelledby="post-header-{{id}}"
aria-describedby="post-content-{{id}}"
>
<!-- The firstpost and starter classes below aren't used for anything other than to identify the first post in behat -->
<div
class="d-flex focus-target mb-4 {{#firstpost}}firstpost starter{{/firstpost}}"
aria-label='{{#str}} postbyuser, mod_forum, {"post": "{{subject}}", "user": "{{author.fullname}}"} {{/str}}'
data-post-id="{{id}}" data-content="forum-post"
>
{{#isfirstunread}}<a id="unread" aria-hidden="true"></a>{{/isfirstunread}}
<div class="author-image-container d-inline-block pt-2">
{{^isdeleted}}
{{#author}}
{{#urls.profileimage}}
<img
class="rounded-circle w-100"
src="{{{.}}}"
alt="{{#str}} pictureof, core, {{author.fullname}} {{/str}}"
aria-hidden="true"
>
{{/urls.profileimage}}
{{/author}}
{{/isdeleted}}
{{#unread}}
<div class="icon-size-4 text-info text-center mt-3 icon-no-margin">
<span
data-toggle="tooltip"
data-placement="left"
title="{{#str}} unreadpost, mod_forum {{/str}}"
tabindex="0"
>{{#pix}} i/flagged, core {{/pix}}</span>
</div>
{{/unread}}
</div>
<div class="d-flex flex-column w-100" data-region-content="forum-post-core">
<header id="post-header-{{uniqid}}">
{{^isdeleted}}
<div class="d-flex flex-wrap align-items-center mb-3">
<address class="mb-0 mr-2" tabindex="-1">
{{#author}}
<h4 class="h6 d-lg-inline-block mb-0 author-header mr-1">
{{#parentauthorname}}
{{#isprivatereply}}
<span class="text-danger">
{{#str}}
authorreplyingprivatelytoauthor, mod_forum, {
"respondant":"<a class='font-weight-bold author-name' data-region='author-name' href='{{{urls.profile}}}'>{{fullname}}</a>",
"author":"{{parentauthorname}}"
}
{{/str}}
</span>
{{/isprivatereply}}
{{^isprivatereply}}
{{#str}}
authorreplyingtoauthor, mod_forum, {
"respondant":"<a class='font-weight-bold author-name' data-region='author-name' href='{{{urls.profile}}}'>{{fullname}}</a>",
"author":"{{parentauthorname}}"
}
{{/str}}
{{/isprivatereply}}
{{/parentauthorname}}
{{^parentauthorname}}
<a class='font-weight-bold author-name' data-region='author-name' href="{{{urls.profile}}}">{{fullname}}</a>
{{/parentauthorname}}
</h4>
{{/author}}
<time class="text-muted">
{{#userdate}} {{timecreated}}, {{#str}} strftimerecentfull, core_langconfig {{/str}} {{/userdate}}
</time>
</address>
<div class="d-flex align-items-center ml-auto">
{{#author.groups}}
{{#urls.image}}
<div class="mr-2">
{{#urls.group}}
<a href="{{urls.group}}" aria-label='{{#str}} memberofgroup, group, {{name}}{{/str}}'>
<img
class="rounded-circle group-image"
src="{{{urls.image}}}"
alt="{{#str}} pictureof, core, {{name}} {{/str}}"
aria-hidden="true"
title="{{#str}} pictureof, core, {{name}} {{/str}}"
>
</a>
{{/urls.group}}
{{^urls.group}}
<img class="rounded-circle group-image"
src="{{{urls.image}}}"
alt="{{#str}} pictureof, core, {{name}} {{/str}}"
title="{{#str}} pictureof, core, {{name}} {{/str}}"
>
{{/urls.group}}
</div>
{{/urls.image}}
{{/author.groups}}
{{^readonly}}
{{#capabilities.view}}
<a
href="{{{urls.view}}}"
class="d-inline-block mr-1 icon-no-margin"
title="{{#str}} permanentlinktopost, mod_forum {{/str}}"
>
{{#pix}} e/insert_edit_link, core {{/pix}}
</a>
{{/capabilities.view}}
{{#showactionmenu}}
<div class="dropdown">
<button
class="btn btn-icon text-muted icon-no-margin icon-size-3"
type="button"
id="post-actions-menu-{{uniqid}}"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
aria-label="{{#str}} actionsforpost, mod_forum {{/str}}"
>
{{#pix}} i/moremenu {{/pix}}
</button>
<!-- inline style to fix RTL placement bug -->
<div class="dropdown-menu" aria-labelledby="post-actions-menu-{{uniqid}}" style="right: auto">
{{#capabilities}}
{{#controlreadstatus}}
{{#unread}}
<a
href="{{{urls.markasread}}}"
class="dropdown-item"
role="menuitem"
>
{{#str}} markread, mod_forum {{/str}}
</a>
{{/unread}}
{{^unread}}
<a
href="{{{urls.markasunread}}}"
class="dropdown-item"
role="menuitem"
>
{{#str}} markunread, mod_forum {{/str}}
</a>
{{/unread}}
{{/controlreadstatus}}
{{#urls.viewparent}}
<a
href="{{{.}}}"
class="dropdown-item"
title="{{#str}} permanentlinktoparentpost, mod_forum {{/str}}"
role="menuitem"
>
{{#str}} parent, mod_forum {{/str}}
</a>
{{/urls.viewparent}}
{{#edit}}
<a
href="{{{urls.edit}}}"
class="dropdown-item"
role="menuitem"
>
{{#str}} edit, mod_forum {{/str}}
</a>
{{/edit}}
{{#split}}
<a
href="{{{urls.split}}}"
class="dropdown-item"
role="menuitem"
>
{{#str}} prune, mod_forum {{/str}}
</a>
{{/split}}
{{#delete}}
<a
href="{{{urls.delete}}}"
class="dropdown-item"
role="menuitem"
>
{{#str}} delete, mod_forum {{/str}}
</a>
{{/delete}}
{{#export}}
<a
href="{{{urls.export}}}"
class="dropdown-item"
role="menuitem"
>
{{#str}} addtoportfolio, core_portfolio {{/str}}
</a>
{{/export}}
{{/capabilities}}
</div>
</div>
{{/showactionmenu}}
{{/readonly}}
</div>
</div>
{{/isdeleted}}
{{$subject}}
<h2
class="h1 font-weight-bold post-subject mt-n2"
data-region-content="forum-post-core-subject"
data-reply-subject="{{replysubject}}"
>{{{subject}}}</h2>
{{/subject}}
{{#hasreplycount}}
<span class="sr-only">{{#str}} numberofreplies, mod_forum, {{replycount}} {{/str}}</span>
{{/hasreplycount}}
</header>
<div class="post-message" id="post-content-{{id}}">
{{{message}}}
</div>
{{^isdeleted}}
{{#attachments}}
{{#isimage}}
<div class="attachedimages">
<img
src="{{{url}}}"
alt="{{#str}} attachmentname, mod_forum, {{filename}} {{/str}}"
style="max-width: 100%"
>
{{#urls.export}}
<a href="{{{.}}}" title="{{#str}} addtoportfolio, core_portfolio {{/str}}">
{{#pix}} t/portfolioadd, core {{/pix}}
</a>
{{/urls.export}}
{{#html.plagiarism}}
<div>{{{.}}}</div>
{{/html.plagiarism}}
</div>
{{/isimage}}
{{/attachments}}
{{#attachments}}
{{^isimage}}
<div class="mt-3">
<span class="icon-size-5">{{#pix}} {{icon}}, core {{/pix}}</span>
<div class="align-bottom d-inline-block">
<a
href="{{{url}}}"
aria-label="{{#str}} attachmentname, mod_forum, {{filename}} {{/str}}"
class="font-weight-bold"
>
{{#str}} attachmentnameandfilesize, mod_forum, {"name": "{{filename}}", "size": "{{filesizeformatted}}"} {{/str}}
</a>
{{#urls.export}}
<a class="icon-no-margin" href="{{{.}}}" title="{{#str}} exportattachmentname, mod_forum, {{filename}} {{/str}}">
{{#pix}} t/portfolioadd, core {{/pix}}
</a>
{{/urls.export}}
{{#html.plagiarism}}
<div>{{{.}}}</div>
{{/html.plagiarism}}
</div>
</div>
{{/isimage}}
{{/attachments}}
<div class="d-flex mt-3 align-items-center">
{{#html.rating}}
<div>{{{.}}}</div>
{{/html.rating}}
<div class="ml-auto d-flex flex-column">
{{#haswordcount}}
<span class="ml-auto badge badge-light">
{{#str}} numwords, core, {{wordcount}} {{/str}}
</span>
{{/haswordcount}}
{{#html.taglist}}
<div class="d-inline-block ml-auto {{#haswordcount}}mt-2{{/haswordcount}}">{{{.}}}</div>
{{/html.taglist}}
</div>
</div>
{{/isdeleted}}
{{$footer}}
{{^isdeleted}}
{{^readonly}}
<div class="d-flex mt-3">
{{#capabilities.reply}}
<button
class="btn btn-primary btn-lg"
data-href="{{{urls.reply}}}"
data-post-id="{{id}}"
data-action="create-inpage-reply"
data-can-reply-privately="{{capabilities.canreplyprivately}}"
>
{{#str}} reply, mod_forum {{/str}}
</button>
{{/capabilities.reply}}
{{^capabilities.reply}}
{{#discussionlocked}}
<button class="btn btn-secondary btn-lg disabled" disabled>
{{#str}} reply, mod_forum {{/str}}
</button>
{{/discussionlocked}}
{{/capabilities.reply}}
</div>
{{/readonly}}
{{/isdeleted}}
{{#discussionlocked}}
<div><span class="badge badge-danger mt-2">{{#str}} locked, mod_forum {{/str}}</span></div>
{{/discussionlocked}}
{{/footer}}
</div>
</div>
{{$replies}}
<div class="indent inline-reply-container" data-region="inpage-reply-container"></div>
<div class="indent replies-container" data-region="replies-container">
{{#hasreplies}}
{{#replies}}
{{> mod_forum/forum_discussion_modern_post_reply }}
{{/replies}}
{{/hasreplies}}
</div>
{{/replies}}
</article>

View file

@ -0,0 +1,85 @@
{{!
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 mod_forum/forum_discussion_post
Template to render a single post from a discussion.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
{{< mod_forum/forum_discussion_modern_first_post }}
{{$subject}}
<h3
{{#isdeleted}}class="h6 font-weight-bold"{{/isdeleted}}
{{^isdeleted}}class="sr-only"{{/isdeleted}}
data-region-content="forum-post-core-subject"
>{{{subject}}}</h3>
{{/subject}}
{{$footer}}
{{^isdeleted}}
{{^readonly}}
{{#capabilities.reply}}
<div class="d-flex mt-1">
<button
class="font-weight-bold btn btn-link btn-lg px-0"
data-href="{{{urls.reply}}}"
data-post-id="{{id}}"
data-action="create-inpage-reply"
data-can-reply-privately="{{capabilities.canreplyprivately}}"
>
{{#str}} reply, mod_forum {{/str}}
</button>
</div>
{{/capabilities.reply}}
{{/readonly}}
{{/isdeleted}}
{{/footer}}
{{$replies}}
<div class="indent my-4" data-region="replies-visibility-toggle-container" style="display: none">
<button class="btn btn-link pl-0" data-action="show-replies">
{{#str}}
showpreviousrepliescount,
mod_forum,
<span data-region="reply-count">{{#totalreplycount}}{{.}}{{/totalreplycount}}{{^totalreplycount}}0{{/totalreplycount}}</span>
{{/str}}
</button>
<button class="btn btn-link hidden pl-0" data-action="hide-replies">
{{#str}}
hidepreviousrepliescount,
mod_forum,
<span data-region="reply-count">{{#totalreplycount}}{{.}}{{/totalreplycount}}{{^totalreplycount}}0{{/totalreplycount}}</span>
{{/str}}
</button>
</div>
<div class="indent replies-container" data-region="replies-container">
{{#hasreplies}}
{{#replies}}
{{> mod_forum/forum_discussion_modern_post_reply }}
{{/replies}}
{{/hasreplies}}
</div>
<div class="indent inline-reply-container" data-region="inpage-reply-container"></div>
{{/replies}}
{{/ mod_forum/forum_discussion_modern_first_post }}

View file

@ -0,0 +1,34 @@
{{!
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 mod_forum/forum_discussion_modern_posts
Template to render a list of posts for a discussion.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
{{#posts}}
{{> mod_forum/forum_discussion_modern_first_post }}
{{/posts}}

View file

@ -31,7 +31,7 @@
}} }}
{{< mod_forum/forum_discussion_post }} {{< mod_forum/forum_discussion_post }}
{{$replies}} {{$replies}}
<div class="indent"> <div class="indent" data-region="replies-container">
{{#replies}} {{#replies}}
{{> mod_forum/forum_discussion_nested_post }} {{> mod_forum/forum_discussion_nested_post }}
{{/replies}} {{/replies}}

View file

@ -314,15 +314,13 @@
</div> </div>
</div> </div>
<div data-region="replies-container">
{{$replies}} {{$replies}}
<div> <div data-region="replies-container">
{{#hasreplies}} {{#hasreplies}}
{{#replies}} {{#replies}}
{{> mod_forum/forum_discussion_post }} {{> mod_forum/forum_discussion_post }}
{{/replies}} {{/replies}}
{{/hasreplies}} {{/hasreplies}}
</div> </div>
{{/replies}} {{/replies}}
</div>
</article> </article>

View file

@ -34,7 +34,7 @@
{{< mod_forum/forum_discussion_post }} {{< mod_forum/forum_discussion_post }}
{{$replies}} {{$replies}}
<!-- The forumthread class is only added for behat --> <!-- The forumthread class is only added for behat -->
<div class="indent forumthread post-replies"> <div class="indent forumthread post-replies" data-region="replies-container">
{{#replies}} {{#replies}}
{{> mod_forum/forum_discussion_threaded_post }} {{> mod_forum/forum_discussion_threaded_post }}
{{/replies}} {{/replies}}

View file

@ -0,0 +1,104 @@
{{!
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 mod_forum/inpage_reply_modern
In page reply HTML for the "modern" discussion display mode.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
"postid": 0,
"reply_url": "",
"sesskey": "",
"parentsubject": ""
}
}}
<div
class="mt-4 mb-4"
data-content="inpage-reply-content"
style="display: none;"
>
<div class="d-flex">
<div class="author-image-container">
{{#loggedinuser}}
<img
class="rounded-circle w-100"
src="{{{profileimageurl}}}"
alt="{{#str}} pictureof, core, {{fullname}} {{/str}}"
aria-hidden="true"
>
{{/loggedinuser}}
</div>
<div class="w-100">
{{#loggedinuser}}
<h4 class="h5 font-weight-bold reply-author">{{#str}} replyauthorself, mod_forum, {{firstname}}{{/str}}</h4>
{{/loggedinuser}}
<form data-post-id="{{postid}}" data-content="inpage-reply-form" action="{{{reply_url}}}">
<textarea
name="post"
rows="3"
class="w-100"
{{#parentauthorname}}
placeholder="{{#str}} replyingtoauthor, forum, {{.}} {{/str}}"
{{/parentauthorname}}
{{^parentauthorname}}
placeholder="{{#str}} replyplaceholder, forum {{/str}}"
{{/parentauthorname}}
data-auto-rows
data-min-rows="3"
data-max-rows="10"
></textarea>
<input type="hidden" name="postformat" value="{{postformat}}"/>
<input type="hidden" name="subject" value="{{#str}} inpagereplysubject, forum, {{parentsubject}} {{/str}}"/>
<input type="hidden" name="reply" value="{{postid}}"/>
<input type="hidden" name="sesskey" value="{{sesskey}}"/>
<div class="d-flex mt-3 align-items-center flex-wrap">
<button class="btn btn-primary" data-action="forum-inpage-submit">
<span data-region="submit-text">{{#str}} post, core {{/str}}</span>
<span data-region="loading-icon-container" class="hidden">{{> core/loading }}</span>
</button>
<button data-action="forum-advanced-reply" class="btn btn-link mr-auto" type="submit">
{{#str}} advanced, core {{/str}}
</button>
{{#canreplyprivately}}
<div class="form-check form-check-inline">
<span class="switch">
<input name="privatereply" type="checkbox" id="private-reply-checkbox-{{uniqid}}">
<label class="mb-0" for="private-reply-checkbox-{{uniqid}}">
{{#str}} privatereply, forum {{/str}}
</label>
</span>
</div>
{{/canreplyprivately}}
<button
class="btn btn-icon icon-no-margin icon-size-4 text-muted"
title="{{#str}} cancelreply, mod_forum {{/str}}"
data-action="cancel-inpage-reply"
>
{{#pix}} i/delete, core {{/pix}}
</button>
</div>
</form>
</div>
</div>
</div>

View file

@ -47,6 +47,7 @@ class mod_forum_entities_author_testcase extends advanced_testcase {
'person', 'person',
'test person', 'test person',
'test@example.com', 'test@example.com',
false,
'middle', 'middle',
'tteeeeest', 'tteeeeest',
'ppppeeerssson', 'ppppeeerssson',
@ -60,6 +61,7 @@ class mod_forum_entities_author_testcase extends advanced_testcase {
$this->assertEquals('person', $author->get_last_name()); $this->assertEquals('person', $author->get_last_name());
$this->assertEquals('test person', $author->get_full_name()); $this->assertEquals('test person', $author->get_full_name());
$this->assertEquals('test@example.com', $author->get_email()); $this->assertEquals('test@example.com', $author->get_email());
$this->assertEquals(false, $author->is_deleted());
$this->assertEquals('middle', $author->get_middle_name()); $this->assertEquals('middle', $author->get_middle_name());
$this->assertEquals('tteeeeest', $author->get_first_name_phonetic()); $this->assertEquals('tteeeeest', $author->get_first_name_phonetic());
$this->assertEquals('ppppeeerssson', $author->get_last_name_phonetic()); $this->assertEquals('ppppeeerssson', $author->get_last_name_phonetic());

View file

@ -49,7 +49,8 @@ class mod_forum_entities_discussion_summary_testcase extends advanced_testcase {
'test', 'test',
'person', 'person',
'test person', 'test person',
'test@example.com' 'test@example.com',
false
); );
$lastauthor = new author_entity( $lastauthor = new author_entity(
2, 2,
@ -57,7 +58,8 @@ class mod_forum_entities_discussion_summary_testcase extends advanced_testcase {
'test 2', 'test 2',
'person 2', 'person 2',
'test 2 person 2', 'test 2 person 2',
'test2@example.com' 'test2@example.com',
false
); );
$discussion = new discussion_entity( $discussion = new discussion_entity(
1, 1,

View file

@ -57,7 +57,8 @@ class mod_forum_exporters_author_testcase extends advanced_testcase {
'test', 'test',
'user', 'user',
'test user', 'test user',
'test@example.com' 'test@example.com',
false
); );
$exporter = new author_exporter($author, 1, [], true, [ $exporter = new author_exporter($author, 1, [], true, [
@ -95,7 +96,8 @@ class mod_forum_exporters_author_testcase extends advanced_testcase {
'test', 'test',
'user', 'user',
'test user', 'test user',
'test@example.com' 'test@example.com',
false
); );
$group = $datagenerator->create_group(['courseid' => $course->id]); $group = $datagenerator->create_group(['courseid' => $course->id]);
@ -132,7 +134,8 @@ class mod_forum_exporters_author_testcase extends advanced_testcase {
'test', 'test',
'user', 'user',
'test user', 'test user',
'test@example.com' 'test@example.com',
false
); );
$group = $datagenerator->create_group(['courseid' => $course->id]); $group = $datagenerator->create_group(['courseid' => $course->id]);

View file

@ -548,6 +548,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
$exporteduser2 = [ $exporteduser2 = [
'id' => (int) $user2->id, 'id' => (int) $user2->id,
'fullname' => fullname($user2), 'fullname' => fullname($user2),
'isdeleted' => false,
'groups' => [], 'groups' => [],
'urls' => [ 'urls' => [
'profile' => $urlfactory->get_author_profile_url($user2entity), 'profile' => $urlfactory->get_author_profile_url($user2entity),
@ -562,6 +563,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
'id' => (int) $user3->id, 'id' => (int) $user3->id,
'fullname' => fullname($user3), 'fullname' => fullname($user3),
'groups' => [], 'groups' => [],
'isdeleted' => false,
'urls' => [ 'urls' => [
'profile' => $urlfactory->get_author_profile_url($user3entity), 'profile' => $urlfactory->get_author_profile_url($user3entity),
'profileimage' => $urlfactory->get_author_profile_image_url($user3entity), 'profileimage' => $urlfactory->get_author_profile_image_url($user3entity),
@ -642,6 +644,16 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
// Delete one user, to test that we still receive posts by this user. // Delete one user, to test that we still receive posts by this user.
delete_user($user3); delete_user($user3);
$exporteduser3 = [
'id' => (int) $user3->id,
'fullname' => get_string('deleteduser', 'mod_forum'),
'groups' => [],
'isdeleted' => true,
'urls' => [
'profile' => $urlfactory->get_author_profile_url($user3entity),
'profileimage' => $urlfactory->get_author_profile_image_url($user3entity),
]
];
// Create what we expect to be returned when querying the discussion. // Create what we expect to be returned when querying the discussion.
$expectedposts = array( $expectedposts = array(

View file

@ -76,12 +76,24 @@ $cm = \cm_info::create($coursemodule);
require_course_login($course, true, $cm); require_course_login($course, true, $cm);
$istypesingle = 'single' === $forum->get_type();
if ($mode) {
set_user_preference('forum_displaymode', $mode);
}
$displaymode = get_user_preferences('forum_displaymode', $CFG->forum_displaymode);
$PAGE->set_context($forum->get_context()); $PAGE->set_context($forum->get_context());
$PAGE->set_title($forum->get_name()); $PAGE->set_title($forum->get_name());
$PAGE->add_body_class('forumtype-' . $forum->get_type()); $PAGE->add_body_class('forumtype-' . $forum->get_type());
$PAGE->set_heading($course->fullname); $PAGE->set_heading($course->fullname);
$PAGE->set_button(forum_search_form($course, $search)); $PAGE->set_button(forum_search_form($course, $search));
if ($istypesingle && $displaymode == FORUM_MODE_MODERN) {
$PAGE->add_body_class('modern-display-mode reset-style');
}
if (empty($cm->visible) && !has_capability('moodle/course:viewhiddenactivities', $forum->get_context())) { if (empty($cm->visible) && !has_capability('moodle/course:viewhiddenactivities', $forum->get_context())) {
redirect( redirect(
$urlfactory->get_course_url_from_forum($forum), $urlfactory->get_course_url_from_forum($forum),
@ -125,16 +137,10 @@ if (!empty($CFG->enablerssfeeds) && !empty($CFG->forum_enablerssfeeds) && $forum
echo $OUTPUT->header(); echo $OUTPUT->header();
echo $OUTPUT->heading(format_string($forum->get_name()), 2); echo $OUTPUT->heading(format_string($forum->get_name()), 2);
if ('single' !== $forum->get_type() && !empty($forum->get_intro())) { if (!$istypesingle && !empty($forum->get_intro())) {
echo $OUTPUT->box(format_module_intro('forum', $forumrecord, $cm->id), 'generalbox', 'intro'); echo $OUTPUT->box(format_module_intro('forum', $forumrecord, $cm->id), 'generalbox', 'intro');
} }
if ($mode) {
set_user_preference('forum_displaymode', $mode);
}
$displaymode = get_user_preferences('forum_displaymode', $CFG->forum_displaymode);
if ($sortorder) { if ($sortorder) {
set_user_preference('forum_discussionlistsortorder', $sortorder); set_user_preference('forum_discussionlistsortorder', $sortorder);
} }

View file

@ -1,2 +1,2 @@
define ("theme_boost/loader",["jquery","./tether","core/event"],function(a,b,c){window.jQuery=a;window.Tether=b;M.util.js_pending("theme_boost/loader:children");require(["theme_boost/aria","theme_boost/pending","theme_boost/util","theme_boost/alert","theme_boost/button","theme_boost/carousel","theme_boost/collapse","theme_boost/dropdown","theme_boost/modal","theme_boost/scrollspy","theme_boost/tab","theme_boost/tooltip","theme_boost/popover"],function(b){a("body").popover({trigger:"focus",selector:"[data-toggle=popover][data-trigger!=hover]"});a("html").popover({container:"body",selector:"[data-toggle=popover][data-trigger=hover]",trigger:"hover",delay:{hide:500}});a.fn.dropdown.Constructor.Default.flip=!1;a("a[data-toggle=\"tab\"]").on("shown.bs.tab",function(b){var c=a(b.target).attr("href");if(history.replaceState){history.replaceState(null,null,c)}else{location.hash=c}});var d=window.location.hash;if(d){a(".nav-link[href=\""+d+"\"]").tab("show")}c.getLegacyEvents().done(function(b){a(document).on(b.FILTER_CONTENT_UPDATED,function(){a("body").popover({selector:"[data-toggle=\"popover\"]",trigger:"focus"})})});b.init();M.util.js_complete("theme_boost/loader:children")});return{}}); define ("theme_boost/loader",["jquery","./tether","core/event"],function(a,b,c){window.jQuery=a;window.Tether=b;M.util.js_pending("theme_boost/loader:children");require(["theme_boost/aria","theme_boost/pending","theme_boost/util","theme_boost/alert","theme_boost/button","theme_boost/carousel","theme_boost/collapse","theme_boost/dropdown","theme_boost/modal","theme_boost/scrollspy","theme_boost/tab","theme_boost/tooltip","theme_boost/popover"],function(b){a("body").popover({trigger:"focus",selector:"[data-toggle=popover][data-trigger!=hover]"});a("html").popover({container:"body",selector:"[data-toggle=popover][data-trigger=hover]",trigger:"hover",delay:{hide:500}});a("html").tooltip({container:"body",selector:"[data-toggle=\"tooltip\"]"});a.fn.dropdown.Constructor.Default.flip=!1;a("a[data-toggle=\"tab\"]").on("shown.bs.tab",function(b){var c=a(b.target).attr("href");if(history.replaceState){history.replaceState(null,null,c)}else{location.hash=c}});var d=window.location.hash;if(d){a(".nav-link[href=\""+d+"\"]").tab("show")}c.getLegacyEvents().done(function(b){a(document).on(b.FILTER_CONTENT_UPDATED,function(){a("body").popover({selector:"[data-toggle=\"popover\"]",trigger:"focus"})})});b.init();M.util.js_complete("theme_boost/loader:children")});return{}});
//# sourceMappingURL=loader.min.js.map //# sourceMappingURL=loader.min.js.map

File diff suppressed because one or more lines are too long

View file

@ -59,6 +59,11 @@ define(['jquery', './tether', 'core/event'], function(jQuery, Tether, Event) {
} }
}); });
jQuery("html").tooltip({
container: "body",
selector: '[data-toggle="tooltip"]'
});
// Disables flipping the dropdowns up and getting hidden behind the navbar. // Disables flipping the dropdowns up and getting hidden behind the navbar.
jQuery.fn.dropdown.Constructor.Default.flip = false; jQuery.fn.dropdown.Constructor.Default.flip = false;

View file

@ -46,3 +46,23 @@ p.arrow_button {
margin: 0 0 10px 5px; margin: 0 0 10px 5px;
} }
.btn.btn-icon {
@extend .btn-link;
height: $icon-width;
width: $icon-width;
padding: 0;
border-radius: 50%;
flex-shrink: 0;
@include hover-focus {
background-color: $gray-200;
}
@each $size, $length in $iconsizes {
&.icon-size-#{$size} {
height: ($length + 20px) !important; /* stylelint-disable-line declaration-no-important */
width: ($length + 20px) !important; /* stylelint-disable-line declaration-no-important */
}
}
}

View file

@ -2158,13 +2158,13 @@ div.editor_atto_toolbar button .icon {
} }
} }
$switch-height: (1rem * .8) !default; $switch-height: 1.25rem !default;
$switch-height-half: ($switch-height / 2) !default; $switch-height-half: ($switch-height / 2) !default;
$switch-border-radius: $switch-height !default; $switch-border-radius: $switch-height !default;
$switch-bg: $custom-control-indicator-bg !default; $switch-bg: $gray-300 !default;
$switch-checked-bg: map-get($theme-colors, 'primary') !default; $switch-checked-bg: map-get($theme-colors, 'primary') !default;
$switch-disabled-bg: $custom-control-indicator-disabled-bg !default; $switch-disabled-bg: $gray-200 !default;
$switch-disabled-color: $custom-control-label-disabled-color !default; $switch-disabled-color: $gray-600 !default;
$switch-thumb-bg: $white !default; $switch-thumb-bg: $white !default;
$switch-thumb-border-radius: 50% !default; $switch-thumb-border-radius: 50% !default;
$switch-thumb-padding: 2px !default; $switch-thumb-padding: 2px !default;

View file

@ -173,6 +173,208 @@ select {
} }
} }
$author-image-width: 70px;
$author-image-margin: 24px;
$author-image-width-sm: 30px;
$author-image-margin-sm: 8px;
/** Gently highlight the selected post by changing it's background to blue and then fading it out. */
@keyframes background-highlight {
from {
background-color: rgba(0, 123, 255, 0.5);
}
to {
background-color: inherit;
}
}
.path-mod-forum.modern-display-mode {
.discussionsubscription {
margin-top: 0;
text-align: inherit;
margin-bottom: 0;
}
.preload-subscribe,
.preload-unsubscribe {
display: none;
}
.post-message {
line-height: 1.6;
}
.indent {
margin-left: 0;
}
/** Reset the badge styling back to pill style. */
.badge {
font-size: inherit;
font-weight: inherit;
padding-left: .5rem;
padding-right: .5rem;
border-radius: 10rem;
}
.badge-light {
background-color: #f6f6f6;
color: #5b5b5b;
}
/** Style the ratings like a badge. */
.rating-aggregate-container {
background-color: #f6f6f6;
color: #5b5b5b;
padding: .25em .5em;
line-height: 1;
margin-right: .5rem;
vertical-align: middle;
border-radius: 10rem;
text-align: center;
}
.ratinginput {
padding: .25em 1.75rem 0.25em .75em;
line-height: 1;
height: auto;
border-radius: 10rem;
}
.group-image {
width: 35px;
height: 35px;
margin-right: 0;
float: none;
display: inline-block;
}
/** Don't show the discussion locked alert in this mode because it's already indicated with a badge. */
.alert.discussionlocked {
@extend .sr-only;
}
/** Fix muted text contrast ratios for accessibility. */
.text-muted,
.dimmed_text {
color: #707070 !important; /* stylelint-disable-line declaration-no-important */
}
.author-header {
font-style: italic;
.author-name {
font-style: normal;
}
}
/** Make the tag list text screen reader visible only */
.tag_list > b {
@extend .sr-only;
}
:target > .focus-target {
animation-name: background-highlight;
animation-duration: 1s;
animation-timing-function: ease-in-out;
animation-iteration-count: 1;
}
.forum-post-container {
.replies-container {
.forum-post-container {
border-top: 1px solid #dee2e6;
padding-top: 1.5rem;
.replies-container .forum-post-container {
border-top: none;
padding-top: 0;
}
}
.inline-reply-container .reply-author {
display: none;
}
}
.post-message p:last-of-type {
margin-bottom: 0;
}
.author-image-container {
width: $author-image-width;
margin-right: $author-image-margin;
flex-shrink: 0;
}
.inline-reply-container textarea {
border: 0;
resize: none;
}
.indent {
/**
* The first post and first set of replies have a larger author image so offset the 2nd
* set of replies by the image width + margin to ensure they align.
*/
.indent {
padding-left: $author-image-width + $author-image-margin;
/**
* Reduce the size of the the author image for all second level replies (and below).
*/
.author-image-container {
width: $author-image-width-sm;
margin-right: $author-image-margin-sm;
}
/**
* Adjust the indentation offset for all 3rd level replies and below for the smaller author image.
*/
.indent {
padding-left: $author-image-width-sm + $author-image-margin-sm;
/**
* Stop indenting the replies after the 5th reply.
*/
.indent .indent .indent {
padding-left: 0;
}
}
}
}
}
}
/** Extra small devices (portrait phones, less than 576px). */
@include media-breakpoint-down(sm) {
#page-mod-forum-discuss.modern-display-mode {
.forum-post-container {
.author-image-container {
width: $author-image-width-sm;
margin-right: $author-image-margin-sm;
}
.indent {
.indent {
padding-left: $author-image-width-sm + $author-image-margin-sm;
.indent .indent {
padding-left: 0;
}
}
}
}
.group-image {
width: 30px;
height: 30px;
}
}
}
// End styling for mod_forum.
.maincalendar .calendarmonth td, .maincalendar .calendarmonth td,
.maincalendar .calendarmonth th { .maincalendar .calendarmonth th {
border: 1px dotted $table-border-color; border: 1px dotted $table-border-color;

View file

@ -184,3 +184,76 @@ body:not(.jsenabled) .langmenu:hover > .dropdown-menu,
.active.carousel-item-left { .active.carousel-item-left {
transform: translateX(-100%); transform: translateX(-100%);
} }
/**
* Reset all of the forced style on the page.
* - Remove borders on header and content.
* - Remove most of the vertical padding.
* - Make the content region flex grow so it pushes things like the
* next activity selector to the bottom of the page.
*/
$allow-reset-style: true !default;
@if $allow-reset-style {
body.reset-style {
#page-header {
.card {
border: none;
.page-header-headings {
h1 {
margin-bottom: 0;
}
}
}
& > div {
padding-top: 0 !important; /* stylelint-disable-line declaration-no-important */
padding-bottom: 0 !important; /* stylelint-disable-line declaration-no-important */
}
}
#page-content {
padding-bottom: 0 !important; /* stylelint-disable-line declaration-no-important */
#region-main-box {
#region-main {
border: none;
display: inline-flex;
flex-direction: column;
padding: 0;
height: 100%;
width: 100%;
padding-left: $card-spacer-x;
padding-right: $card-spacer-x;
vertical-align: top;
div[role="main"] {
flex: 1;
}
.activity-navigation {
overflow: hidden;
}
&.has-blocks {
width: calc(100% - #{$blocks-plus-gutter});
@include media-breakpoint-down(lg) {
width: 100%;
}
}
}
[data-region="blocks-column"] {
margin-left: auto;
}
@include media-breakpoint-down(lg) {
display: flex;
flex-direction: column;
}
}
}
}
}

File diff suppressed because one or more lines are too long

View file

@ -1 +1,4 @@
// General variables for all presets // General variables for all presets
// Disable the Boost theme reset styling and fixed width content.
$allow-reset-style: false;

File diff suppressed because one or more lines are too long