diff --git a/assets/typescript/Comments.ts b/assets/typescript/Comments.ts
index 6fd16a06..c2835821 100644
--- a/assets/typescript/Comments.ts
+++ b/assets/typescript/Comments.ts
@@ -1,3 +1,6 @@
+///
+
+
let globalCommentLock = false;
function commentsLocked(): boolean
@@ -18,13 +21,31 @@ function commentsFreeLock(): void
globalCommentLock = false;
}
-interface CommentDeletionInfo {
- comment_id: number;
+interface CommentNotice {
error: string;
message: string;
}
-function commentDelete(ev: Event)
+interface CommentDeletionInfo extends CommentNotice {
+ comment_id: number;
+}
+
+interface CommentPostInfo extends CommentNotice {
+ comment_id: number;
+ category_id: number;
+ comment_text: string;
+ comment_html: string;
+ comment_created: Date;
+ comment_edited: Date | null;
+ comment_deleted: Date | null;
+ comment_reply_to: number;
+ comment_pinned: Date | null;
+ user_id: number;
+ username: string;
+ user_colour: number;
+}
+
+function commentDelete(ev: Event): void
{
if (!checkUserPerm('comments', CommentPermission.Delete) || !commentsRequestLock())
return;
@@ -37,13 +58,13 @@ function commentDelete(ev: Event)
return;
commentsFreeLock();
- var json: CommentDeletionInfo = JSON.parse(xhr.responseText) as CommentDeletionInfo,
+ let json: CommentDeletionInfo = JSON.parse(xhr.responseText) as CommentDeletionInfo,
message = json.error || json.message;
if (message)
alert(message);
else {
- var elem = document.getElementById('comment-' + json.comment_id);
+ let elem = document.getElementById('comment-' + json.comment_id);
if (elem)
elem.parentNode.removeChild(elem);
@@ -54,12 +75,265 @@ function commentDelete(ev: Event)
xhr.send();
}
-function commentsInit() {
+function commentPostEventHandler(ev: Event): void
+{
+ const form: HTMLFormElement = ev.target as HTMLFormElement,
+ formData: FormData = ExtractFormData(form, true);
+
+ commentPost(
+ formData,
+ info => commentPostSuccess(form, info),
+ message => commentPostFail
+ );
+}
+
+function commentPost(formData: FormData, onSuccess: (comment: CommentPostInfo) => void = null, onFail: (message: string) => void = null): void
+{
+ if (!commentsRequestLock())
+ return;
+
+ const xhr = new XMLHttpRequest();
+
+ xhr.addEventListener('readystatechange', () => {
+ if (xhr.readyState !== 4)
+ return;
+
+ commentsFreeLock();
+
+ const json: CommentPostInfo = JSON.parse(xhr.responseText) as CommentPostInfo,
+ message: string = json.error || json.message;
+
+ if (message && onFail)
+ onFail(message);
+ else if (!message && onSuccess)
+ onSuccess(json);
+ });
+
+ xhr.open('POST', '/comments.php?m=create');
+ xhr.setRequestHeader('X-Misuzu-XHR', 'comments');
+ xhr.send(formData);
+}
+
+function commentPostSuccess(form: HTMLFormElement, comment: CommentPostInfo): void {
+ if (form.classList.contains('comment--reply'))
+ (form.parentNode.parentNode.querySelector('label.comment__action') as HTMLLabelElement).click();
+
+ //commentInsert(info, form);
+}
+
+function commentPostFail(message: string): void {
+ alert(message);
+}
+
+function commentsInit(): void {
const commentDeletes: HTMLCollectionOf = document.getElementsByClassName('comment__action--delete') as HTMLCollectionOf;
- for (var i = 0; i < commentDeletes.length; i++) {
- commentDeletes[i].onclick = commentDelete;
+ for (let i = 0; i < commentDeletes.length; i++) {
+ commentDeletes[i].addEventListener('click', commentDelete);
commentDeletes[i].dataset.href = commentDeletes[i].href;
commentDeletes[i].href = 'javascript:void(0);';
}
+
+ const commentInputs: HTMLCollectionOf = document.getElementsByClassName('comment__text--input') as HTMLCollectionOf;
+
+ for (let i = 0; i < commentInputs.length; i++) {
+ commentInputs[i].addEventListener('keydown', ev => {
+ if (ev.keyCode === 13 && ev.ctrlKey && !ev.altKey && !ev.shiftKey) {
+ let form = commentInputs[i].form;
+ commentPost(
+ ExtractFormData(form, true),
+ info => commentPostSuccess(form, info),
+ message => commentPostFail
+ );
+ }
+ });
+ }
+}
+
+function commentConstruct(comment: CommentPostInfo, layer: number = 0): HTMLElement {
+ const isReply = comment.comment_reply_to > 0;
+
+ const commentElement: HTMLDivElement = document.createElement('div');
+ commentElement.className = 'comment';
+ commentElement.id = 'comment-' + comment.comment_id;
+
+ // layer 2
+ const commentContainer: HTMLDivElement = commentElement.appendChild(document.createElement('div'));
+ commentContainer.className = 'comment__container';
+
+ const commentReplies: HTMLDivElement = commentElement.appendChild(document.createElement('div'));
+ commentReplies.className = 'comment__replies comment__replies--indent-' + layer;
+ commentReplies.id = commentElement.id + '-replies';
+
+ // container
+ const commentAvatar: HTMLAnchorElement = commentContainer.appendChild(document.createElement('a'));
+ commentAvatar.className = 'avatar comment__avatar';
+ commentAvatar.href = '/profile.php?u=' + comment.user_id;
+ commentAvatar.style.backgroundImage = `url('/profile.php?m=avatar&u=${comment.user_id}')`;
+
+ const commentContent: HTMLDivElement = commentContainer.appendChild(document.createElement('div'));
+ commentContent.className = 'comment__content';
+
+ // content
+ const commentInfo = commentContent.appendChild(document.createElement('div'));
+ commentInfo.className = 'comment__info';
+
+ const commentText = commentContent.appendChild(document.createElement('div'));
+ commentText.className = 'comment__text';
+
+ if (comment.comment_html)
+ commentText.innerHTML = comment.comment_html;
+ else
+ commentText.textContent = comment.comment_text;
+
+ const commentActions = commentContent.appendChild(document.createElement('div'));
+ commentActions.className = 'comment__actions';
+
+}
+
+function commentInsert(comment, form): void
+{
+ var isReply = form.classList.contains('comment--reply'),
+ parent = isReply
+ ? form.parentNode
+ : form.parentNode.parentNode.getElementsByClassName('comments__listing')[0],
+ repliesIndent = isReply
+ ? (parseInt(parent.classList[1].substr(25)) + 1)
+ : 1;
+
+ // info
+ var commentUser = document.createElement('a');
+ commentUser.className = 'comment__user comment__user--link';
+ commentUser.textContent = comment.username;
+ commentUser.href = '/profile?u=' + comment.user_id;
+ commentUser.style.color = comment.user_colour == null || (comment.user_colour & 0x40000000) > 0
+ ? 'inherit'
+ : '#' + (comment.user_colour & 0xFFFFFF).toString(16);
+ commentInfo.appendChild(commentUser);
+
+ var commentLink = document.createElement('a');
+ commentLink.className = 'comment__link';
+ commentLink.href = '#' + commentElement.id;
+ commentInfo.appendChild(commentLink);
+
+ var commentTime = document.createElement('time'),
+ commentDate = new Date(comment.comment_created + 'Z');
+ commentTime.className = 'comment__date';
+ commentTime.title = commentDate.toLocaleString();
+ commentTime.dateTime = commentDate.toISOString();
+ commentTime.textContent = timeago().format(commentDate);
+ commentLink.appendChild(commentTime);
+
+ // actions
+ if (typeof commentVote === 'function') {
+ var commentLike = document.createElement('a');
+ commentLike.className = 'comment__action comment__action--link comment__action--like';
+ commentLike.href = 'javascript:void(0);';
+ commentLike.textContent = 'Like';
+ commentLike.onclick = commentVote;
+ commentActions.appendChild(commentLike);
+
+ var commentDislike = document.createElement('a');
+ commentDislike.className = 'comment__action comment__action--link comment__action--dislike';
+ commentDislike.href = 'javascript:void(0);';
+ commentDislike.textContent = 'Dislike';
+ commentDislike.onclick = commentVote;
+ commentActions.appendChild(commentDislike);
+ }
+
+ // if we're executing this it's fairly obvious that we can reply,
+ // so no need to have a permission check on it here
+ var commentReply = document.createElement('label');
+ commentReply.className = 'comment__action comment__action--link';
+ commentReply.htmlFor = 'comment-reply-toggle-' + comment.comment_id;
+ commentReply.textContent = 'Reply';
+ commentActions.appendChild(commentReply);
+
+ // reply section
+ var commentReplyState = document.createElement('input');
+ commentReplyState.id = commentReply.htmlFor;
+ commentReplyState.type = 'checkbox';
+ commentReplyState.className = 'comment__reply-toggle';
+ commentReplies.appendChild(commentReplyState);
+
+ var commentReplyInput = document.createElement('form');
+ commentReplyInput.id = 'comment-reply-' + comment.comment_id;
+ commentReplyInput.className = 'comment comment--input comment--reply';
+ commentReplyInput.method = 'post';
+ commentReplyInput.action = 'javascript:void(0);';
+ commentReplyInput.onsubmit = commentPostEventHandler;
+ commentReplies.appendChild(commentReplyInput);
+
+ // reply attributes
+ var replyCategory = document.createElement('input');
+ replyCategory.name = 'comment[category]';
+ replyCategory.value = comment.category_id;
+ replyCategory.type = 'hidden';
+ commentReplyInput.appendChild(replyCategory);
+
+ var replyCsrf = document.createElement('input');
+ replyCsrf.name = 'csrf';
+ replyCsrf.value = '{{ csrf_token("comments") }}';
+ replyCsrf.type = 'hidden';
+ commentReplyInput.appendChild(replyCsrf);
+
+ var replyId = document.createElement('input');
+ replyId.name = 'comment[reply]';
+ replyId.value = comment.comment_id;
+ replyId.type = 'hidden';
+ commentReplyInput.appendChild(replyId);
+
+ var replyContainer = document.createElement('div');
+ replyContainer.className = 'comment__container';
+ commentReplyInput.appendChild(replyContainer);
+
+ // reply container
+ var replyAvatar = document.createElement('div');
+ replyAvatar.className = 'avatar comment__avatar';
+ replyAvatar.style.backgroundImage = 'url(\'/profile.php?m=avatar&u={0}\')'.replace('{0}', comment.user_id);
+ replyContainer.appendChild(replyAvatar);
+
+ var replyContent = document.createElement('div');
+ replyContent.className = 'comment__content';
+ replyContainer.appendChild(replyContent);
+
+ // reply content
+ var replyInfo = document.createElement('div');
+ replyInfo.className = 'comment__info';
+ replyContent.appendChild(replyInfo);
+
+ var replyUser = document.createElement('div');
+ replyUser.className = 'comment__user';
+ replyUser.textContent = comment.username;
+ replyUser.style.color = comment.user_colour == null || (comment.user_colour & 0x40000000) > 0
+ ? 'inherit'
+ : '#' + (comment.user_colour & 0xFFFFFF).toString(16);
+ replyInfo.appendChild(replyUser);
+
+ var replyText = document.createElement('textarea');
+ replyText.className = 'comment__text input__textarea comment__text--input';
+ replyText.name = 'comment[text]';
+ replyText.placeholder = 'Share your extensive insights...';
+ replyContent.appendChild(replyText);
+
+ var replyActions = document.createElement('div');
+ replyActions.className = 'comment__actions';
+ replyContent.appendChild(replyActions);
+
+ var replyButton = document.createElement('button');
+ replyButton.className = 'input__button comment__action comment__action--button comment__action--post';
+ replyButton.textContent = 'Reply';
+ replyActions.appendChild(replyButton);
+
+ if (isReply)
+ parent.appendChild(commentElement);
+ else
+ parent.insertBefore(commentElement, parent.firstElementChild);
+
+ timeago().render(commentTime);
+
+ var placeholder = document.getElementById('_no_comments_notice_' + comment.category_id);
+
+ if (placeholder)
+ placeholder.parentNode.removeChild(placeholder);
}
diff --git a/assets/typescript/Common.ts b/assets/typescript/Common.ts
new file mode 100644
index 00000000..82a67fe4
--- /dev/null
+++ b/assets/typescript/Common.ts
@@ -0,0 +1,3 @@
+interface Array {
+ find(predicate: (search: T) => boolean) : T;
+}
diff --git a/assets/typescript/FormUtilities.ts b/assets/typescript/FormUtilities.ts
new file mode 100644
index 00000000..b07564f5
--- /dev/null
+++ b/assets/typescript/FormUtilities.ts
@@ -0,0 +1,48 @@
+function ExtractFormData(form: HTMLFormElement, resetSource: boolean = false): FormData
+{
+ const formData: FormData = new FormData;
+
+ for (let i = 0; i < form.length; i++) {
+ let input: HTMLInputElement = form[i] as HTMLInputElement,
+ type = input.type.toLowerCase(),
+ isCheckbox = type === 'checkbox';
+
+ if (isCheckbox && !input.checked)
+ continue;
+
+ formData.append(input.name, input.value || '');
+ }
+
+ if (resetSource)
+ ResetForm(form);
+
+ return formData;
+}
+
+interface FormHiddenDefault {
+ Name: string;
+ Value: string;
+}
+
+function ResetForm(form: HTMLFormElement, defaults: FormHiddenDefault[] = []): void
+{
+ for (let i = 0; i < form.length; i++) {
+ let input: HTMLInputElement = form[i] as HTMLInputElement;
+
+ switch (input.type.toLowerCase()) {
+ case 'checkbox':
+ input.checked = false;
+ break;
+
+ case 'hidden':
+ let hiddenDefault: FormHiddenDefault = defaults.find(fhd => fhd.Name.toLowerCase() === input.name.toLowerCase());
+
+ if (hiddenDefault)
+ input.value = hiddenDefault.Value;
+ break;
+
+ default:
+ input.value = '';
+ }
+ }
+}
diff --git a/assets/typescript/misuzu.ts b/assets/typescript/misuzu.ts
index c7da76c4..aae0d959 100644
--- a/assets/typescript/misuzu.ts
+++ b/assets/typescript/misuzu.ts
@@ -3,6 +3,7 @@
///
///
///
+///
declare const timeago: any;
declare const hljs: any;
diff --git a/templates/_layout/comments.twig b/templates/_layout/comments.twig
index 94d32ef6..57ba1a3b 100644
--- a/templates/_layout/comments.twig
+++ b/templates/_layout/comments.twig
@@ -195,257 +195,17 @@
}
}
- if (typeof commentPost === 'function') { // can comment
+ if (typeof commentPostEventHandler === 'function') { // can comment
var commentForms = document.getElementsByClassName('comment--input');
for (var i = 0; i < commentForms.length; i++) {
commentForms[i].action = 'javascript:void(0);';
- commentForms[i].onsubmit = commentPost;
+ commentForms[i].onsubmit = commentPostEventHandler;
}
}
});
- {% if perms.can_comment %}
-
- {% endif %}
-
{% if perms.can_vote %}