docbrown/app/assets/javascripts/initializers/initializeCommentsPage.js.erb
Nick Karnik 2136b105a3 Implement ability to style text in comments using hotkeys (#959)
* Resolves #891
    * This is applied to root level comments as well as nested replies.
    * We are handling latest browsers and special cases for FF, IE8/9/10
    * All newer browser except FF support undo/redo functionality in the exact reverse order of text insertion
    * Refactored some code
    * Support for Bold & Italic text
    * Support for Text Links
    * In a subsequent change, we can consider adding hotkey for image URL
        * Didn't add this feature at the moment since there's no universally accepted key-combination for image URL's that I know of
    * IE 8-11 is not supported because it is a bit convoluted and I don't have a way to test it reliably
        * We can revisit this if it is really needed and find a jQuery plugin to handle it
2018-10-19 15:39:22 -04:00

474 lines
18 KiB
Text

function initializeCommentsPage() {
if (document.getElementById('comments-container')) {
toggleCodeOfConduct();
var commentableId = document.getElementById('comments-container').dataset.commentableId;
var commentableType = document.getElementById('comments-container').dataset.commentableType;
var ajaxReq;
if (window.XMLHttpRequest) {
ajaxReq = new XMLHttpRequest();
} else {
ajaxReq = new ActiveXObject('Microsoft.XMLHTTP');
}
ajaxReq.onreadystatechange = function () {
if (ajaxReq.readyState === XMLHttpRequest.DONE) {
var responseObj = JSON.parse(ajaxReq.response);
var reactions = responseObj.reactions;
var allNodes = document.getElementsByClassName('single-comment-node');
var positiveReactionCounts = responseObj.positive_reaction_counts;
for (var i = 0; i < reactions.length; i++) {
var buttForComment = document.getElementById('button-for-comment-' + reactions[i].reactable_id);
if (buttForComment) {
buttForComment.classList.add('reacted');
}
}
for (var i = 0; i < positiveReactionCounts.length; i++) {
var buttForComment = document.getElementById('button-for-comment-' + positiveReactionCounts[i].id);
if (buttForComment) {
if (positiveReactionCounts[i].count > 0) {
if (!document.getElementById('reactions-count-' + positiveReactionCounts[i].id)) {
buttForComment.innerHTML = buttForComment.innerHTML + "<span class='reactions-count' id='reactions-count-" + positiveReactionCounts[i].id + "'>" + positiveReactionCounts[i].count + '</span>';
} else {
document.getElementById('reactions-count-' + positiveReactionCounts[i].id).innerHTML = positiveReactionCounts[i].count;
}
}
}
}
for (var i = 0; i < allNodes.length; i++) {
if (allNodes[i].dataset.commentAuthorId == responseObj.current_user.id) {
allNodes[i].dataset.currentUserComment = "true";
var userActionsEl = allNodes[i].children[0].children[2].children[0];
var buttEl = document.getElementById('button-for-comment-' + allNodes[i].dataset.commentId);
if (userActionsEl && buttEl) {
userActionsEl.className = 'current-user-actions';
userActionsEl.style.display = 'inline-block';
document.getElementById('button-for-comment-' + allNodes[i].dataset.commentId).classList.add('reacted');
}
}
}
}
};
var timeString = (Date.now()).toString();
ajaxReq.open("GET", "/reactions?commentable_id="+commentableId+"&commentable_type="+commentableType, true);
ajaxReq.send();
var butts = document.getElementsByClassName('reaction-button');
for (var i = 0; i < butts.length; i++) {
var butt = butts[i];
butt.onclick = function (event) {
event.preventDefault();
var thisButt = this;
var userStatus = document.getElementsByTagName('body')[0].getAttribute('data-user-status');
if (userStatus === 'logged-out') {
showModal('react-to-comment');
return;
}
thisButt.classList.add('reacted');
function successCb(response) {
var reactionCountSpan = thisButt.children[2];
if (response.result === 'create') {
thisButt.classList.add('reacted');
if (reactionCountSpan) {
reactionCountSpan.innerHTML = parseInt(reactionCountSpan.innerHTML) + 1;
}
} else {
thisButt.classList.remove('reacted');
if (reactionCountSpan) {
reactionCountSpan.innerHTML = parseInt(reactionCountSpan.innerHTML) - 1;
}
}
}
var formData = new FormData();
formData.append('reactable_type', 'Comment');
formData.append('reactable_id', thisButt.dataset.commentId);
getCsrfToken()
.then(sendFetch('reaction-creation', formData))
.then(function (response) {
if (response.status === 200) {
response.json().then(successCb);
}
});
};
}
var replyButts = document.getElementsByClassName('toggle-reply-form');
for (var i = 0; i < replyButts.length; i++) {
var butt = replyButts[i];
butt.onclick = function(event) {
event.preventDefault();
if (event.target.classList.contains("thread-indication")){
return false;
} else {
var userStatus = document.getElementsByTagName('body')[0].getAttribute('data-user-status');
if (userStatus == 'logged-out') {
showModal('reply-to-comment');
return;
}
var actionNode = event.target.parentNode;
var parentId = actionNode.dataset.commentId;
document.getElementById("button-for-comment-"+actionNode.dataset.commentId).style.zIndex = "5";
var waitingOnCSRF = setInterval(function () {
var metaTag = document.querySelector("meta[name='csrf-token']");
if (metaTag) {
clearInterval(waitingOnCSRF);
actionNode.innerHTML = buildCommentFormHTML(commentableId, commentableType, parentId);
setTimeout(function () {
actionNode.getElementsByTagName('textarea')[0].focus();
}, 30);
}
}, 1);
// document.getElementById('textarea-for-'+parentId).focus();
};
return false;
}
}
var editButts = document.getElementsByClassName('edit-butt');
for (var i = 0; i < editButts.length; i++) {
var butt = editButts[i];
butt.onclick = function () {
// event.preventDefault();
// alert("edit clicked!")
};
}
if (document.getElementById('new_comment')) {
document.getElementById('new_comment').onsubmit = handleCommentSubmit;
}
}
}
function toggleCodeOfConduct() {
var user = userData();
if (!user){
return;
}
var codeOfConduct = user.checked_code_of_conduct
var commentCount = user.number_of_comments
var checkboxWrapper = document.getElementById('toggle-code-of-conduct-checkbox')
if (checkboxWrapper && !codeOfConduct && commentCount < 1) {
checkboxWrapper.innerHTML = '<input type="checkbox" name="checked_code_of_conduct" class="checkbox" required/>\
<label for="checked_code_of_conduct"/>I\'ve read the <a href="https://dev.to/code-of-conduct">code of conduct</a></label>'
}
}
function replaceActionButts(el) {
var loggedInActionButts = "";
var wrapper = el.getElementsByClassName("actions")[0]
if ( el.dataset.currentUserComment == "true"){
loggedInActionButts = '<a data-no-instant href="' + el.parentNode.parentNode.dataset.path + '/delete_confirm" class="edit-butt">DELETE</a>\
<a href="' + el.parentNode.parentNode.dataset.path + '/edit">EDIT</a>'
}
wrapper.innerHTML = '<span class="current-user-actions">'+loggedInActionButts+'</span><a href="#" class="toggle-reply-form">REPLY</a>';
}
function handleCommentSubmit(event) {
event.preventDefault();
var form = event.target;
form.classList.add('submitting');
var parentComment = document.getElementById("comment-node-"+event.target.dataset.commentId);
var body = JSON.stringify({
comment: {
body_markdown: form.getElementsByTagName("textarea")[0].value,
commentable_id: form.querySelector("#comment_commentable_id").value,
commentable_type: form.querySelector("#comment_commentable_type").value,
parent_id: form.querySelector("#comment_parent_id") ? form.querySelector("#comment_parent_id").value : null,
}
});
getCsrfToken()
.then(sendFetch('comment-creation', body))
.then(function (response) {
if (response.status === 200) {
response.json().then(function(newComment) {
var newNode = document.createElement('div');
if (newComment.status == "comment already exists") {
return false;
}
if (newComment.status == "errors") {
alert("There was a problem submitting this comment.")
return false;
}
newNode.innerHTML = buildCommentHTML(newComment);
var docBody = document.getElementsByTagName('body')[0]
var userData = JSON.parse(docBody.getAttribute('data-user'))
userData.checked_code_of_conduct = true;
docBody.dataset.user = JSON.stringify(userData);
var checkbox = form.getElementsByClassName('code-of-conduct')[0]
if (checkbox) {
checkbox.innerHTML = ""
}
var mainCommentsForm = document.getElementById("new_comment");
if (parentComment) {
replaceActionButts(parentComment);
if (newComment.depth > 2) {
parentComment.getElementsByClassName("toggle-reply-form")[0].innerHTML = ""
}
var actionsNode = parentComment.getElementsByClassName("inner-comment")[0];
actionsNode.parentNode.insertBefore(newNode, actionsNode.nextSibling);
}
else if (mainCommentsForm) {
var mainCommentsForm = document.getElementById("new_comment");
var textArea = document.getElementById("text-area");
mainCommentsForm.classList.remove("submitting");
textArea.value = "";
textArea.classList = "";
var preview = document.getElementById("preview-div");
preview.classList.add("preview-toggle");
preview.innerHTML = "";
var container = document.getElementById("comment-trees-container");
container.insertBefore(newNode, container.firstChild);
}
else if (document.getElementById("notifications-container")) {
var newDiv = document.createElement("span")
newDiv.innerHTML = '<div class="reply-sent-notice">Reply sent — <a href="'+newComment.url+'">Check it out</a></div>'
form.replaceWith(newDiv);
}
else {
window.location.replace(newComment.url)
}
initializeCommentsPage();
initializeCommentDropdown();
})
}
return false;
});
return false;
}
function handleFocus(event) {
var userStatus = document.getElementsByTagName('body')[0].getAttribute('data-user-status');
var area = event.target;
if (userStatus == 'logged-out') {
event.preventDefault();
showModal('reply-to-comment');
area.blur();
setTimeout(function () {
area.blur();
showModal('reply-to-comment');
}, 100);
} else {
handleSizeChange(event);
}
}
function handleBlur(event) {
setTimeout(function () {
var el = document.getElementById('text-area');
if (el.value.length == 0) {
el.className = '';
}
}, 100);
}
function handleKeyUp(event) {
handleSizeChange(event);
}
// Handler for when Ctrl+Enter OR Command+Enter is pressed
function handleSubmit(event) {
// Get user details and extract code of conduct & comment count
var user = userData();
if (!user) {
return;
}
var commentCount = user.number_of_comments;
// var codeOfConduct = user.checked_code_of_conduct;
// Code of conduct is false the first time even if the user has accepted it
// so we are not using it in the check below
// It also seems unnecessary if comment count >= 1 indicates that
// it was previously accepted
// Ensure that user has commented at least once
// and we have a non-empty comment to post
if (commentCount >= 1 &&
event.target.value.trim() !== '') {
// get a reference to the submit button for the text area (first level comment vs. reply comment)
var parentFormId = event.target.parentElement.parentElement.id || event.target.parentElement.id;
var submitButton = document.querySelector('#' + parentFormId + ' input[type="submit"].comment-action-button');
submitButton.click();
}
}
// Handler when Ctrl+B/I OR Command+B/I is pressed
function handleBoldAndItalic(event) {
var textArea = event.target;
var selection = textArea.value.substring(textArea.selectionStart, textArea.selectionEnd);
var selectionStart = textArea.selectionStart;
var surroundingStr = event.keyCode === KEY_CODE_B ? "**" : "_";
replaceSelectedText(textArea, `${surroundingStr}${selection}${surroundingStr}`);
var selectionStartWithOffset = selectionStart + surroundingStr.length;
textArea.setSelectionRange(selectionStartWithOffset, selectionStartWithOffset + selection.length);
}
// Handler when Ctrl+K OR Command+K is pressed
function handleLink(event) {
var textArea = event.target;
var selection = textArea.value.substring(textArea.selectionStart, textArea.selectionEnd);
var selectionStart = textArea.selectionStart;
replaceSelectedText(textArea, `[${selection}](url)`);
// start position + length of selection + [](
var startOffset = selectionStart + selection.length + 3;
// start offset + 'url'.length
var endOffset = startOffset + 3;
textArea.setSelectionRange(startOffset, endOffset);
}
function replaceSelectedText(textArea, text) {
// Chrome and other modern browsers (except FF and IE 8,9,10,11)
if(document.execCommand('insertText', false, text)) {
}
// Firefox (non-standard method)
else if(typeof textArea.setRangeText === "function") {
textArea.setRangeText(text);
}
/*
Disabling IE 8-11 for now as there's no easy way to verify all this
We can revisit this if it's really needed and find a jQuery plugin to use
// IE 8-10
else if(document.selection) {
const ieRange = document.selection.createRange();
ieRange.text = text;
// Move cursor after the inserted text
ieRange.collapse(false); // to the end
ieRange.select();
}
else {
// Does not support IE 11 yet
}
*/
}
var KEY_CODE_B = 66;
var KEY_CODE_I = 73;
var KEY_CODE_K = 75;
var ENTER_KEY_CODE = 13;
function handleKeyDown(event) {
if (event.ctrlKey || event.metaKey) {
switch (event.keyCode) {
case KEY_CODE_B:
event.preventDefault();
handleBoldAndItalic(event);
break;
case KEY_CODE_I:
event.preventDefault();
handleBoldAndItalic(event);
break;
case KEY_CODE_K:
event.preventDefault();
handleLink(event);
break;
case ENTER_KEY_CODE:
event.preventDefault();
handleSubmit(event);
break;
default:
break;
}
}
}
function handleSizeChange(event) {
var lines = event.target.value.split(/\r*\n/);
var lineCount = lines.length;
if (lineCount > 11) {
document.getElementById('text-area').className = 'embiggened-max';
} else if (lineCount > 6) {
document.getElementById('text-area').className = 'embiggened-more';
} else {
document.getElementById('text-area').className = 'embiggened';
}
}
function validateField(event) {
if (document.getElementById('text-area')) {
var commentField = document.getElementById('text-area').value;
if (commentField == '') {
event.preventDefault();
}
}
}
function generateUploadFormdata(image) {
var token = document.querySelector("meta[name='csrf-token']").content;
var formData = new FormData();
formData.append('authenticity_token', token);
formData.append('image', image[0]);
return formData;
}
function handleImageUpload(event, randomIdNumber) {
var commentableId = document.getElementById('comments-container').dataset.commentableId;
event.preventDefault();
document.getElementById('image-upload-'+ randomIdNumber).click();
document.getElementById('image-upload-'+ randomIdNumber).onchange = function (e) {
var image = document.getElementById('image-upload-'+ randomIdNumber).files;
if (image.length > 0) {
document.getElementById("image-upload-file-label-" + randomIdNumber).style.color = '#888888';
document.getElementById("image-upload-file-label-" + randomIdNumber).innerHTML = "Uploading...";
document.getElementById("image-upload-submit-" + randomIdNumber).value = "uploading";
setTimeout(function(){
document.getElementById("image-upload-submit-" + randomIdNumber).click(function(){});
},50)
}
}
document.getElementById("image-upload-submit-" + randomIdNumber).onclick = function (e) {
e.preventDefault();
var image = document.getElementById('image-upload-' + randomIdNumber).files;
if (image.length > 0) {
getCsrfToken()
.then(sendFetch("image-upload", generateUploadFormdata(image)))
.then(function (response) {
if (response.status === 200) {
response.json().then(
function uploadImageCb(json) {
var address = document.getElementById("uploaded-image-" + randomIdNumber);
var button = document.getElementById("image-upload-button-" + randomIdNumber);
var messageContainer = document.getElementById("image-upload-file-label-" + randomIdNumber)
// button.style.display = "none";
messageContainer.style.display = "none";
address.value = json.link;
address.classList.add("showing");
address.select();
var uploadedMessage = 'Uploaded! Paste into editor';
messageContainer.innerHTML = uploadedMessage;
messageContainer.style.color = '#00c673';
messageContainer.style.position = "relative"
messageContainer.style.top = "5px";
}
);
} else {
document.getElementById("image-upload-file-label-" + randomIdNumber).innerHTML = "Invalid file!";
document.getElementById("image-upload-file-label-" + randomIdNumber).style.color = '#e05252';
document.getElementById("image-upload-submit-" + randomIdNumber).style.display = 'none';
}
})
.catch(function (err) {
// there's currently no error handling
})
}
}
}