js/controllers/CommentListViewController.js
import LoadMoreCommentsViewController from '~/controllers/LoadMoreCommentsViewController';
import CommentViewController from '~/controllers/CommentViewController';
import CommentTemplate from '~/template/CommentTemplate';
import AnimationController, { Animation } from '~/controllers/AnimationController';
import WriteCommentViewController from '~/controllers/WriteCommentViewController';
import AnimationControllerDelegate from '~/delegate/AnimationControllerDelegate';
import ViewController from '~/controllers/ViewController';
import LoadingIcon from '~/svg/LoadingIcon';
import * as Decode from '~/helpers/Decode';
import Comment from '~/models/Comment';
import Theme from '~/models/Theme';
export const OPACITY_TRANSITION_DURATION = 200; // in ms
function immediateChildWithClass(el, className) {
const children = el.children;
for (let i = 0; i < children.length; i++) {
if (children[i].classList.contains(className)) {
return children[i];
}
}
return null;
}
/**
* Manages a list of comments
*/
export default class CommentListViewController extends ViewController {
/**
* Creates the comment list controller from HTML element.
* @param {HTMLElement} comment - List physical element
* @param {Post|Answer} owner - The owner
* @param {Object} [opts={}]] - additional options
*/
constructor(commentList, owner) {
super(commentList);
// The original node for comment list.
this._node = commentList;
/** @type {Post|Answer|Comment} */
this.owner = owner;
// The location to prepend instances
this._prependRef = immediateChildWithClass(this._node, 'comment--prepend-ref');
this._appendRef = immediateChildWithClass(this._node, 'comment--append-ref');
this._appendRefFirst = immediateChildWithClass(this._node, 'comment--append-first-ref');
// if there is a 'Write Comment' big button
const writeCommentButton = immediateChildWithClass(this._node, 'comment-item__write-init');
if (writeCommentButton) {
new WriteCommentViewController(
writeCommentButton,
owner,
this
);
}
// Both of these probably don't exist
const loadMoreButton = immediateChildWithClass(this._node, 'comment-item__load-more');
if (loadMoreButton) {
// If we should use Expand Button versus Load More button
const preferExpand = loadMoreButton.classList.contains('comment-item__load-more--expand');
new LoadMoreCommentsViewController(
loadMoreButton,
owner,
this,
preferExpand
);
}
}
/**
* For static construction this will setup the sublists
*/
setupSublists() {
const children = this._node.children;
for (let i = 0; i < children.length; i++) {
if (children[i].classList.contains('comment')) {
const sublist = new CommentViewController(
children[i],
Comment.fromJSON(
Decode.b64toJSON(children[i].dataset.comment)
)
);
sublist.subCommentList.setupSublists();
}
}
}
/**
* Creates a 'loading' instance in this comment list.
* @param {string} message what to display in box
* @param {InstanceType} type The type of instance to add
* @param {boolean} [animated=true] If we should animate element
* @return {Object} has `async .destroy()` function to destroy loading instance. Also `.node`
*/
createLoadingInstance(message, type = InstanceType.prepend, animated = true) {
const loadingHTML = (
<li class="comment-item comment-loading">
{ LoadingIcon.cloneNode(true) } { message }
</li>
);
let animationController = animated ? new AnimationController(
loadingHTML,
[ Animation.expand.height ]
) : null;
this.addInstance(loadingHTML, type);
if (animationController !== null) {
animationController.triggerAnimation();
animationController.delegate.didUnfinishAnimation = (controller) => {
this._node.removeChild(loadingHTML);
}
}
// Make sure the loading instance lasts at least 100ms
return {
node: loadingHTML,
destroy: () => {
if (animationController === null) {
this._node.removeChild(loadingHTML);
} else {
animationController.untriggerAnimation();
}
}
}
}
/**
* Reports an error
*/
createErrorInstance(message) {
// TODO:
}
/**
* Creates a comment in the list
* @param {Comment} comment The comment to add
* @param {InstanceType} type Where it should be added
* @return {HTMLElement} created element
*/
async createCommentInstance(comment, type = InstanceType.prepend) {
const commentContent = await new CommentTemplate(comment);
const commentHTML = <li/>;
commentContent.loadInContext(commentHTML);
this.addInstance(commentHTML, type);
return commentHTML;
}
/**
* Creates a 'grouped comment' instance. i.e. group of comments. The difference between this and calling
* `createCommentInstance` multiple times is this will perform a proper animation of the nodes.
*
* @param {Comment[]} comments List of all comments
* @param {InstanceType} type Where it should be added
* @param {boolean} [animated=false] If should be animated
* @param {Object} constructionOptions - options when constructing each comment
* @return {HTMLElement} created element
*/
async createMultipleCommentInstances(comments, type = InstanceType.prepend, animated = false, constructionOptions = {}) {
const bodies = await Promise.all(
comments.map(comment => new CommentTemplate(comment, constructionOptions))
);
const commentHTML = <li/>;
for (let i = 0; i < bodies.length; i++) {
bodies[i].loadInContext(commentHTML);
}
this.addInstance(commentHTML, type);
if (animated) {
const animationController = new AnimationController(
commentHTML,
[ Animation.expand.height ]
);
animationController.triggerAnimation();
}
return commentHTML;
}
/**
* Adds an instance
* @param {HTMLElement} html HTML node
* @param {InstanceType} type The instance type
*/
addInstance(html, type) {
const ref = InstanceType.getReference(type, this);
this._node.insertBefore(html, ref);
html.style.opacity = 0;
// Wait for transition to finish, then change
setTimeout(() => { html.style.opacity = 1 }, OPACITY_TRANSITION_DURATION);
}
}
export const InstanceType = {
append: Symbol('CommentList.InstanceType.append'),
prepend: Symbol('CommentList.InstanceType.prepend'),
appendFirst: Symbol('CommentList.InstanceType.appendFirst'),
getReference(ty, commentList) {
if (ty === InstanceType.append) return commentList._appendRef.nextSibling;
else if (ty === InstanceType.prepend) return commentList._prependRef;
else if (ty === InstanceType.appendFirst) return commentList._appendRefFirst;
else return null;
}
};