diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5df1b9b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.sass-cache diff --git a/_config.php b/_config.php index 38c672a..3fbfa6d 100644 --- a/_config.php +++ b/_config.php @@ -2,4 +2,5 @@ Deprecation::notification_version('2.0', 'comments'); -define('COMMENTS_DIR', ltrim(Director::makeRelative(realpath(__DIR__)), DIRECTORY_SEPARATOR)); \ No newline at end of file +define('COMMENTS_DIR', basename(__DIR__)); +define('COMMENTS_THIRDPARTY', COMMENTS_DIR . DIRECTORY_SEPARATOR . 'thirdparty'); diff --git a/code/controllers/CommentingController.php b/code/controllers/CommentingController.php index 844bb19..789f6ba 100644 --- a/code/controllers/CommentingController.php +++ b/code/controllers/CommentingController.php @@ -13,10 +13,27 @@ class CommentingController extends Controller { 'approve', 'rss', 'CommentsForm', + 'reply', 'doPostComment', 'doPreviewComment' ); + private static $url_handlers = array( + 'reply/$ParentCommentID//$ID/$OtherID' => 'reply', + ); + + /** + * Fields required for this form + * + * @var array + * @config + */ + private static $required_fields = array( + 'Name', + 'Email', + 'Comment' + ); + /** * Base class this commenting form is for * @@ -289,6 +306,47 @@ class CommentingController extends Controller { return false; } + /** + * Create a reply form for a specified comment + * + * @param Comment $comment + */ + public function ReplyForm($comment) { + // Enables multiple forms with different names to use the same handler + $form = $this->CommentsForm(); + $form->setName('ReplyForm_'.$comment->ID); + $form->addExtraClass('reply-form'); + + // Load parent into reply form + $form->loadDataFrom(array( + 'ParentCommentID' => $comment->ID + )); + + // Customise action + $form->setFormAction($this->Link('reply', $comment->ID)); + + $this->extend('updateReplyForm', $form); + return $form; + } + + + /** + * Request handler for reply form. + * This method will disambiguate multiple reply forms in the same method + * + * @param SS_HTTPRequest $request + */ + public function reply(SS_HTTPRequest $request) { + // Extract parent comment from reply and build this way + if($parentID = $request->param('ParentCommentID')) { + $comment = DataObject::get_by_id('Comment', $parentID, true); + if($comment) { + return $this->ReplyForm($comment); + } + } + return $this->httpError(404); + } + /** * Post a comment form * @@ -297,26 +355,42 @@ class CommentingController extends Controller { public function CommentsForm() { $usePreview = $this->getOption('use_preview'); + $nameRequired = _t('CommentInterface.YOURNAME_MESSAGE_REQUIRED', 'Please enter your name'); + $emailRequired = _t('CommentInterface.EMAILADDRESS_MESSAGE_REQUIRED', 'Please enter your email address'); + $emailInvalid = _t('CommentInterface.EMAILADDRESS_MESSAGE_EMAIL', 'Please enter a valid email address'); + $urlInvalid = _t('CommentInterface.COMMENT_MESSAGE_URL', 'Please enter a valid URL'); + $commentRequired = _t('CommentInterface.COMMENT_MESSAGE_REQUIRED', 'Please enter your comment'); + $fields = new FieldList( $dataFields = new CompositeField( - TextField::create("Name", _t('CommentInterface.YOURNAME', 'Your name')) - ->setCustomValidationMessage(_t('CommentInterface.YOURNAME_MESSAGE_REQUIRED', 'Please enter your name')) - ->setAttribute('data-message-required', _t('CommentInterface.YOURNAME_MESSAGE_REQUIRED', 'Please enter your name')), + // Name + TextField::create("Name", _t('CommentInterface.YOURNAME', 'Your name')) + ->setCustomValidationMessage($nameRequired) + ->setAttribute('data-msg-required', $nameRequired), - EmailField::create("Email", _t('CommentingController.EMAILADDRESS', "Your email address (will not be published)")) - ->setCustomValidationMessage(_t('CommentInterface.EMAILADDRESS_MESSAGE_REQUIRED', 'Please enter your email address')) - ->setAttribute('data-message-required', _t('CommentInterface.EMAILADDRESS_MESSAGE_REQUIRED', 'Please enter your email address')) - ->setAttribute('data-message-email', _t('CommentInterface.EMAILADDRESS_MESSAGE_EMAIL', 'Please enter a valid email address')), + // Email + EmailField::create( + "Email", + _t('CommentingController.EMAILADDRESS', "Your email address (will not be published)") + ) + ->setCustomValidationMessage($emailRequired) + ->setAttribute('data-msg-required', $emailRequired) + ->setAttribute('data-msg-email', $emailInvalid) + ->setAttribute('data-rule-email', true), - TextField::create("URL", _t('CommentingController.WEBSITEURL', "Your website URL")) - ->setAttribute('data-message-url', _t('CommentInterface.COMMENT_MESSAGE_URL', 'Please enter a valid URL')), + // Url + TextField::create("URL", _t('CommentingController.WEBSITEURL', "Your website URL")) + ->setAttribute('data-msg-url', $urlInvalid) + ->setAttribute('data-rule-url', true), - TextareaField::create("Comment", _t('CommentingController.COMMENTS', "Comments")) - ->setCustomValidationMessage(_t('CommentInterface.COMMENT_MESSAGE_REQUIRED', 'Please enter your comment')) - ->setAttribute('data-message-required', _t('CommentInterface.COMMENT_MESSAGE_REQUIRED', 'Please enter your comment')) + // Comment + TextareaField::create("Comment", _t('CommentingController.COMMENTS', "Comments")) + ->setCustomValidationMessage($commentRequired) + ->setAttribute('data-msg-required', $commentRequired) ), HiddenField::create("ParentID"), HiddenField::create("ReturnURL"), + HiddenField::create("ParentCommentID"), HiddenField::create("BaseClass") ); @@ -345,11 +419,7 @@ class CommentingController extends Controller { } // required fields for server side - $required = new RequiredFields(array( - 'Name', - 'Email', - 'Comment' - )); + $required = new RequiredFields($this->config()->required_fields); // create the comment form $form = new Form($this, 'CommentsForm', $fields, $actions, $required); @@ -397,8 +467,8 @@ class CommentingController extends Controller { } } - if($member) { - $form->loadDataFrom($member); + if(!empty($member)) { + $form->loadDataFrom($member); } // hook to allow further extensions to alter the comments form diff --git a/code/extensions/CommentsExtension.php b/code/extensions/CommentsExtension.php index 21e240a..d39ed5c 100644 --- a/code/extensions/CommentsExtension.php +++ b/code/extensions/CommentsExtension.php @@ -19,6 +19,8 @@ class CommentsExtension extends DataExtension { * gravatar_default: Theme for 'not found' gravatar {@see http://gravatar.com/site/implement/images} * gravatar_rating: Gravatar rating (same as the standard default) * show_comments_when_disabled: Show older comments when commenting has been disabled. + * order_comments_by: Default sort order. + * order_replies_by: Sort order for replies. * comments_holder_id: ID for the comments holder * comment_permalink_prefix: ID prefix for each comment * require_moderation: Require moderation for all comments @@ -27,6 +29,8 @@ class CommentsExtension extends DataExtension { * frontend_spam: Display spam comments in the frontend, if the user can moderate them. * html_allowed: Allow for sanitized HTML in comments * use_preview: Preview formatted comment (when allowing HTML) + * nested_comments: Enable nested comments + * nested_depth: Max depth of nested comments in levels (where root is 1 depth) 0 means no limit. * * @var array * @@ -45,6 +49,7 @@ class CommentsExtension extends DataExtension { 'gravatar_rating' => 'g', 'show_comments_when_disabled' => false, 'order_comments_by' => '"Created" DESC', + 'order_replies_by' => false, 'comments_per_page' => 10, 'comments_holder_id' => 'comments-holder', 'comment_permalink_prefix' => 'comment-', @@ -56,6 +61,8 @@ class CommentsExtension extends DataExtension { 'html_allowed' => false, 'html_allowed_elements' => array('a', 'img', 'i', 'b'), 'use_preview' => false, + 'nested_comments' => false, + 'nested_depth' => 2, ); /** @@ -141,24 +148,6 @@ class CommentsExtension extends DataExtension { } } - /** - * Returns the RelationList of all comments against this object. Can be used as a data source - * for a gridfield with write access. - * - * @return CommentList - */ - public function AllComments() { - $comments = CommentList::create($this->ownerBaseClass)->forForeignID($this->owner->ID); - $this->owner->extend('updateAllComments', $comments); - return $comments; - } - - public function getComments() { - // TODO: find out why this is being triggered when combined with blog - // Deprecation::notice('2.0', 'Use PagedComments to get paged comments'); - return $this->PagedComments(); - } - /** * Get comment moderation rules for this parent * @@ -194,16 +183,27 @@ class CommentsExtension extends DataExtension { } /** - * Returns the root level comments, with spam and unmoderated items excluded, for use in the frontend + * Returns the RelationList of all comments against this object. Can be used as a data source + * for a gridfield with write access. * * @return CommentList */ - public function Comments() { - // Get all non-spam comments + public function AllComments() { $order = $this->owner->getCommentsOption('order_comments_by'); - $list = $this - ->AllComments() + $comments = CommentList::create($this->ownerBaseClass) + ->forForeignID($this->owner->ID) ->sort($order); + $this->owner->extend('updateAllComments', $comments); + return $comments; + } + + /** + * Returns all comments against this object, with with spam and unmoderated items excluded, for use in the frontend + * + * @return CommentList + */ + public function AllVisibleComments() { + $list = $this->AllComments(); // Filter spam comments for non-administrators if configured $showSpam = $this->owner->getCommentsOption('frontend_spam') && $this->owner->canModerateComments(); @@ -218,6 +218,23 @@ class CommentsExtension extends DataExtension { $list = $list->filter('Moderated', 1); } + $this->owner->extend('updateAllVisibleComments', $list); + return $list; + } + + /** + * Returns the root level comments, with spam and unmoderated items excluded, for use in the frontend + * + * @return CommentList + */ + public function Comments() { + $list = $this->AllVisibleComments(); + + // If nesting comments, only show root level + if($this->owner->getCommentsOption('nested_comments')) { + $list = $list->filter('ParentCommentID', 0); + } + $this->owner->extend('updateComments', $list); return $list; } @@ -384,8 +401,9 @@ class CommentsExtension extends DataExtension { $enabled = $this->getCommentsEnabled(); if($enabled && $this->owner->getCommentsOption('include_js')) { Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js'); + Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery-validate/lib/jquery.form.js'); - Requirements::javascript(THIRDPARTY_DIR . '/jquery-validate/jquery.validate.pack.js'); + Requirements::javascript(COMMENTS_THIRDPARTY . '/jquery-validate/jquery.validate.min.js'); Requirements::javascript('comments/javascript/CommentsInterface.js'); } diff --git a/code/dataobjects/Comment.php b/code/model/Comment.php similarity index 75% rename from code/dataobjects/Comment.php rename to code/model/Comment.php index 2ec2084..ce89529 100755 --- a/code/dataobjects/Comment.php +++ b/code/model/Comment.php @@ -13,13 +13,15 @@ * @property integer $ParentID ID of the parent page / dataobject * @property boolean $AllowHtml If true, treat $Comment as HTML instead of plain text * @property string $SecretToken Secret admin token required to provide moderation links between sessions + * @property integer $Depth Depth of this comment in the nested chain * * @method HasManyList ChildComments() List of child comments * @method Member Author() Member object who created this comment - * + * @method Comment ParentComment() Parent comment this is a reply to * @package comments */ class Comment extends DataObject { + /** * @var array */ @@ -34,10 +36,16 @@ class Comment extends DataObject { 'ParentID' => 'Int', 'AllowHtml' => 'Boolean', 'SecretToken' => 'Varchar(255)', + 'Depth' => 'Int', ); private static $has_one = array( - 'Author' => 'Member', + "Author" => "Member", + "ParentComment" => "Comment", + ); + + private static $has_many = array( + "ChildComments" => "Comment" ); private static $default_sort = '"Created" DESC'; @@ -71,12 +79,16 @@ class Comment extends DataObject { private static $summary_fields = array( 'Name' => 'Submitted By', 'Email' => 'Email', - 'Comment' => 'Comment', + 'Comment.LimitWordCount' => 'Comment', 'Created' => 'Date Posted', 'ParentTitle' => 'Post', 'IsSpam' => 'Is Spam', ); + private static $field_labels = array( + 'Author' => 'Author Member', + ); + public function onBeforeWrite() { parent::onBeforeWrite(); @@ -84,6 +96,18 @@ class Comment extends DataObject { if($this->AllowHtml) { $this->Comment = $this->purifyHtml($this->Comment); } + + // Check comment depth + $this->updateDepth(); + } + + public function onBeforeDelete() { + parent::onBeforeDelete(); + + // Delete all children + foreach($this->ChildComments() as $comment) { + $comment->delete(); + } } /** @@ -224,7 +248,7 @@ class Comment extends DataObject { public function castingHelper($field) { // Safely escape the comment if($field === 'EscapedComment') { - return $this->AllowHtml ? 'HTMLText' : 'Varchar'; + return $this->AllowHtml ? 'HTMLText' : 'Text'; } return parent::castingHelper($field); } @@ -474,6 +498,7 @@ class Comment extends DataObject { $this->write(); $this->extend('afterMarkUnapproved'); } + /** * @return string */ @@ -506,14 +531,46 @@ class Comment extends DataObject { * Modify the default fields shown to the user */ public function getCMSFields() { - $fields = parent::getCMSFields(); + $commentField = $this->AllowHtml ? 'HtmlEditorField' : 'TextareaField'; + $fields = new FieldList( + $this + ->obj('Created') + ->scaffoldFormField($this->fieldLabel('Created')) + ->performReadonlyTransformation(), + TextField::create('Name', $this->fieldLabel('AuthorName')), + $commentField::create('Comment', $this->fieldLabel('Comment')), + EmailField::create('Email', $this->fieldLabel('Email')), + TextField::create('URL', $this->fieldLabel('URL')), + FieldGroup::create(array( + CheckboxField::create('Moderated', $this->fieldLabel('Moderated')), + CheckboxField::create('IsSpam', $this->fieldLabel('IsSpam')), + )) + ->setTitle('Options') + ->setDescription(_t( + 'Comment.OPTION_DESCRIPTION', + 'Unmoderated and spam comments will not be displayed until approved' + )) + ); - $hidden = array('ParentID', 'AuthorID', 'BaseClass', 'AllowHtml', 'SecretToken'); - - foreach($hidden as $private) { - $fields->removeByName($private); + // Show member name if given + if(($author = $this->Author()) && $author->exists()) { + $fields->insertAfter( + TextField::create('AuthorMember', $this->fieldLabel('Author'), $author->Title) + ->performReadonlyTransformation(), + 'Name' + ); } + // Show parent comment details + if(($parent = $this->ParentComment()) && $parent->exists()) { + $fields->insertAfter( + TextField::create('ParentCommentDescription', $this->fieldLabel('ParentComment'), $parent->Title) + ->performReadonlyTransformation(), + 'Created' + ); + } + + $this->extend('updateCMSFields', $fields); return $fields; } @@ -558,8 +615,135 @@ class Comment extends DataObject { return $gravatar; } + + /** + * Determine if replies are enabled for this instance + * + * @return boolean + */ + public function getRepliesEnabled() { + // Check reply option + if(!$this->getOption('nested_comments')) { + return false; + } + + // Check if depth is limited + $maxLevel = $this->getOption('nested_depth'); + return !$maxLevel || $this->Depth < $maxLevel; + } + + /** + * Returns the list of all replies + * + * @return SS_List + */ + public function AllReplies() { + // No replies if disabled + if(!$this->getRepliesEnabled()) { + return new ArrayList(); + } + + // Get all non-spam comments + $order = $this->getOption('order_replies_by') + ?: $this->getOption('order_comments_by'); + $list = $this + ->ChildComments() + ->sort($order); + + $this->extend('updateAllReplies', $list); + return $list; + } + + /** + * Returns the list of replies, with spam and unmoderated items excluded, for use in the frontend + * + * @return SS_List + */ + public function Replies() { + // No replies if disabled + if(!$this->getRepliesEnabled()) { + return new ArrayList(); + } + $list = $this->AllReplies(); + + // Filter spam comments for non-administrators if configured + $parent = $this->getParent(); + $showSpam = $this->getOption('frontend_spam') && $parent && $parent->canModerateComments(); + if(!$showSpam) { + $list = $list->filter('IsSpam', 0); + } + + // Filter un-moderated comments for non-administrators if moderation is enabled + $showUnmoderated = $parent && ( + ($parent->ModerationRequired === 'None') + || ($this->getOption('frontend_moderation') && $parent->canModerateComments()) + ); + if (!$showUnmoderated) { + $list = $list->filter('Moderated', 1); + } + + $this->extend('updateReplies', $list); + return $list; + } + + /** + * Returns the list of replies paged, with spam and unmoderated items excluded, for use in the frontend + * + * @return PaginatedList + */ + public function PagedReplies() { + $list = $this->Replies(); + + // Add pagination + $list = new PaginatedList($list, Controller::curr()->getRequest()); + $list->setPaginationGetVar('repliesstart'.$this->ID); + $list->setPageLength($this->getOption('comments_per_page')); + + $this->extend('updatePagedReplies', $list); + return $list; + } + + /** + * Generate a reply form for this comment + * + * @return Form + */ + public function ReplyForm() { + // Ensure replies are enabled + if(!$this->getRepliesEnabled()) { + return null; + } + + // Check parent is available + $parent = $this->getParent(); + if(!$parent || !$parent->exists()) { + return null; + } + + // Build reply controller + $controller = CommentingController::create(); + $controller->setOwnerRecord($parent); + $controller->setBaseClass($parent->ClassName); + $controller->setOwnerController(Controller::curr()); + + return $controller->ReplyForm($this); + } + + /** + * Refresh of this comment in the hierarchy + */ + public function updateDepth() { + $parent = $this->ParentComment(); + if($parent && $parent->exists()) { + $parent->updateDepth(); + $this->Depth = $parent->Depth + 1; + } else { + $this->Depth = 1; + } + } } + /** * Provides the ability to generate cryptographically secure tokens for comment moderation */ diff --git a/code/CommentList.php b/code/model/CommentList.php similarity index 100% rename from code/CommentList.php rename to code/model/CommentList.php diff --git a/config.rb b/config.rb new file mode 100644 index 0000000..a5dcfd2 --- /dev/null +++ b/config.rb @@ -0,0 +1,23 @@ +# Require any additional compass plugins here. + +# Set this to the root of your project when deployed: +http_path = "/" +css_dir = "css" +sass_dir = "scss" +javascripts_dir = "javascript" + +# You can select your preferred output style here (can be overridden via the command line): +# output_style = :expanded or :nested or :compact or :compressed + +# To enable relative paths to assets via compass helper functions. Uncomment: +relative_assets = true + +# To disable debugging comments that display the original location of your selectors. Uncomment: +line_comments = true + + +# If you prefer the indented syntax, you might want to regenerate this +# project again passing --syntax sass, or you can uncomment this: +# preferred_syntax = :sass +# and then run: +# sass-convert -R --from scss --to sass scss scss && rm -rf sass && mv scss sass diff --git a/css/comments.css b/css/comments.css new file mode 100644 index 0000000..fe51670 --- /dev/null +++ b/css/comments.css @@ -0,0 +1,173 @@ +/* line 12, ../scss/comments.scss */ +#comments-holder { + clear: both; +} +/* line 15, ../scss/comments.scss */ +#comments-holder h2 { + margin: 30px 0 10px; + padding-bottom: 5px; + border-bottom: 1px solid #eee; +} +/* line 21, ../scss/comments.scss */ +#comments-holder h3 { + margin-top: 0; +} +/* line 25, ../scss/comments.scss */ +#comments-holder .field { + clear: left; +} +/* line 29, ../scss/comments.scss */ +#comments-holder .num, +#comments-holder .author { + font-size: 1.3em; +} +/* line 34, ../scss/comments.scss */ +#comments-holder .num { + color: #999; + margin-right: 5px; +} +/* line 39, ../scss/comments.scss */ +#comments-holder .num-total { + line-height: 40px; + margin-bottom: 0; +} +/* line 44, ../scss/comments.scss */ +#comments-holder .comments-list { + margin: 0; +} +/* line 49, ../scss/comments.scss */ +#comments-holder .comment { + clear: both; + list-style-type: none; + overflow: auto; + padding: 20px 0 10px; + position: relative; +} +/* line 57, ../scss/comments.scss */ +#comments-holder .comment.author-comment:after { + content: 'Author'; + float: right; + position: absolute; + top: 1.5em; + right: 0; + font-size: 1em; + font-weight: bold; + color: #00acee; +} +/* line 68, ../scss/comments.scss */ +#comments-holder .comment.author-comment .comment-text { + border: 1px solid #00acee; +} +/* line 75, ../scss/comments.scss */ +#comments-holder .comment.spam .comment-text { + border: 1px dashed #f48b33; +} +/* line 80, ../scss/comments.scss */ +#comments-holder .comment .comment-text { + background-color: #fff; + border: 1px solid #ddd; + box-shadow: none; + margin: 0; + padding: 0 15% 0 20px; + white-space: pre; + white-space: pre-wrap; + white-space: pre-line; + word-wrap: break-word; +} +/* line 91, ../scss/comments.scss */ +#comments-holder .comment .comment-text p:last-child { + margin-bottom: 0; +} +/* line 96, ../scss/comments.scss */ +#comments-holder .comment .date { + font-size: 16px; +} +/* line 99, ../scss/comments.scss */ +#comments-holder .comment .date:before { + content: '\0000a0\0000a0\0000a0\0000a0'; +} +/* line 104, ../scss/comments.scss */ +#comments-holder .comment.unmoderated { + border: 2px dashed gray; + margin: 2em 0 4em; + padding: 2em; +} +/* line 109, ../scss/comments.scss */ +#comments-holder .comment.unmoderated .comment-moderation { + margin-bottom: 0; +} +/* line 114, ../scss/comments.scss */ +#comments-holder .comment .info { + margin-bottom: 10px; +} +/* line 118, ../scss/comments.scss */ +#comments-holder .comment.spam .comment { + border: 1px dashed #f48b33; + color: #f48b33; + border-radius: 4px; + padding: 2.5em 1em 1em; +} +/* line 125, ../scss/comments.scss */ +#comments-holder .comment .comment-replies-container { + margin-left: 20px; + padding-left: 10px; + border-left: 1px dashed #999; +} +/* line 130, ../scss/comments.scss */ +#comments-holder .comment .comment-replies-container .comment-reply-form-holder { + padding: 0 10px; +} +/* line 133, ../scss/comments.scss */ +#comments-holder .comment .comment-replies-container .comment-replies-holder { + padding: 0 0 0 10px; +} +/* line 140, ../scss/comments.scss */ +#comments-holder .comment-moderation { + float: right; + margin: 1em 0 2em; +} +/* line 144, ../scss/comments.scss */ +#comments-holder .comment-moderation .heading { + margin-bottom: 0; +} +/* line 149, ../scss/comments.scss */ +#comments-holder .action-links { + margin: 20px 0 10px; +} +/* line 152, ../scss/comments.scss */ +#comments-holder .action-links li { + display: inline; + list-style-type: none; + margin-left: 20px; + overflow: auto; +} +/* line 158, ../scss/comments.scss */ +#comments-holder .action-links li:first-child { + margin-left: 0; +} +/* line 162, ../scss/comments.scss */ +#comments-holder .action-links li.comment-reply-action { + float: right; +} +/* line 168, ../scss/comments.scss */ +#comments-holder .comment-count { + margin: 15px 0; +} +/* line 173, ../scss/comments.scss */ +#comments-holder .commenting-area { + margin-top: 50px; +} +/* line 176, ../scss/comments.scss */ +#comments-holder .commenting-area label.left { + font-weight: normal; +} +/* line 181, ../scss/comments.scss */ +#comments-holder .commenting-rss-feed { + margin-top: 4em; + text-align: right; +} +/* line 186, ../scss/comments.scss */ +#comments-holder .no-comments-yet { + display: inline-block; + margin-top: 10px; +} diff --git a/javascript/CommentsInterface.js b/javascript/CommentsInterface.js index e29eb0a..d934db3 100755 --- a/javascript/CommentsInterface.js +++ b/javascript/CommentsInterface.js @@ -2,115 +2,142 @@ * @package comments */ (function($) { - $(document).ready(function () { - - var container = $('.comments-holder-container'), - commentsHolder = $('.comments-holder'), - commentsList = $('.comments-list', commentsHolder), - pagination = $('.comments-pagination'), - noCommentsYet = $('.no-comments-yet', commentsHolder), - form = $('form', container), - previewEl = form.find('#PreviewComment'); + $.entwine( "ss.comments", function($) { /** - * Init + * Enable form validation */ - previewEl.hide(); - $(':submit[name=action_doPreviewComment]').show(); + $('.comments-holder-container form').entwine({ + onmatch: function() { + + // @todo Reinstate preview-comment functionality - /** - * Validate - */ - form.validate({ - invalidHandler : function(form, validator){ - $('html, body').animate({ - scrollTop: $(validator.errorList[0].element).offset().top - 30 - }, 200); + /** + * Validate + */ + $(this).validate({ + + /** + * Ignore hidden elements in this form + */ + ignore: ':hidden', + + /** + * Use default 'required' for error labels + */ + errorClass: "required", + + /** + * Use span instead of labels + */ + errorElement: "span", + + /** + * On error, scroll to the invalid element + */ + invalidHandler : function(form, validator){ + $('html, body').animate({ + scrollTop: $(validator.errorList[0].element).offset().top - 30 + }, 200); + }, + + /** + * Ensure any new error message has the correct class and placement + */ + errorPlacement: function(error, element) { + error + .addClass('message') + .insertAfter(element); + } + }); + this._super(); }, - showErrors: function(errorMap, errorList) { - this.defaultShowErrors(); - // hack to add the extra classes we need to the validation message elements - form.find('span.error').addClass('message required'); - }, - - errorElement: "span", - errorClass: "error", - ignore: '.hidden', - rules: { - Name : { - required : true - }, - Email : { - required : true, - email : true - }, - Comment: { - required : true - }, - URL: { - url : true + onunmatch: function() { + this._super(); + } + }); + + /** + * Comment reply form + */ + $( ".comment-replies-container .comment-reply-form-holder" ).entwine({ + onmatch: function() { + // If and only if this is not the currently selected form, hide it on page load + var selectedHash = window.document.location.hash.substr(1), + form = $(this).children('.reply-form'); + if( !selectedHash || selectedHash !== form.prop( 'id' ) ) { + this.hide(); } + this._super(); }, - messages: { - Name : { - required : form.find('[name="Name"]').data('message-required') - }, - Email : { - required : form.find('[name="Email"]').data('message-required'), - email : form.find('[name="Email"]').data('message-email') - }, - Comment: { - required : form.find('[name="Comment"]').data('message-required') - }, - URL: { - url : form.find('[name="Comment"]').data('message-url') + onunmatch: function() { + this._super(); + } + }); + + /** + * Toggle on/off reply form + */ + $( ".comment-reply-link" ).entwine({ + onclick: function( e ) { + var allForms = $( ".comment-reply-form-holder" ), + formID = $( this ).prop('href').replace(/^[^#]*#/, '#'), + form = $(formID).closest('.comment-reply-form-holder'); + + // Prevent focus + e.preventDefault(); + if(form.is(':visible')) { + allForms.slideUp(); + } else { + allForms.not(form).slideUp(); + form.slideDown(); } } }); - - form.submit(function (e) { - // trigger validation - if(!form.validate().valid()) return false; - }); + /** * Preview comment by fetching it from the server via ajax. */ + /* @todo Migrate to work with nested comments $(':submit[name=action_doPreviewComment]', form).click(function(e) { - e.preventDefault(); + e.preventDefault(); - if(!form.validate().valid()) { - return false; - } + if(!form.validate().valid()) { + return false; + } - previewEl.show().addClass('loading').find('.middleColumn').html(' '); + previewEl.show().addClass('loading').find('.middleColumn').html(' '); - form.ajaxSubmit({ - success: function(response) { - var responseEl = $(response); - if(responseEl.is('form')) { - // Validation failed, renders form instead of single comment - form.find(".data-fields").replaceWith(responseEl.find(".data-fields")); - } else { - // Default behaviour - previewEl.removeClass('loading').find('.middleColumn').html(responseEl); - } - }, - data: {'action_doPreviewComment': 1} - }); + form.ajaxSubmit({ + success: function(response) { + var responseEl = $(response); + if(responseEl.is('form')) { + // Validation failed, renders form instead of single comment + form.find(".data-fields").replaceWith(responseEl.find(".data-fields")); + } else { + // Default behaviour + previewEl.removeClass('loading').find('.middleColumn').html(responseEl); + } + }, + data: {'action_doPreviewComment': 1} + }); }); + */ /** * Hide outdated preview on form changes */ + /* $(':input', form).on('change keydown', function() { - previewEl.removeClass('loading').hide(); - }); + previewEl.removeClass('loading').hide(); + });*/ /** * Clicking one of the metalinks performs the operation via ajax * this inclues the spam and approve links */ + /* @todo Migrate to work with nested comments commentsList.on('click', '.action-links a', function(e) { var link = $(this); var comment = link.parents('.comment:first'); @@ -149,11 +176,12 @@ e.preventDefault(); }); - + */ /** * Ajax pagination */ + /* @todo Migrate to work with nested comments pagination.find('a').on('click', function(){ commentsList.addClass('loading'); $.ajax({ @@ -165,14 +193,16 @@ pagination.hide().html(html.find('.comments-pagination:first').html()).fadeIn(); commentsList.removeClass('loading'); $('html, body').animate({ - scrollTop: commentsList.offset().top - 30 - }, 200); + scrollTop: commentsList.offset().top - 30 + }, 200); }, failure: function(html) { alert('Error loading comments'); } }); return false; - }); + });*/ }); })(jQuery); + + diff --git a/scss/comments.scss b/scss/comments.scss new file mode 100644 index 0000000..2d9eedd --- /dev/null +++ b/scss/comments.scss @@ -0,0 +1,190 @@ +// Colours +$blue: #00acee !default; +$blueDark: #0064cd !default; +$green: #46a546 !default; +$redLight: #DB4A39 !default; +$red: #9d261d !default; +$yellow: #ffc40d !default; +$orange: #f48b33 !default; +$pink: #c3325f !default; +$purple: #7a43b6 !default; + +#comments-holder { + clear: both; + + h2 { + margin: 30px 0 10px; + padding-bottom: 5px; + border-bottom: 1px solid #eee; + } + + h3 { + margin-top: 0; + } + + .field { + clear: left; + } + + .num, + .author { + font-size: 1.3em; + } + + .num { + color: #999; + margin-right: 5px; + } + + .num-total { + line-height: 40px; + margin-bottom: 0; + } + + .comments-list { + margin: 0; + } + + // A published comment + .comment { + clear:both; + list-style-type: none; + overflow: auto; + padding: 20px 0 10px; + position: relative; + + &.author-comment { + &:after { + content: 'Author'; + float: right; + position: absolute; + top: 1.5em; + right: 0; + font-size: 1em; + font-weight: bold; + color: $blue; + } + + .comment-text { + border: 1px solid $blue; + } + + } + + &.spam { + .comment-text { + border: 1px dashed $orange; + } + } + + .comment-text { + background-color: #fff; + border: 1px solid #ddd; + box-shadow: none; + margin: 0; + padding: 0 15% 0 20px; + white-space: pre; + white-space: pre-wrap; + white-space: pre-line; + word-wrap: break-word; + + p:last-child { + margin-bottom: 0; + } + } + + .date { + font-size: 16px; + + &:before { + content: '\0000a0\0000a0\0000a0\0000a0'; + } + } + + &.unmoderated { + border: 2px dashed gray; + margin: 2em 0 4em; + padding: 2em; + + .comment-moderation { + margin-bottom: 0; // Remove the margin to compensate for unmoderated comment padding. + } + } + + .info { + margin-bottom: 10px; + } + + &.spam .comment{ + border: 1px dashed $orange; + color: $orange; + border-radius: 4px; + padding: 2.5em 1em 1em; + } + + .comment-replies-container { + margin-left: 20px; + padding-left: 10px; + border-left: 1px dashed #999; + + .comment-reply-form-holder { + padding: 0 10px; // Prevent clipping issues on slideUp/Down + } + .comment-replies-holder { + padding: 0 0 0 10px; + } + } + } + + // Admin actions + .comment-moderation { + float: right; + margin: 1em 0 2em; + + .heading { + margin-bottom: 0; + } + } + + .action-links { + margin: 20px 0 10px; + + li { + display: inline; + list-style-type: none; + margin-left: 20px; + overflow: auto; + + &:first-child { + margin-left: 0; + } + + &.comment-reply-action { + float: right; + } + } + } + + .comment-count { + margin: 15px 0; + } + + // The comment form + .commenting-area { + margin-top: 50px; + + label.left{ + font-weight: normal; + } + } + + .commenting-rss-feed { + margin-top: 4em; + text-align: right; + } + + .no-comments-yet { + display: inline-block; + margin-top: 10px; + } +} diff --git a/templates/CommentReplies.ss b/templates/CommentReplies.ss new file mode 100644 index 0000000..0b8d9a4 --- /dev/null +++ b/templates/CommentReplies.ss @@ -0,0 +1,23 @@ +<% if $RepliesEnabled %> +
+ +
+ $ReplyForm +
+ +
+ <% if $Replies %> + + <% with $Replies %> + <% include ReplyPagination %> + <% end_with %> + <% end_if %> +
+
+<% end_if %> diff --git a/templates/CommentsInterface.ss b/templates/CommentsInterface.ss index 4577746..0cd141c 100755 --- a/templates/CommentsInterface.ss +++ b/templates/CommentsInterface.ss @@ -1,3 +1,5 @@ +<% require themedCSS('comments', 'comments') %> + <% if $CommentsEnabled %>

<% _t('CommentsInterface_ss.POSTCOM','Post your comment') %>

@@ -21,7 +23,7 @@
<% if $PagedComments %> -