' + inline.marks[number] + '
';
});
}
// Join paragraphs
string = ps.join (this.setup['server-eol'] + this.setup['server-eol']);
// Replace code block markers with original markdown code
string = string.replace (this.rx.md.blockMarker, function (marker, number) {
return '' + block.marks[number] + '
';
});
return string;
};
// Collection of convenient string related functions (strings.js)
HashOverConstructor.prototype.strings = {
// sprintf specifiers regular expression
specifiers: /%([cdfs])/g,
// Curly-brace variable regular expression
curlyBraces: /(\{.+?\})/g,
// Curly-brace variable name regular expression
curlyNames: /\{(.+?)\}/,
// Simplistic JavaScript port of sprintf function in C
sprintf: function (string, args)
{
var string = string || '';
var args = args || [];
var count = 0;
// Replace specifiers with array items
return string.replace (this.specifiers, function (match, type)
{
// Return original specifier if there isn't an item for it
if (args[count] === undefined) {
return match;
}
// Switch through each specific type
switch (type) {
// Single characters
case 'c': {
// Use only first character
return args[count++][0];
}
// Integer numbers
case 'd': {
// Parse item as integer
return parseInt (args[count++]);
}
// Floating point numbers
case 'f': {
// Parse item as float
return parseFloat (args[count++]);
}
// Strings
case 's': {
// Use string as-is
return args[count++];
}
}
});
},
// Converts a string containing {curly} variables into an array
templatifier: function (text)
{
// Split string by curly variables
var template = text.split (this.curlyBraces);
// Initial variable indexes
var indexes = {};
// Run through template
for (var i = 0, il = template.length; i < il; i++) {
// Get curly variable names
var curly = template[i].match (this.curlyNames);
// Check if any curly variables exist
if (curly !== null && curly[1] !== undefined) {
// If so, store the name
var name = curly[1];
// Check if variable was previously encountered
if (indexes[name] !== undefined) {
// If so, add index to existing indexes
indexes[name].push (i);
} else {
// If not, create indexes
indexes[name] = [ i ];
}
// And remove curly variable from template
template[i] = '';
}
}
// Return template and indexes
return {
template: template,
indexes: indexes
}
},
// Templatify UI HTML from backend
templatify: function (ui)
{
// Initial template
var template = {};
// Templatify each UI HTML string
for (var name in ui) {
if (ui.hasOwnProperty (name) === true) {
template[name] = this.templatifier (ui[name]);
}
}
return template;
},
// Parses an HTML template
parseTemplate: function (template, data)
{
// Clone template
var textClone = template.template.slice ();
// Run through template data
for (var name in data) {
// Store indexes
var indexes = template.indexes[name];
// Do nothing if no indexes exist for data
if (indexes === undefined) {
continue;
}
// Otherwise, add data at each index of template
for (var i = 0, il = indexes.length; i < il; i++) {
textClone[(indexes[i])] = data[name];
}
}
// Merge template clone to string
var text = textClone.join ('');
return text;
}
};
// Calls a method that may or may not exist (optionalmethod.js)
HashOverConstructor.prototype.optionalMethod = function (name, args, object)
{
var method = object ? this[object][name] : this[name];
var context = object ? this[object] : this;
// Check if the method exists
if (method && typeof (method) === 'function') {
return method.apply (context, args);
}
};
// Add comment parsing regular expressions (parsecomment.js)
HashOverConstructor.prototype.rx.html = {
// URL replacement for automatic hyperlinks
linksReplace: '$1',
// Matches various line ending styles
lines: /(?:\r\n|\r|\n)/g,
// For tags
code: {
// Matches opening
open: //i,
// Replacement for code tag processing
replace: /()([\s\S]*?)(<\/code>)/ig,
// Matches code tag markers
marker: /CODE_TAG\[([0-9]+)\]/g
},
// For tags
pre: {
// Matches opening
open: //i,
// Replacement for pre tag processing
replace: /()([\s\S]*?)(<\/pre>)/ig,
// Matches pre tag markers
marker: /PRE_TAG\[([0-9]+)\]/g
},
// Tags that will have their inner HTML trimmed
trimTags: {
// Matches blockquote/ul/ol tags openings
open: /<(blockquote|ul|ol)>/,
// Replacement for blockquote/ul/ol trimming
replace: /(<(blockquote|ul|ol)>)([\s\S]*?)(<\/\2>)/ig
}
};
// Add comment content to HTML template (parsecomment.js)
HashOverConstructor.prototype.parseComment = function (comment, parent, collapse, popular)
{
// Parameter defaults
parent = parent || null;
// Reference to this object
var hashover = this;
var commentKey = comment.permalink;
var permalink = this.prefix (commentKey);
var nameClass = 'hashover-name-plain';
var commentDate = comment.date;
var codeTagCount = 0;
var codeTags = [];
var preTagCount = 0;
var preTags = [];
var classes = '';
var replies = '';
// Get instantiated prefix
var prefix = this.prefix ();
// Initial template
var template = {
hashover: prefix,
permalink: commentKey
};
// Text for avatar image alt attribute
var permatext = commentKey.slice(1).split('r').pop();
// Check if this comment is a popular comment
if (popular === true) {
// Attempt to get parent comment permalink
parent = this.permalinkParent (commentKey);
// Get parent comment by its permalink if it exists
if (parent !== null) {
parent = this.permalinkComment (parent, this.instance.comments.primary);
}
// And remove "-pop" from text for avatar
permatext = permatext.replace ('-pop', '');
} else {
// Append class to indicate comment is a reply when appropriate
if (parent !== null) {
classes += ' hashover-reply';
}
// Check if we have comments to collapse
if (collapse === true && this.instance['total-count'] > 0) {
// If so, check if we've reached the collapse limit
if (this.instance.collapseLimit >= this.setup['collapse-limit']) {
// If so, append class to indicate collapsed comment
classes += ' hashover-hidden';
} else {
// If not, increase collapse limit
this.instance.collapseLimit++;
}
}
}
// Add avatar image to template
template.avatar = this.strings.parseTemplate (
this.ui['user-avatar'], {
src: comment.avatar,
href: permalink,
text: permatext
}
);
// Check if comment is not a notice
if (comment.notice === undefined) {
// If so, define commenter name
var name = comment.name || this.setup['default-name'];
// Initial website
var website = comment.website;
// Name is Twitter handle indicator
var isTwitter = (name.charAt (0) === '@');
// Check if user's name is a Twitter handle
if (isTwitter === true) {
// If so, remove the leading "@" character
name = name.slice (1);
// Set Twitter name class
nameClass = 'hashover-name-twitter';
// Get the name length
var nameLength = name.length;
// Check if Twitter handle is valid length
if (nameLength > 1 && nameLength <= 30) {
// Set website to Twitter profile if a specific website wasn't given
if (website === undefined) {
website = 'http://twitter.com/' + name;
}
}
}
// Check whether user gave a website
if (website !== undefined) {
// If so, set normal website class where appropriate
if (isTwitter === false) {
nameClass = 'hashover-name-website';
}
// And set name as a hyperlink
var nameElement = this.strings.parseTemplate (
this.ui['name-link'], {
hashover: prefix,
href: website,
permalink: commentKey,
name: name
}
);
} else {
// If not, set name as plain text
var nameElement = this.strings.parseTemplate (
this.ui['name-span'], {
hashover: prefix,
permalink: commentKey,
name: name
}
);
}
// Construct thread link
if (this.ui['thread-link'] !== undefined) {
if ((comment.url && comment.title) !== undefined) {
template['thread-link'] = this.strings.parseTemplate (
this.ui['thread-link'], {
url: comment.url,
title: comment.title
}
);
}
}
// Check if comment has a parent
if (parent !== null && this.ui['parent-link'] !== undefined) {
// If so, create the parent thread permalink
var parentThread = 'hashover-' + parent.permalink;
// Get the parent's name
var parentName = parent.name || this.setup['default-name'];
// Add thread parent hyperlink to template
template['parent-link'] = this.strings.parseTemplate (
this.ui['parent-link'], {
hashover: prefix,
href: comment.url || this.instance['file-path'],
parent: parentThread,
permalink: commentKey,
name: parentName
}
);
}
// Check if the logged in user owns the comment
if (comment['user-owned'] !== undefined) {
// If so, append class to indicate comment is from logged in user
classes += ' hashover-user-owned';
// Define "Reply" link with original poster title
var replyTitle = this.locale['commenter-tip'];
var replyClass = 'hashover-no-email';
} else {
// Check if commenter is subscribed
if (comment.subscribed === true) {
// If so, set subscribed title
var replyTitle = name + ' ' + this.locale['subscribed-tip'];
var replyClass = 'hashover-has-email';
} else{
// If not, set unsubscribed title
var replyTitle = name + ' ' + this.locale['unsubscribed-tip'];
var replyClass = 'hashover-no-email';
}
}
// Check if the comment is editable for the user
if ((comment['editable'] && this.ui['edit-link']) !== undefined) {
// If so, add "Edit" hyperlink to template
template['edit-link'] = this.strings.parseTemplate (
this.ui['edit-link'], {
hashover: prefix,
href: comment.url || this.instance['file-path'],
permalink: commentKey
}
);
}
// Add like link and count to template if likes are enabled
if (this.setup['allows-likes'] !== false) {
this.addRatings (comment, template, 'like', commentKey);
}
// Add dislike link and count to template if dislikes are enabled
if (this.setup['allows-dislikes'] !== false) {
this.addRatings (comment, template, 'dislike', commentKey);
}
// Add name HTML to template
template.name = this.strings.parseTemplate (
this.ui['name-wrapper'], {
class: nameClass,
link: nameElement
}
);
// Add IP address HTML to template
if (comment.ipaddr !== undefined) {
template['ipaddr'] = this.strings.parseTemplate (
this.ui['ip-span'], {
ipaddr: comment.ipaddr
}
);
}
// Append status text to date
if (comment['status-text'] !== undefined) {
commentDate += ' (' + comment['status-text'] + ')';
}
// Add date from comment as permalink hyperlink to template
template.date = this.strings.parseTemplate (
this.ui['date-link'], {
hashover: prefix,
href: comment.url || this.instance['file-path'],
permalink: 'hashover-' + commentKey,
title: comment['date-time'],
date: commentDate
}
);
// Add "Reply" hyperlink to template
template['reply-link'] = this.strings.parseTemplate (
this.ui['reply-link'], {
hashover: prefix,
href: comment.url || this.instance['file-path'],
permalink: commentKey,
class: replyClass,
title: replyTitle
}
);
// Add reply count to template
if (comment.replies !== undefined) {
template['reply-count'] = comment.replies.length;
if (template['reply-count'] > 0) {
if (template['reply-count'] !== 1) {
template['reply-count'] += ' ' + this.locale['replies'];
} else {
template['reply-count'] += ' ' + this.locale['reply'];
}
}
}
// Add HTML anchor tag to URLs
var body = comment.body.replace (this.rx.links, this.rx.html.linksReplace);
// Replace [img] tags with placeholders if embedded images are enabled
if (hashover.setup['allows-images'] !== false) {
body = body.replace (this.rx.imageTags, function (m, link, url) {
return hashover.embedImage.apply (hashover, arguments);
});
}
// Parse markdown in comment if enabled
if (this.setup['uses-markdown'] !== false) {
body = this.parseMarkdown (body);
}
// Check if there are code tags in the comment
if (this.rx.html.code.open.test (body) === true) {
// If so, define regular expression callback
var codeReplacer = function (fullTag, open, html, close) {
// Create code marker
var codeMarker = open + 'CODE_TAG[' + codeTagCount + ']' + close;
// Store original HTML for later re-injection
codeTags[codeTagCount] = hashover.EOLTrim (html);
// Increase code tag count
codeTagCount++;
// Return code tag marker
return codeMarker;
};
// And replace code tags with marker text
body = body.replace (this.rx.html.code.replace, codeReplacer);
}
// Check if there are pre tags in the comment
if (this.rx.html.pre.open.test (body) === true) {
// If so, define regular expression callback
var preReplacer = function (fullTag, open, html, close) {
// Create pre marker
var preMarker = open + 'PRE_TAG[' + preTagCount + ']' + close;
// Store original HTML for later re-injection
preTags[preTagCount] = hashover.EOLTrim (html);
// Increase pre tag count
preTagCount++;
// Return pre tag marker
return preMarker;
};
// And replace pre tags with marker text
body = body.replace (this.rx.html.pre.replace, preReplacer);
}
// Check if comment has whitespace to be trimmed
if (this.rx.html.trimTags.open.test (body) === true) {
// If so, define a regular expression callback
var tagTrimmer = function (fullTag, open, name, html, close) {
return open + hashover.EOLTrim (html) + close;
};
// And trim whitespace from comment
body = body.replace (this.rx.html.trimTags.replace, tagTrimmer);
}
// Break comment into paragraphs
var paragraphs = body.split (this.rx.paragraphs);
// Initial paragraph'd comment
var pdComment = '';
// Run through paragraphs
for (var i = 0, il = paragraphs.length; i < il; i++) {
// Replace single line breaks with break tags
var lines = paragraphs[i].replace (this.rx.html.lines, '
');
// Wrap comment in paragraph tags
pdComment += '' + lines + '
' + this.setup['server-eol'];
}
// Replace code tag markers with original code tag HTML
if (codeTagCount > 0) {
pdComment = pdComment.replace (this.rx.html.code.marker, function (m, i) {
return codeTags[i];
});
}
// Replace pre tag markers with original pre tag HTML
if (preTagCount > 0) {
pdComment = pdComment.replace (this.rx.html.pre.marker, function (m, i) {
return preTags[i];
});
}
// Add comment data to template
template.comment = pdComment;
} else {
// Append notice class
classes += ' hashover-notice ' + comment['notice-class'];
// Add notice to template
template.comment = comment.notice;
// Add name HTML to template
template.name = this.strings.parseTemplate (
this.ui['name-wrapper'], {
class: nameClass,
link: comment.title
}
);
}
// Comment HTML template
var html = this.strings.parseTemplate (this.ui['theme'], template);
// Check if comment has replies
if (comment.replies !== undefined) {
// If so, append class to indicate comment has replies
classes += ' hashover-has-replies';
// Recursively parse replies
for (var i = 0, il = comment.replies.length; i < il; i++) {
replies += this.parseComment (comment.replies[i], comment, collapse);
}
}
// Wrap comment HTML
var wrapper = this.strings.parseTemplate (
this.ui['comment-wrapper'], {
hashover: prefix,
permalink: commentKey,
class: classes,
html: html + replies
}
);
return wrapper;
};
// Converts an HTML string to DOM elements (htmlchildren.js)
HashOverConstructor.prototype.htmlChildren = function (html)
{
// Create a div to place the HTML into for parsing
var div = this.createElement ('div', {
innerHTML: html
});
// Return the child elements
return div.children;
};
// For appending new comments to the thread on page (appendcomments.js)
HashOverConstructor.prototype.appendComments = function (comments, dest, parent)
{
// Set append element to more section
dest = dest || this.instance['sort-section'];
// HTML parsing time
var htmlTime = 0;
// Run through each comment
for (var i = 0, il = comments.length; i < il; i++) {
// Current comment
var comment = comments[i];
// Attempt to get the comment element
var element = this.getElement (comment.permalink);
// Check if comment exists
if (element !== null) {
// If so, re-append the comment element
element.parentNode.appendChild (element);
// Check comment's replies
if (comment.replies !== undefined) {
this.appendComments (comment.replies, element, comment);
}
// And do nothing else
continue;
}
// Parse comment
var html = this.parseComment (comment, parent);
// HTML parsing start time
var htmlStart = Date.now ();
// Check if we can insert HTML adjacently
if ('insertAdjacentHTML' in dest) {
// If so, insert comment adjacently
dest.insertAdjacentHTML ('beforeend', html);
} else {
// If not, convert HTML to NodeList
var element = this.htmlChildren (html);
// And append the first node
dest.appendChild (element[0]);
}
// HTML parsing end time
var htmlEnd = Date.now ();
// Add to HTML parsing time
htmlTime += htmlEnd - htmlStart;
// Add controls to the comment
this.addControls (comment);
}
// Re-append more comments link
this.reappendMoreLink ();
// And return HTML parsing
return htmlTime;
};
// Initial timeouts (messages.js)
HashOverConstructor.prototype.messageTimeouts = {};
// Gets a computed element style by property (messages.js)
HashOverConstructor.prototype.computeStyle = function (element, property, type)
{
// Check for modern browser support (Mozilla Firefox, Google Chrome)
if (window.getComputedStyle !== undefined) {
// If found, get the computed styles for the element
var computedStyle = window.getComputedStyle (element, null);
// And get the specific property
computedStyle = computedStyle.getPropertyValue (property);
} else {
// Otherwise, assume we're in IE
var computedStyle = element.currentStyle[property];
}
// Cast value to specified type
switch (type) {
case 'int': {
computedStyle = computedStyle.replace (/px|em/, '');
computedStyle = parseInt (computedStyle) || 0;
break;
}
case 'float': {
computedStyle = computedStyle.replace (/px|em/, '');
computedStyle = parseFloat (computedStyle) || 0.0;
break;
}
}
return computedStyle;
};
// Gets the client height of a message element (messages.js)
HashOverConstructor.prototype.getHeight = function (element, setChild)
{
// Get first child of message element
var firstChild = element.children[0];
// Set max-height style to initial
firstChild.style.maxHeight = 'initial';
// Get various computed styles
var borderTop = this.computeStyle (firstChild, 'border-top-width', 'int');
var borderBottom = this.computeStyle (firstChild, 'border-bottom-width', 'int');
var marginBottom = this.computeStyle (firstChild, 'margin-bottom', 'int');
var border = borderTop + borderBottom;
// Calculate its client height
var maxHeight = firstChild.clientHeight + border + marginBottom;
// Set its max-height style as well if told to
if (setChild === true) {
firstChild.style.maxHeight = maxHeight + 'px';
} else {
firstChild.style.maxHeight = '';
}
return maxHeight;
};
// Open a message element (messages.js)
HashOverConstructor.prototype.openMessage = function (element)
{
// Reference to this object
var hashover = this;
// Add classes to indicate message element is open
this.classes.remove (element, 'hashover-message-animated');
this.classes.add (element, 'hashover-message-open');
// Get height of element
var maxHeight = this.getHeight (element);
// Get first child of message element
var firstChild = element.children[0];
// Remove class indicating message element is open
this.classes.remove (element, 'hashover-message-open');
setTimeout (function () {
// Add class to indicate message element is open
hashover.classes.add (element, 'hashover-message-open');
hashover.classes.add (element, 'hashover-message-animated');
// Set max-height styles
element.style.maxHeight = maxHeight + 'px';
firstChild.style.maxHeight = maxHeight + 'px';
// Set max-height style to initial after transition
setTimeout (function () {
element.style.maxHeight = 'initial';
firstChild.style.maxHeight = 'initial';
}, 150);
}, 150);
};
// Close a message element (messages.js)
HashOverConstructor.prototype.closeMessage = function (element)
{
// Reference to this object
var hashover = this;
// Set max-height style to specific height before transition
element.style.maxHeight = this.getHeight (element, true) + 'px';
setTimeout (function () {
// Remove max-height style from message elements
element.children[0].style.maxHeight = '';
element.style.maxHeight = '';
// Remove classes indicating message element is open
hashover.classes.remove (element, 'hashover-message-open');
hashover.classes.remove (element, 'hashover-message-error');
}, 150);
};
// Handle message element(s) (messages.js)
HashOverConstructor.prototype.showMessage = function (messageText, type, permalink, error)
{
// Reference to this object
var hashover = this;
// Check if message is in an edit form
if (type === 'edit') {
// If so, get message from edit form by permalink
var container = this.getElement ('edit-message-container-' + permalink);
var message = this.getElement ('edit-message-' + permalink);
} else {
// If not, check if message is anything other than a reply
if (type !== 'reply') {
// If so, get primary message element
var container = this.getElement ('message-container');
var message = this.getElement ('message');
} else {
// If not, get message from reply form by permalink
var container = this.getElement ('reply-message-container-' + permalink);
var message = this.getElement ('reply-message-' + permalink);
}
}
// Check if the message isn't empty
if (messageText !== undefined && messageText !== '') {
// Add message text to element
message.textContent = messageText;
// Add class to indicate message is an error if set
if (error === true) {
this.classes.add (container, 'hashover-message-error');
}
}
// Add class to indicate message element is open
this.openMessage (container);
// Instantiated permalink as timeout key
var key = this.prefix (permalink);
// Add the comment to message counts
if (this.messageTimeouts[key] === undefined) {
this.messageTimeouts[key] = {};
}
// Clear necessary timeout
if (this.messageTimeouts[key][type] !== undefined) {
clearTimeout (this.messageTimeouts[key][type]);
}
// Add timeout to close message element after 10 seconds
this.messageTimeouts[key][type] = setTimeout (function () {
hashover.closeMessage (container);
}, 10000);
};
// Handles display of various email warnings (validateemail.js)
HashOverConstructor.prototype.emailValidator = function (form, subscribe, type, permalink)
{
// Do nothing if email form doesn't exist
if (form.email === undefined) {
return true;
}
// Check if email form is empty
if (form.email.value === '') {
// If so, return true if user unchecked subscribe checkbox
if (this.getElement(subscribe).checked === false) {
return true;
}
// Ask user if they are sure they don't want reply notifications
var notifications = confirm (this.locale['no-email-warning']);
// Check if user did not confirm
if (notifications === false) {
// If so, focus email field
form.email.focus ();
// And return false
return false;
}
} else {
// If not, check if email is valid
if (this.rx.email.test (form.email.value) === false) {
// If so, check if user unchecked subscribe checkbox
if (this.getElement(subscribe).checked === false) {
// If so, remove email address
form.email.value = '';
// And return true
return true;
}
// Otherwise, get message from locales
var message = this.locale['invalid-email'];
// Show message
this.showMessage (message, type, permalink, true);
// Focus email input
form.email.focus ();
// And return false
return false;
}
}
// Otherwise, return true
return true;
};
// Validate a comment form e-mail field (validateemail.js)
HashOverConstructor.prototype.validateEmail = function (type, permalink, form)
{
// Subscribe checkbox ID
var subscribe = type + '-subscribe';
// Append permalink if form is a reply or edit
if (type === 'reply' || type === 'edit') {
subscribe += '-' + permalink;
}
// Attempt to validate form fields
var valid = this.emailValidator (form, subscribe, type, permalink);
// And return validity
return valid;
};
// Validate a comment form (validatecomment.js)
HashOverConstructor.prototype.commentValidator = function (form, type, skipComment)
{
// Check each input field for if they are required
for (var field in this.setup['form-fields']) {
// Skip other people's prototypes
if (this.setup['form-fields'].hasOwnProperty (field) !== true) {
continue;
}
// Check if the field is required, and that the input exists
if (this.setup['form-fields'][field] === 'required' && form[field] !== undefined) {
// Check if it has a value
if (form[field].value === '') {
// If not, add a class indicating a failed post
this.classes.add (form[field], 'hashover-emphasized-input');
// Focus the input
form[field].focus ();
// Return error message to display to the user
return this.strings.sprintf (this.locale['field-needed'], [
this.locale[field]
]);
}
// And remove class indicating a failed post
this.classes.remove (form[field], 'hashover-emphasized-input');
}
}
// Check if a comment was given
if (skipComment !== true && form.comment.value === '') {
// If not, add a class indicating a failed post
this.classes.add (form.comment, 'hashover-emphasized-input');
// Focus the comment textarea
form.comment.focus ();
// Error message to display to the user
var localeKey = (type === 'reply') ? 'reply-needed' : 'comment-needed';
var errorMessage = this.locale[localeKey];
// Return a error message to display to the user
return errorMessage;
}
// And return true
return true;
};
// Validate required comment credentials (validatecomment.js)
HashOverConstructor.prototype.validateComment = function (form, type, permalink, skipComment)
{
// Attempt to validate comment
var message = this.commentValidator (form, type, skipComment);
// Check if comment is invalid
if (message !== true) {
// If so, display validator's message
this.showMessage (message, type, permalink, true);
// And return false
return false;
}
// Validate e-mail if user isn't logged in or is editing
if (this.setup['user-is-logged-in'] === false || type === 'edit') {
// Return false on any failure
if (this.validateEmail (type, permalink, form) === false) {
return false;
}
}
// And return true
return true;
};
// For adding new comments to comments array (addcomments.js)
HashOverConstructor.prototype.addComments = function (comment, type)
{
// Check if comment is a reply
if (type === 'reply') {
// If so, fetch parent comment by its permalink
var parent = this.permalinkComment (
this.permalinkParent (comment.permalink),
this.instance.comments.primary
);
// Check if the parent comment exists
if (parent !== null) {
// If so, check if comment has replies
if (parent.replies !== undefined) {
// If so, append comment to replies
parent.replies.push (comment);
} else {
// If not, create replies array
parent.replies = [ comment ];
}
// And do nothing else
return;
}
}
// Otherwise, append to primary comments
this.instance.comments.primary.push (comment);
};
// Increase comment counts (ajaxpost.js)
HashOverConstructor.prototype.incrementCounts = function (type)
{
// Count top level comments
if (type !== 'reply') {
this.instance['primary-count']++;
}
// Increase all count
this.instance['total-count']++;
};
// For posting comments (ajaxpost.js)
HashOverConstructor.prototype.AJAXPost = function (json, permalink, type)
{
// Reference to this object
var hashover = this;
// Check if comment is a reply
if (type === 'reply') {
// If so, get element of comment being replied to
var dest = this.getElement (permalink);
} else {
// If not, use sort section element
var dest = this.instance['sort-section'];
}
// Get primary comments in order
var comments = this.instance.comments.primary;
// Check if there are no comments
if (this.instance['total-count'] === 0) {
// If so, replace "Be the first to comment!"
this.instance.comments.primary[0] = json.comment;
// And place comment on page
dest.innerHTML = this.parseComment (json.comment);
} else {
// If not, add comment to comments array
this.addComments (json.comment, type);
// Sort comments if sort method drop down menu exists
this.elementExists ('sort-select', function (sortSelect) {
comments = hashover.sortComments (comments, sortSelect.value);
});
// And append comments
this.appendComments (comments);
}
// Add controls to the new comment
this.addControls (json.comment);
// Update comment count
this.elementExists ('count', function (count) {
count.textContent = json.count;
});
// Show comment count wrapper
this.elementExists ('count-wrapper', function (countWrapper) {
countWrapper.style.display = '';
});
// Increment counts
this.incrementCounts (type);
};
// For editing comments (ajaxedit.js)
HashOverConstructor.prototype.AJAXEdit = function (json, permalink)
{
// Get old comment element
var comment = this.getElement (permalink);
// Get old comment from primary comments
var oldItem = this.permalinkComment (permalink, this.instance.comments.primary);
// Get new comment child elements
var newComment = this.htmlChildren (this.parseComment (json.comment));
// Get old and new comment elements
var newElements = newComment[0].children;
var oldElements = comment.children;
// Replace old comment with edited comment
for (var i = newElements.length - 1; i >= 0; i--) {
comment.replaceChild (newElements[i], oldElements[i]);
}
// Add controls back to the comment
this.addControls (json.comment);
// Update primary comments with edited comment
for (var attribute in json.comment) {
if (json.comment.hasOwnProperty (attribute) === true) {
oldItem[attribute] = json.comment[attribute];
}
}
};
// Posts comments via AJAX (postrequest.js)
HashOverConstructor.prototype.postRequest = function (form, button, type, permalink, callback)
{
// Reference to this object
var hashover = this;
// Form inputs
var inputs = form.elements;
// Initial request queries
var queries = [];
// Get all form input names and values
for (var i = 0, il = inputs.length; i < il; i++) {
// Skip submit inputs
if (inputs[i].type === 'submit') {
continue;
}
// Skip unchecked checkboxes
if (inputs[i].type === 'checkbox' && inputs[i].checked !== true) {
continue;
}
// Otherwise, get encoded input value
var value = encodeURIComponent (inputs[i].value);
// Add query to queries array
queries.push (inputs[i].name + '=' + value);
}
// Add final queries
queries = queries.concat ([
// Add current client time
'time=' + HashOverConstructor.getClientTime (),
// Add AJAX indicator
'ajax=yes'
]);
// Create post comment request queries
var postQueries = queries.concat ([
button.name + '=' + encodeURIComponent (button.value)
]);
// Send request to post comment
this.ajax ('POST', form.action, postQueries, function (json) {
// Afterwards, check if JSON contains no comment
if (json.comment === undefined) {
// If so, display message returned instead
hashover.showMessage (json.message, type, permalink, true);
// And return false
return false;
}
// Execute callback function if one was provided
if (typeof (callback) === 'function') {
callback ();
}
// Otherwise, check if comment is anything other than an edit
if (type !== 'edit') {
// If so, execute primary comment post function
hashover.AJAXPost.apply (hashover, [ json, permalink, type ]);
} else {
// If not, execute comment edit function
hashover.AJAXEdit.apply (hashover, [ json, permalink ]);
}
// Get the comment element by its permalink
var scrollToElement = hashover.getElement (json.comment.permalink);
// Scroll comment into view
scrollToElement.scrollIntoView ({
behavior: 'smooth',
block: 'start',
inline: 'start'
});
// And clear the comment form
form.comment.value = '';
// Re-enable button on success
setTimeout (function () {
button.disabled = false;
}, 1000);
}, true);
// Re-enable button after 10 seconds
setTimeout (function () {
button.disabled = false;
}, 10000);
// And return false
return false;
};
// For posting comments, both traditionally and via AJAX (postcomment.js)
HashOverConstructor.prototype.postComment = function (form, button, type, permalink, callback)
{
// Return false if comment is invalid
if (this.validateComment (form, type, permalink) === false) {
return false;
}
// Disable button
setTimeout (function () {
button.disabled = true;
}, 250);
// Post by sending an AJAX request if enabled
if (this.setup['uses-ajax'] !== false) {
return this.postRequest.apply (this, arguments);
}
// Re-enable button after 10 seconds
setTimeout (function () {
button.disabled = false;
}, 10000);
// And return true
return true;
};
// Generate file from permalink (permalinkfile.js)
HashOverConstructor.prototype.permalinkFile = function (permalink)
{
// Remove leading 'c'
var file = permalink.slice (1);
// Replace 'r' by '-'
file = file.replace (/r/g, '-');
// Remove "-pop" if present
file = file.replace ('-pop', '');
return file;
};
// Changes a given hyperlink into a "Cancel" hyperlink (cancelswitcher.js)
HashOverConstructor.prototype.cancelSwitcher = function (form, link, wrapper, permalink)
{
// Initial state properties of hyperlink
var reset = {
textContent: link.textContent,
title: link.title,
onclick: link.onclick
};
function linkOnClick ()
{
// Remove fields from form wrapper
wrapper.textContent = '';
// Reset button
link.textContent = reset.textContent;
link.title = reset.title;
link.onclick = reset.onclick;
return false;
}
// Change hyperlink to "Cancel" hyperlink
link.textContent = this.locale['cancel'];
link.title = this.locale['cancel'];
// This resets the "Cancel" hyperlink to initial state onClick
link.onclick = linkOnClick;
// Check if cancel buttons are enabled
if (this.setup['uses-cancel-buttons'] !== false) {
// If so, get "Cancel" button
var cancelButtonId = form + '-cancel-' + permalink;
var cancelButton = this.getElement (cancelButtonId);
// Attach event listeners to "Cancel" button
cancelButton.onclick = linkOnClick;
}
};
// Attach click event to formatting revealer hyperlinks (formattingonclick.js)
HashOverConstructor.prototype.formattingOnclick = function (type, permalink)
{
// Prepend dash to permalink if present
permalink = permalink ? '-' + permalink : '';
// Reference to this object
var hashover = this;
// Get "Formatting" hyperlink element
var link = this.getElement (type + '-formatting' + permalink);
// Get formatting message element
var message = this.getElement (type + '-formatting-message' + permalink);
// Attach click event to formatting revealer hyperlink
link.onclick = function ()
{
// Check if message is open
if (hashover.classes.contains (message, 'hashover-message-open')) {
// If so, close it
hashover.closeMessage (message);
// And do nothing else
return false;
}
// Otherwise, open it
hashover.openMessage (message);
return false;
}
};
// Adds duplicate event listeners to an element (duplicateproperties.js)
HashOverConstructor.prototype.duplicateProperties = function (element, names, value)
{
// Initial properties
var properties = {};
// Construct a properties object with duplicate values
for (var i = 0, il = names.length; i < il; i++) {
properties[(names[i])] = value;
}
// Add the properties to the object
element = this.addProperties (element, properties);
return element;
};
// Shorthand for `Document.getElementById` (getelement.js)
HashOverConstructor.prototype.getElement = function (id, asIs)
{
// Prepend pseudo-namespace prefix unless told not to
id = (asIs === true) ? id : this.prefix (id);
// Attempt to get the element by its ID
var element = document.getElementById (id);
// And return element
return element;
};
// Execute callback function if element isn't false (getelement.js)
HashOverConstructor.prototype.elementExists = function (id, callback, asIs)
{
// Attempt to get element
var element = this.getElement (id, asIs);
// Execute callback if element exists
if (element !== null) {
return callback (element);
}
// Otherwise, return false
return false;
};
// Returns false if key event is the enter key (formevents.js)
HashOverConstructor.prototype.enterCheck = function (event)
{
return (event.keyCode === 13) ? false : true;
};
// Prevents enter key on inputs from submitting form (formevents.js)
HashOverConstructor.prototype.preventSubmit = function (form)
{
// Get login info inputs
var infoInputs = form.getElementsByClassName ('hashover-input-info');
// Set enter key press to return false
for (var i = 0, il = infoInputs.length; i < il; i++) {
infoInputs[i].onkeypress = this.enterCheck;
}
};
// Displays reply form (replytocomment.js)
HashOverConstructor.prototype.replyToComment = function (comment)
{
// Reference to this object
var hashover = this;
// Get permalink from comment
var permalink = comment.permalink;
// Get reply link element
var link = this.getElement ('reply-link-' + permalink);
// Get file
var file = this.permalinkFile (permalink);
// Create reply form element
var form = this.createElement ('form', {
id: this.prefix ('reply-' + permalink),
className: 'hashover-reply-form',
action: this.setup['http-backend'] + '/form-actions.php',
method: 'post'
});
// Place reply fields into form
form.innerHTML = this.strings.parseTemplate (
this.ui['reply-form'], {
hashover: this.prefix (),
permalink: permalink,
url: comment.url || this.instance['page-url'],
thread: comment.thread || this.instance['thread-name'],
title: comment.title || this.instance['page-title'],
file: file
}
);
// Prevent input submission
this.preventSubmit (form);
// Get form by its permalink ID
var replyForm = this.getElement ('placeholder-reply-form-' + permalink);
// Add form to page
replyForm.appendChild (form);
// Change "Reply" link to "Cancel" link
this.cancelSwitcher ('reply', link, replyForm, permalink);
// Attach event listeners to "Post Reply" button
var postReply = this.getElement ('reply-post-' + permalink);
// Attach click event to formatting revealer hyperlink
this.formattingOnclick ('reply', permalink);
// Set onclick and onsubmit event handlers
this.duplicateProperties (postReply, [ 'onclick', 'onsubmit' ], function () {
return hashover.postComment (form, this, 'reply', permalink, link.onclick);
});
// Focus comment field
form.comment.focus ();
// And return false
return true;
};
// Displays edit form (editcomment.js)
HashOverConstructor.prototype.editComment = function (comment, callback)
{
// Do nothing if the comment isn't editable
if (comment['editable'] !== true) {
return false;
}
// Reference to this object
var hashover = this;
// Path to root backend directory
var backendPath = HashOverConstructor.getBackendPath (true);
// Path to comment edit information backend script
var editInfo = backendPath + '/comment-info.php';
// Get permalink from comment JSON object
var permalink = comment.permalink;
// Get file
var file = this.permalinkFile (permalink);
// Set request queries
var queries = [
'url=' + encodeURIComponent (comment.url || this.instance['page-url']),
'thread=' + encodeURIComponent (comment.thread || this.instance['thread-name']),
'comment=' + encodeURIComponent (file)
];
// Get edit link element
var link = this.getElement ('edit-link-' + permalink);
// Set loading class to edit link
this.classes.add (link, 'hashover-loading');
// Send request for comment information
this.ajax ('post', editInfo, queries, function (info) {
// Check if request returned an error
if (info.error !== undefined) {
// If so, display error
alert (info.error);
// Remove loading class from edit link
hashover.classes.remove (link, 'hashover-loading');
// And do nothing else
return;
}
// Get and clean comment body
var body = info.body.replace (hashover.rx.links, '$1');
// Get edit form placeholder
var placeholder = hashover.getElement ('placeholder-edit-form-' + permalink);
// Available comment status options
var statuses = [ 'approved', 'pending', 'deleted' ];
// Create edit form element
var form = hashover.createElement ('form', {
id: hashover.prefix ('edit-' + permalink),
className: 'hashover-edit-form',
action: hashover.setup['http-backend'] + '/form-actions.php',
method: 'post'
});
// Place edit form fields into form
form.innerHTML = hashover.strings.parseTemplate (
hashover.ui['edit-form'], {
hashover: hashover.prefix (),
permalink: permalink,
url: comment.url || hashover.instance['page-url'],
thread: comment.thread || hashover.instance['thread-name'],
title: comment.title || hashover.instance['page-title'],
file: file,
name: info.name || '',
email: info.email || '',
website: info.website || '',
body: body
}
);
// Prevent input submission
hashover.preventSubmit (form);
// Add edit form to placeholder
placeholder.appendChild (form);
// Set status dropdown menu option to comment status
hashover.elementExists ('edit-status-' + permalink, function (status) {
if (comment.status !== undefined) {
status.selectedIndex = statuses.indexOf (comment.status);
}
});
// Uncheck subscribe checkbox if user isn't subscribed
hashover.elementExists ('edit-subscribe-' + permalink, function (sub) {
if (comment.subscribed !== true) {
sub.checked = null;
}
});
// Get delete button
var editDelete = hashover.getElement('edit-delete-' + permalink);
// Get "Save Edit" button
var saveEdit = hashover.getElement ('edit-post-' + permalink);
// Change "Edit" link to "Cancel" link
hashover.cancelSwitcher ('edit', link, placeholder, permalink);
// Displays confirmation dialog for comment deletion
editDelete.onclick = function () {
return confirm (hashover.locale['delete-comment']);
};
// Attach click event to formatting revealer hyperlink
hashover.formattingOnclick ('edit', permalink);
// Set onclick and onsubmit event handlers
hashover.duplicateProperties (saveEdit, [ 'onclick', 'onsubmit' ], function () {
return hashover.postComment (form, this, 'edit', permalink, link.onclick);
});
// Remove loading class from edit link
hashover.classes.remove (link, 'hashover-loading');
// And execute callback if one was given
if (typeof (callback) === 'function') {
callback ();
}
}, true);
// And return false
return false;
};
// Changes Element.textContent onmouseover and reverts onmouseout (mouseoverchanger.js)
HashOverConstructor.prototype.mouseOverChanger = function (element, over, out)
{
// Reference to this object
var hashover = this;
if (over === null || out === null) {
element.onmouseover = null;
element.onmouseout = null;
return false;
}
element.onmouseover = function ()
{
this.textContent = hashover.locale[over];
};
element.onmouseout = function ()
{
this.textContent = hashover.locale[out];
};
};
// For liking comments (likecomment.js)
HashOverConstructor.prototype.likeComment = function (action, comment)
{
// Reference to this object
var hashover = this;
// Get permalink from comment
var permalink = comment.permalink;
// Get get from permalink
var file = this.permalinkFile (permalink);
// The opposite action
var opposite = (action === 'like') ? 'dislike' : 'like';
// Get like/dislike button
var actionLink = this.getElement (action + '-' + permalink);
var oppositeLink = this.getElement (opposite + '-' + permalink);
// Path to like/dislike backend script
var likePath = this.setup['http-backend'] + '/like.php';
// Set request queries
var queries = [
'url=' + encodeURIComponent (comment.url || this.instance['page-url']),
'thread=' + encodeURIComponent (comment.thread || this.instance['thread-name']),
'comment=' + encodeURIComponent (file),
'action=' + action
];
// Applies liked/disliked classes
var applyClasses = function (action, link)
{
// Choose liked/disliked locale keys
var title = (action === 'like') ? 'liked-comment' : 'disliked-comment';
var content = (action === 'like') ? 'liked' : 'disliked';
// Change class to indicate comment has been liked/disliked
hashover.classes.add (link, 'hashover-' + action + 'd');
hashover.classes.remove (link, 'hashover-' + action);
// Change title and class to indicate comment has been liked/disliked
link.title = hashover.locale[title];
};
// Removes liked/disliked classes
var removeClasses = function (action, link)
{
// Choose like/dislike locale keys
var title = (action === 'like') ? 'like-comment' : 'dislike-comment';
var content = (action === 'like') ? 'like' : 'dislike';
// Change class to indicate comment has been unliked/undisliked
hashover.classes.add (link, 'hashover-' + action);
hashover.classes.remove (link, 'hashover-' + action + 'd');
// Change title and class to indicate comment has been unliked/undisliked
link.title = hashover.locale[title];
};
// Adjusts like/dislike counts
var adjustNumbers = function (number, link, locale)
{
// Check if comment has likes
if (number > 0) {
// If so, change number of likes/dislikes
link.textContent = number;
// And set font weight bold
link.style.fontWeight = 'bold';
} else {
// If not, remove like count
link.textContent = hashover.locale[locale];
// And set font weight normal
link.style.fontWeight = '';
}
};
// When loaded update like count
this.ajax ('POST', likePath, queries, function (likeResponse) {
// If a message is returned display it to the user
if (likeResponse.message !== undefined) {
alert (likeResponse.message);
return;
}
// If an error is returned display a standard error to the user
if (likeResponse.error !== undefined) {
alert (likeResponse.error);
return;
}
// Get like and dislike JSON keys
var likesKey = (action !== 'like') ? 'dislikes' : 'likes';
var oppositeKey = (likesKey === 'likes') ? 'dislikes' : 'likes';
// Get number of likes and dislikes
var likes = likeResponse[likesKey] || 0;
var dislikes = likeResponse[oppositeKey] || 0;
// Adjust like/dislike counts
adjustNumbers (likes, actionLink, action);
// Check if button is marked as a like button
if (hashover.classes.contains (actionLink, 'hashover-' + action) === true) {
// If so, apply classes to indicate comment as been liked/disliked
applyClasses (action, actionLink);
// Check if an opposite action (dislike) link exists
if (oppositeLink !== null) {
// If so, adjust opposite action link count
adjustNumbers (dislikes, oppositeLink, opposite);
// And remove liked/disliked classes from opposite action link
removeClasses (opposite, oppositeLink);
}
} else {
// If not, remove liked/disliked class from action link
removeClasses (action, actionLink);
}
}, true);
};
// Callback to close the embedded image (openembeddedimage.js)
HashOverConstructor.prototype.closeEmbeddedImage = function (image)
{
// Reference to this object
var hashover = this;
// Set image load event handler
image.onload = function ()
{
// Reset title
this.title = hashover.locale['external-image-tip'];
// Remove loading class from wrapper
hashover.classes.remove (this.parentNode, 'hashover-loading');
// Remove open class from wrapper
hashover.classes.remove (this.parentNode, 'hashover-embedded-image-open');
// Remove load event handler
this.onload = null;
};
// Reset source
image.src = image.dataset.placeholder;
};
// Onclick callback function for embedded images (openembeddedimage.js)
HashOverConstructor.prototype.openEmbeddedImage = function (image)
{
// Reference to this object
var hashover = this;
// Check if embedded image is open
if (image.src === image.dataset.url) {
// If so, close it
this.closeEmbeddedImage (image);
// And return void
return;
}
// Set title
image.title = this.locale['loading'];
// Add loading class to wrapper
this.classes.add (image.parentNode, 'hashover-loading');
// Set image load event handler
image.onload = function ()
{
// Set title to "Click to close" locale
this.title = hashover.locale['click-to-close'];
// Remove loading class from wrapper
hashover.classes.remove (this.parentNode, 'hashover-loading');
// Add open class to wrapper
hashover.classes.add (this.parentNode, 'hashover-embedded-image-open');
// Remove load event handler
this.onload = null;
};
// Close embedded image if any error occurs
image.onerror = function () {
hashover.closeEmbeddedImage (this);
};
// Set placeholder image to embedded source
image.src = image.dataset.url;
};
// Convert URL to embed image HTML (embedimage.js)
HashOverConstructor.prototype.embedImage = function (m, link, url)
{
// Reference to this object
var hashover = this;
// Remove hash from image URL
var urlExtension = url.split ('#')[0];
// Remove queries from image URL
urlExtension = urlExtension.split ('?')[0];
// Get file extendion
urlExtension = urlExtension.split ('.');
urlExtension = urlExtension.pop ();
// Check if the image extension is an allowed type
if (this.setup['image-extensions'].indexOf (urlExtension) > -1) {
// If so, create a wrapper element for the embedded image
var embeddedImage = this.createElement ('span', {
className: 'hashover-embedded-image-wrapper'
});
// Append an image tag to the embedded image wrapper
embeddedImage.appendChild (this.createElement ('img', {
className: 'hashover-embedded-image',
src: this.setup['image-placeholder'],
title: this.locale['external-image-tip'],
alt: 'External Image',
dataset: {
placeholder: hashover.setup['image-placeholder'],
url: url
}
}));
// And return the embedded image HTML
return embeddedImage.outerHTML;
}
// Otherwise, return original link
return link;
};
// Appends HashOver theme CSS to page head (appendcss.js)
HashOverConstructor.prototype.appendCSS = function (id)
{
// Get the page head
var head = document.head || document.getElementsByTagName ('head')[0];
// Get head link tags
var links = head.getElementsByTagName ('link');
// Theme CSS regular expression
var themeRegex = new RegExp (this.setup['theme-css']);
// Get the main HashOver element
var mainElement = this.getMainElement (id);
// Do nothing if the theme StyleSheet is already in the
for (var i = 0, il = links.length; i < il; i++) {
if (themeRegex.test (links[i].href) === true) {
// Hide HashOver if the theme isn't loaded
if (links[i].loaded === false) {
mainElement.style.display = 'none';
}
// And do nothing else
return;
}
}
// Otherwise, create element for theme StyleSheet
var css = this.createElement ('link', {
rel: 'stylesheet',
href: this.setup['theme-css'],
type: 'text/css',
loaded: false
});
// Check if the browser supports CSS load events
if (css.onload !== undefined) {
// CSS load and error event handler
var onLoadError = function ()
{
// Get all HashOver class elements
var hashovers = document.getElementsByClassName ('hashover');
// Show all HashOver class elements
for (var i = 0, il = hashovers.length; i < il; i++) {
hashovers[i].style.display = '';
}
// Set CSS as loaded
css.loaded = true;
};
// Hide HashOver
mainElement.style.display = 'none';
// And and CSS load and error event listeners
css.addEventListener ('load', onLoadError, false);
css.addEventListener ('error', onLoadError, false);
}
// Append theme StyleSheet element to page
head.appendChild (css);
};
// Loads all comments and executes a callback to handle them (showmorecomments.js)
HashOverConstructor.prototype.loadAllComments = function (element, callback)
{
// Reference to this object
var hashover = this;
// Just execute callback if all comments are already loaded
if (this.instance['comments-loaded'] === true) {
return callback ();
}
// Otherwise, set request path
var requestPath = this.setup['http-backend'] + '/load-comments.php';
// Set URL queries
var queries = this.queries.concat ([
// Add current client time
'time=' + HashOverConstructor.getClientTime (),
// Add AJAX indicator
'ajax=yes'
]);
// Set class on element to indicate loading
this.classes.add (element, 'hashover-loading');
// Handle AJAX request return data
this.ajax ('POST', requestPath, queries, function (json) {
// Remove loading class from element
hashover.classes.remove (element, 'hashover-loading');
// Replace initial comments
hashover.instance.comments.primary = json.primary;
// And log backend execution time and memory usage in console
console.log (hashover.strings.sprintf (
'HashOver: backend %d ms, %s', [
json.statistics['execution-time'],
json.statistics['script-memory']
]
));
// Execute callback
callback ();
}, true);
// Set all comments as loaded
this.instance['comments-loaded'] = true;
};
// Click event handler for show more comments button (showmorecomments.js)
HashOverConstructor.prototype.showMoreComments = function (element, callback)
{
// Reference to this object
var hashover = this;
// Check if all comments are already shown
if (this.instance['showing-more'] === true) {
// If so, execute callback function
if (typeof (callback) === 'function') {
callback ();
}
// And prevent default event
return false;
}
// Check if AJAX is enabled
if (this.setup['uses-ajax'] === false) {
// If so, hide the more hyperlink; displaying the comments
this.hideMoreLink (callback);
// Set all comments as shown
this.instance['showing-more'] = true;
// And return false to prevent default event
return false;
}
// Otherwise, load all comments
this.loadAllComments (element, function () {
// Afterwards, hide show more comments hyperlink
hashover.hideMoreLink (function () {
// Afterwards, store start time
var execStart = Date.now ();
// Get primary comments
var primary = hashover.instance.comments.primary;
// Attempt to get sort method drop down menu
var sortSelect = hashover.getElement ('sort-select');
// Check if sort method drop down menu exists
if (sortSelect !== null) {
// If so, sort primary comments using select method
var sorted = hashover.sortComments (primary, sortSelect.value);
} else {
// If not, sort primary comment using default method
var sorted = hashover.sortComments (primary);
}
// Append sorted comments
var htmlTime = hashover.appendComments (sorted);
// Execute callback function
if (typeof (callback) === 'function') {
callback ();
}
// Store execution time
var execTime = Math.abs (Date.now () - execStart - htmlTime);
// And log execution time in console
console.log (hashover.strings.sprintf (
'HashOver: front-end %d ms, HTML %d ms', [ execTime, htmlTime ]
));
});
});
// Set all comments as shown
this.instance['showing-more'] = true;
// And prevent default event
return false;
};
// For showing more comments, via AJAX or removing a class (hidemorelink.js)
HashOverConstructor.prototype.hideMoreLink = function (callback)
{
// Reference to this object
var hashover = this;
// Sort section element
var sortSection = this.instance['sort-section'];
// More link element
var moreLink = this.instance['more-link'];
// Add class to hide the more hyperlink
this.classes.add (this.instance['more-link'], 'hashover-hide-more-link');
// Wait for hiding transition to end
setTimeout (function () {
// Remove the more hyperlink from page
moreLink.parentNode.removeChild (moreLink);
// Show comment count and sort options
hashover.getElement('count-wrapper').style.display = '';
// Show popular comments section
hashover.elementExists ('popular-section', function (popularSection) {
popularSection.style.display = '';
});
// Callback to remove specific class names
var classRemover = function (element, elements, i, className) {
hashover.classes.remove (element, className);
};
// Remove hidden comment class from comments
hashover.eachClass (sortSection, 'hashover-hidden', classRemover);
// Execute callback function
if (typeof (callback) === 'function') {
callback ();
}
}, 350);
};
// Creates "Show X Other Comments" button (showmorelink.js)
HashOverConstructor.prototype.showMoreLink = function ()
{
// Reference to this object
var hashover = this;
// Check whether there are more than the collapse limit
if (this.instance['total-count'] > this.setup['collapse-limit']) {
// If so, create "More Comments" hyperlink
this.instance['more-link'] = this.createElement ('a', {
className: 'hashover-more-link',
rel: 'nofollow',
href: '#',
title: this.instance['more-link-text'],
textContent: this.instance['more-link-text'],
onclick: function () {
return hashover.showMoreComments (this);
}
});
// Sort section element
var sortSection = this.instance['sort-section'];
// Sort section child elements
var comments = sortSection.children;
// Store last hidden comment for later use
this.instance['last-shown-comment'] = comments[comments.length - 1];
// Add more button link after sort div
sortSection.appendChild (this.instance['more-link']);
// And consider comments collapsed
this.instance['showing-more'] = false;
} else {
// If not, consider all comments shown
this.instance['showing-more'] = true;
}
};
// Re-appends "Show X Other Comments" button (showmorelink.js)
HashOverConstructor.prototype.reappendMoreLink = function ()
{
// Get show more comments link
var moreLink = this.instance['more-link'];
// Get showing all comment indicator
var showingMore = this.instance['showing-more'];
// Check if show more link exists and comments are still collapsed
if (moreLink !== undefined && showingMore === false) {
// If so, get sort section element
var sortSection = this.instance['sort-section'];
// Get last show comment before all comments were shown
var lastShown = this.instance['last-shown-comment'];
// And insert link after last shown comment
sortSection.insertBefore (moreLink, lastShown.nextSibling)
}
};
// Add Like/Dislike link and count to template (addratings.js)
HashOverConstructor.prototype.addRatings = function (comment, template, action, commentKey)
{
// The opposite action
var opposite = (action === 'like') ? 'dislike' : 'like';
// Check whether this comment was liked/disliked by the visitor
if (comment[action + 'd'] !== undefined) {
// If so, setup indicators that comment was liked/disliked
var className = 'hashover-' + action + 'd';
var title = this.locale[action + 'd-comment'];
} else {
// If not, setup indicators that comment can be liked/disliked
var className = 'hashover-' + action;
var title = this.locale[action + '-comment'];
}
// Check if comment has likes/dislikes
if (comment[action + 's'] !== undefined) {
// If so, set link text to number of likes/dislikes
var text = comment[action + 's'];
} else {
// If not, set link text to Like/Dislike
var text = this.locale[action];
}
// Append class to indicate dislikes are enabled
if (this.setup['allows-' + opposite + 's'] === true) {
className += ' hashover-' + opposite + 's-enabled';
}
// Add like/dislike link to HTML template
template[action + '-link'] = this.strings.parseTemplate (
this.ui[action + '-link'], {
hashover: this.prefix (),
permalink: commentKey,
class: className,
title: title,
text: text
}
);
};
// Add various events to various elements in each comment (addcontrols.js)
HashOverConstructor.prototype.addControls = function (comment)
{
// Reference to this object
var hashover = this;
// Get permalink from comment
var permalink = comment.permalink;
// Adds the same event handlers to each comment reply
function stepIntoReplies ()
{
// Check if the comment has replies
if (comment.replies !== undefined) {
// If so, add event handlers to each reply
for (var i = 0, il = comment.replies.length; i < il; i++) {
hashover.addControls (comment.replies[i]);
}
}
}
// Check if comment is a notice
if (comment.notice !== undefined) {
// If so, handle replies
stepIntoReplies ();
// And do nothing else
return false;
}
// Set onclick functions for external images
if (this.setup['allows-images'] !== false) {
// Main element
var main = this.instance['main-element'];
// Get embedded image elements
var embeds = main.getElementsByClassName ('hashover-embedded-image');
// Run through each embedded image element
for (var i = 0, il = embeds.length; i < il; i++) {
embeds[i].onclick = function () {
hashover.openEmbeddedImage (this);
};
}
}
// Get thread link of comment
this.elementExists ('thread-link-' + permalink, function (threadLink) {
// Add onClick event to thread hyperlink
threadLink.onclick = function ()
{
// Callback to execute after uncollapsing comments
var callback = function ()
{
// Afterwards, get the parent comment permlink
var parentThread = permalink.replace (hashover.rx.thread, '$1');
// Get the parent comment element
var scrollToElement = hashover.getElement (parentThread);
// Scroll to the parent comment
scrollToElement.scrollIntoView ({
behavior: 'smooth',
block: 'start',
inline: 'start'
});
};
// Check if collapsed comments are enabled
if (hashover.setup['collapses-comments'] !== false) {
// If so, show uncollapsed comments
hashover.showMoreComments (threadLink, callback);
} else {
// If not, execute callback directly
callback ();
}
return false;
};
});
// Get reply link of comment
this.elementExists ('reply-link-' + permalink, function (replyLink) {
// Add onClick event to "Reply" hyperlink
replyLink.onclick = function () {
hashover.replyToComment (comment);
return false;
};
});
// Check if the comment is editable for the user
this.elementExists ('edit-link-' + permalink, function (editLink) {
// If so, add onClick event to "Edit" hyperlinks
editLink.onclick = function () {
hashover.editComment (comment);
return false;
};
});
// Check if likes are enabled
if (this.setup['allows-likes'] !== false) {
// If so, check if the like link exists
this.elementExists ('like-' + permalink, function (likeLink) {
// Add onClick event to "Like" hyperlinks
likeLink.onclick = function () {
hashover.likeComment ('like', comment);
return false;
};
});
}
// Check if dislikes are enabled
if (this.setup['allows-dislikes'] !== false) {
// If so, check if the dislike link exists
this.elementExists ('dislike-' + permalink, function (dislikeLink) {
// Add onClick event to "Dislike" hyperlinks
dislikeLink.onclick = function () {
hashover.likeComment ('dislike', comment);
return false;
};
});
}
// Recursively execute this function on replies
stepIntoReplies ();
};
// HashOver latest comments UI initialization process (init.js)
HashOverLatest.prototype.init = function (id)
{
// Reference to this object
var hashover = this;
// Shorthand
var comments = this.instance.comments.primary;
// Initial comments HTML
var html = '';
// Get the main HashOver element
var mainElement = this.getMainElement (id);
// Append theme CSS if enabled
if (this.setup['appends-css'] !== false) {
this.appendCSS (id);
}
// Add main HashOver element to this HashOver instance
this.instance['main-element'] = mainElement;
// Parse every comment
for (var i = 0, il = comments.length; i < il; i++) {
html += this.parseComment (comments[i]);
}
// Check if we can insert HTML adjacently
if ('insertAdjacentHTML' in mainElement) {
// If so, clear main element's contents
mainElement.textContent = '';
// And insert comments adjacently
mainElement.insertAdjacentHTML ('beforeend', html);
} else {
// If not, add comments as element's inner HTML
mainElement.innerHTML = html;
}
// Add control events
for (var i = 0, il = comments.length; i < il; i++) {
this.addControls (comments[i]);
}
// Wait 100 milliseconds
setTimeout (function waitLoop () {
// Check if latest comments loading element no longer still exists
if (hashover.getElement ('loading') === null) {
// If so, get message element
var message = hashover.getElement ('message');
// Open message element if there's a message
if (message.textContent !== '') {
hashover.showMessage ();
}
} else {
// If not, wait 100 more milliseconds
setTimeout (waitLoop, 100);
}
}, 100);
};
// Instantiate after the DOM is parsed
HashOverLatest.onReady (function () {
window.hashoverLatest = new HashOverLatest ();
});
/*
HashOver Statistics
Execution Time : 9.60588 ms
Script Memory Peak : 0.56 MiB
System Memory Peak : 2 MiB
*/