/**
* Get the comments for an entity
- * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+ * @param bool $orderByCreated
+ * @return MorphMany
*/
- public function comments()
+ public function comments($orderByCreated = true)
{
- return $this->morphMany(Comment::class, 'entity')->orderBy('created_at', 'asc');
+ $query = $this->morphMany(Comment::class, 'entity');
+ return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;
}
-
/**
* Get the related search terms.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
protected function getNextLocalId(Entity $entity)
{
- $comments = $entity->comments()->orderBy('local_id', 'desc')->first();
+ $comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
if ($comments === null) return 1;
return $comments->local_id + 1;
}
window.components = {};
let componentNames = Object.keys(componentMapping);
+initAll();
-for (let i = 0, len = componentNames.length; i < len; i++) {
- let name = componentNames[i];
- let elems = document.querySelectorAll(`[${name}]`);
- if (elems.length === 0) continue;
+/**
+ * Initialize components of the given name within the given element.
+ * @param {String} componentName
+ * @param {HTMLElement|Document} parentElement
+ */
+function initComponent(componentName, parentElement) {
+ let elems = parentElement.querySelectorAll(`[${componentName}]`);
+ if (elems.length === 0) return;
- let component = componentMapping[name];
- if (typeof window.components[name] === "undefined") window.components[name] = [];
+ let component = componentMapping[componentName];
+ if (typeof window.components[componentName] === "undefined") window.components[componentName] = [];
for (let j = 0, jLen = elems.length; j < jLen; j++) {
- let instance = new component(elems[j]);
- if (typeof elems[j].components === 'undefined') elems[j].components = {};
- elems[j].components[name] = instance;
- window.components[name].push(instance);
+ let instance = new component(elems[j]);
+ if (typeof elems[j].components === 'undefined') elems[j].components = {};
+ elems[j].components[componentName] = instance;
+ window.components[componentName].push(instance);
}
-}
\ No newline at end of file
+}
+
+/**
+ * Initialize all components found within the given element.
+ * @param parentElement
+ */
+function initAll(parentElement) {
+ if (typeof parentElement === 'undefined') parentElement = document;
+ for (let i = 0, len = componentNames.length; i < len; i++) {
+ initComponent(componentNames[i], parentElement);
+ }
+}
+
+window.components.init = initAll;
\ No newline at end of file
this.elem.addEventListener('submit', this.updateComment.bind(this));
this.editingComment = null;
+ this.parentId = null;
}
handleAction(event) {
let actionElem = event.target.closest('[action]');
+ if (event.target.matches('a[href^="#"]')) {
+ let id = event.target.href.split('#')[1];
+ console.log(document.querySelector('#' + id));
+ window.scrollAndHighlight(document.querySelector('#' + id));
+ }
if (actionElem === null) return;
+ event.preventDefault();
let action = actionElem.getAttribute('action');
if (action === 'edit') this.editComment(actionElem.closest('[comment]'));
if (action === 'delete') this.deleteComment(actionElem.closest('[comment]'));
if (action === 'addComment') this.showForm();
if (action === 'hideForm') this.hideForm();
- if (action === 'reply') this.setReply();
+ if (action === 'reply') this.setReply(actionElem.closest('[comment]'));
+ if (action === 'remove-reply-to') this.removeReplyTo();
}
closeUpdateForm() {
let reqData = {
text: text,
html: md.render(text),
- // parent_id: this.parent_id TODO - Handle replies
+ parent_id: this.parentId || null,
};
// TODO - Loading indicator
let commentId = this.editingComment.getAttribute('comment');
newComment.innerHTML = resp.data;
this.editingComment.innerHTML = newComment.children[0].innerHTML;
window.$events.emit('success', window.trans('entities.comment_updated_success'));
+ window.components.init(this.editingComment);
this.closeUpdateForm();
this.editingComment = null;
});
deleteComment(commentElem) {
let id = commentElem.getAttribute('comment');
// TODO - Loading indicator
- // TODO - Confirm dropdown
window.$http.delete(window.baseUrl(`/ajax/comment/${id}`)).then(resp => {
commentElem.parentNode.removeChild(commentElem);
window.$events.emit('success', window.trans('entities.comment_deleted_success'));
let reqData = {
text: text,
html: md.render(text),
- // parent_id: this.parent_id TODO - Handle replies
+ parent_id: this.parentId || null,
};
// TODO - Loading indicator
window.$http.post(window.baseUrl(`/ajax/page/${this.pageId}/comment`), reqData).then(resp => {
let newComment = document.createElement('div');
newComment.innerHTML = resp.data;
- this.container.appendChild(newComment.children[0]);
-
+ let newElem = newComment.children[0];
+ this.container.appendChild(newElem);
+ window.components.init(newElem);
window.$events.emit('success', window.trans('entities.comment_created_success'));
this.resetForm();
this.updateCount();
this.formInput.value = '';
this.formContainer.appendChild(this.form);
this.hideForm();
+ this.removeReplyTo();
}
showForm() {
this.formContainer.style.display = 'block';
this.formContainer.parentNode.style.display = 'block';
this.elem.querySelector('[comment-add-button]').style.display = 'none';
- this.formInput.focus(); // TODO - Scroll to input on focus
+ this.formInput.focus();
+ window.scrollToElement(this.formInput);
}
hideForm() {
this.elem.querySelector('[comment-add-button]').style.display = 'block';
}
- setReply() {
-
+ setReply(commentElem) {
this.showForm();
+ this.parentId = Number(commentElem.getAttribute('local-id'));
+ this.elem.querySelector('[comment-form-reply-to]').style.display = 'block';
+ let replyLink = this.elem.querySelector('[comment-form-reply-to] a');
+ replyLink.textContent = `#${this.parentId}`;
+ replyLink.href = `#comment${this.parentId}`;
+ }
+
+ removeReplyTo() {
+ this.parentId = null;
+ this.elem.querySelector('[comment-form-reply-to]').style.display = 'none';
}
}
//Global jQuery Config & Extensions
+/**
+ * Scroll the view to a specific element.
+ * @param {HTMLElement} element
+ */
+window.scrollToElement = function(element) {
+ if (!element) return;
+ let top = element.getBoundingClientRect().top + document.body.scrollTop;
+ $('html, body').animate({
+ scrollTop: top - 60 // Adjust to change final scroll position top margin
+ }, 300);
+};
+
+/**
+ * Scroll and highlight an element.
+ * @param {HTMLElement} element
+ */
+window.scrollAndHighlight = function(element) {
+ if (!element) return;
+ window.scrollToElement(element);
+ let color = document.getElementById('custom-styles').getAttribute('data-color-light');
+ let initColor = window.getComputedStyle(element).getPropertyValue('background-color');
+ element.style.backgroundColor = color;
+ setTimeout(() => {
+ element.classList.add('selectFade');
+ element.style.backgroundColor = initColor;
+ }, 10);
+ setTimeout(() => {
+ element.classList.remove('selectFade');
+ element.style.backgroundColor = '';
+ }, 3000);
+};
+
// Smooth scrolling
jQuery.fn.smoothScrollTo = function () {
if (this.length === 0) return;
- $('html, body').animate({
- scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin
- }, 300); // Adjust to change animations speed (ms)
+ window.scrollToElement(this[0]);
return this;
};
let idElem = document.getElementById(text);
$('.page-content [data-highlighted]').attr('data-highlighted', '').css('background-color', '');
if (idElem !== null) {
- let $idElem = $(idElem);
- let color = $('#custom-styles').attr('data-color-light');
- $idElem.css('background-color', color).attr('data-highlighted', 'true').smoothScrollTo();
- setTimeout(() => {
- $idElem.addClass('anim').addClass('selectFade').css('background-color', '');
- setTimeout(() => {
- $idElem.removeClass('selectFade');
- }, 3000);
- }, 100);
+ window.scrollAndHighlight(idElem);
} else {
$('.page-content').find(':contains("' + text + '")').smoothScrollTo();
}
unstickTree();
}
});
-
- // in order to call from other places.
- window.setupPageShow.goToText = goToText;
};
module.exports = setupPageShow;
\ No newline at end of file
// Parse exact matches
let exactMatches = t.match(exactCountRegex);
- console.log(exactMatches);
if (exactMatches !== null && Number(exactMatches[1]) === count) {
result = t.replace(exactCountRegex, '').trim();
break;
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
}
-.anim.selectFade {
+.selectFade {
transition: background-color ease-in-out 3000ms;
}
\ No newline at end of file
.content p {
margin-bottom: 1em;
}
+ .reply-row {
+ padding: $-xs $-s;
+ }
}
.comment-box .header {
padding: $-xs $-s;
background-color: #f8f8f8;
border-bottom: 1px solid #DDD;
- img, a, span {
- display: inline-block;
- vertical-align: top;
- }
- a, span {
- padding: $-xxs 0 $-xxs 0;
- line-height: 1.6;
- }
- a { color: #666; }
- span {
- color: #888;
- padding-left: $-xxs;
+ .meta {
+ img, a, span {
+ display: inline-block;
+ vertical-align: top;
+ }
+ a, span {
+ padding: $-xxs 0 $-xxs 0;
+ line-height: 1.6;
+ }
+ a { color: #666; }
+ span {
+ color: #888;
+ padding-left: $-xxs;
+ }
}
.text-muted {
color: #999;
&.small {
max-width: 840px;
}
+ &.nopad {
+ padding-left: 0;
+ padding-right: 0;
+ }
}
.row {
}
li.padded {
padding: $-xs $-m;
+ line-height: 1.2;
}
a {
display: block;
'comment_deleted_success' => 'Comment deleted',
'comment_created_success' => 'Comment added',
'comment_updated_success' => 'Comment updated',
- 'comment_delete_confirm' => 'This will remove the contents of the comment. Are you sure you want to delete this comment?',
+ 'comment_delete_confirm' => 'Are you sure you want to delete this comment?',
+ 'comment_in_reply_to' => 'In reply to :commentId',
'comment_create' => 'Created'
];
\ No newline at end of file
-<div class="comment-box" comment="{{ $comment->id }}" id="comment{{$comment->local_id}}">
+<div class="comment-box" comment="{{ $comment->id }}" local-id="{{$comment->local_id}}" parent-id="{{$comment->parent_id || ''}}" id="comment{{$comment->local_id}}">
<div class="header">
<div class="float right actions">
<button type="button" class="text-button" action="reply" title="{{ trans('common.reply') }}"><i class="zmdi zmdi-mail-reply-all"></i></button>
@endif
@if(userCan('comment-delete', $comment))
- <button type="button" class="text-button" action="delete" title="{{ trans('common.delete') }}"><i class="zmdi zmdi-delete"></i></button>
+
+ <div dropdown class="dropdown-container">
+ <button type="button" dropdown-toggle class="text-button" title="{{ trans('common.delete') }}"><i class="zmdi zmdi-delete"></i></button>
+ <ul>
+ <li class="padded"><small class="text-muted">{{trans('entities.comment_delete_confirm')}}</small></li>
+ <li><a action="delete" class="text-button neg" ><i class="zmdi zmdi-delete"></i>{{ trans('common.delete') }}</a></li>
+ </ul>
+ </div>
@endif
</div>
- <a href="#comment{{$comment->local_id}}" class="text-muted">#{{$comment->local_id}}</a>
-
- <img width="50" src="{{ $comment->createdBy->getAvatar(50) }}" class="avatar" alt="{{ $comment->createdBy->name }}">
-
- <a href="{{ $comment->createdBy->getProfileUrl() }}">{{ $comment->createdBy->name }}</a>
- {{--TODO - Account for deleted user--}}
- <span title="{{ $comment->created_at }}">
+ <div class="meta">
+ <a href="#comment{{$comment->local_id}}" class="text-muted">#{{$comment->local_id}}</a>
+
+ <img width="50" src="{{ $comment->createdBy->getAvatar(50) }}" class="avatar" alt="{{ $comment->createdBy->name }}">
+
+ <a href="{{ $comment->createdBy->getProfileUrl() }}">{{ $comment->createdBy->name }}</a>
+ {{--TODO - Account for deleted user--}}
+ <span title="{{ $comment->created_at }}">
{{ trans('entities.comment_created', ['createDiff' => $comment->created]) }}
</span>
- @if($comment->isUpdated())
- <span title="{{ $comment->updated_at }}">
+ @if($comment->isUpdated())
+ <span title="{{ $comment->updated_at }}">
•
- {{ trans('entities.comment_updated', ['updateDiff' => $comment->updated, 'username' => $comment->updatedBy->name]) }}
+ {{ trans('entities.comment_updated', ['updateDiff' => $comment->updated, 'username' => $comment->updatedBy->name]) }}
</span>
- @endif
+ @endif
+ </div>
+
</div>
+
+ @if ($comment->parent_id)
+ <div class="reply-row primary-background-light text-muted">
+ {!! trans('entities.comment_in_reply_to', ['commentId' => '<a href="#comment'.$comment->parent_id.'">#'.$comment->parent_id.'</a>']) !!}
+ </div>
+ @endif
+
<div comment-content class="content">
{!! $comment->html !!}
</div>
<div class="comment-box" comment-box style="display:none;">
<div class="header"><i class="zmdi zmdi-comment"></i> {{ trans('entities.comment_new') }}</div>
+ <div comment-form-reply-to class="reply-row primary-background-light text-muted" style="display: none;">
+ <button class="text-button float right" action="remove-reply-to">{{ trans('common.remove') }}</button>
+ {!! trans('entities.comment_in_reply_to', ['commentId' => '<a href=""></a>']) !!}
+ </div>
<div class="content" comment-form-container>
<form novalidate>
<div class="form-group">
</div>
- <div class="container small">
+ <div class="container small nopad">
@include('comments/comments', ['page' => $page])
</div>
@stop