mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-05-28 11:05:43 +08:00
Updated some comment elements and standardised more JS
- Updated comment routes to be simpler. - Updated comments JS to align better with updated component system. - Documented available global JS functions/services. - Removed redundant controller method. - Added window.$events helpers for validation messages and success/error. - Updated JS events system to not be class based for simplicity. - Added window.trans_plural method to handle pluralisation/replacements where you already have the translation string itself. Fixes #1836
This commit is contained in:
@ -1,16 +1,31 @@
|
||||
import {scrollAndHighlightElement} from "../services/util";
|
||||
|
||||
/**
|
||||
* @extends {Component}
|
||||
*/
|
||||
class PageComments {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.pageId = Number(elem.getAttribute('page-id'));
|
||||
setup() {
|
||||
this.elem = this.$el;
|
||||
this.pageId = Number(this.$opts.pageId);
|
||||
|
||||
// Element references
|
||||
this.container = this.$refs.commentContainer;
|
||||
this.formContainer = this.$refs.formContainer;
|
||||
this.commentCountBar = this.$refs.commentCountBar;
|
||||
this.addButtonContainer = this.$refs.addButtonContainer;
|
||||
this.replyToRow = this.$refs.replyToRow;
|
||||
|
||||
// Translations
|
||||
this.updatedText = this.$opts.updatedText;
|
||||
this.deletedText = this.$opts.deletedText;
|
||||
this.createdText = this.$opts.createdText;
|
||||
this.countText = this.$opts.countText;
|
||||
|
||||
// Internal State
|
||||
this.editingComment = null;
|
||||
this.parentId = null;
|
||||
|
||||
this.container = elem.querySelector('[comment-container]');
|
||||
this.formContainer = elem.querySelector('[comment-form-container]');
|
||||
|
||||
if (this.formContainer) {
|
||||
this.form = this.formContainer.querySelector('form');
|
||||
this.formInput = this.form.querySelector('textarea');
|
||||
@ -32,13 +47,14 @@ class PageComments {
|
||||
if (actionElem === null) return;
|
||||
event.preventDefault();
|
||||
|
||||
let action = actionElem.getAttribute('action');
|
||||
if (action === 'edit') this.editComment(actionElem.closest('[comment]'));
|
||||
const action = actionElem.getAttribute('action');
|
||||
const comment = actionElem.closest('[comment]');
|
||||
if (action === 'edit') this.editComment(comment);
|
||||
if (action === 'closeUpdateForm') this.closeUpdateForm();
|
||||
if (action === 'delete') this.deleteComment(actionElem.closest('[comment]'));
|
||||
if (action === 'delete') this.deleteComment(comment);
|
||||
if (action === 'addComment') this.showForm();
|
||||
if (action === 'hideForm') this.hideForm();
|
||||
if (action === 'reply') this.setReply(actionElem.closest('[comment]'));
|
||||
if (action === 'reply') this.setReply(comment);
|
||||
if (action === 'remove-reply-to') this.removeReplyTo();
|
||||
}
|
||||
|
||||
@ -69,14 +85,15 @@ class PageComments {
|
||||
};
|
||||
this.showLoading(form);
|
||||
let commentId = this.editingComment.getAttribute('comment');
|
||||
window.$http.put(`/ajax/comment/${commentId}`, reqData).then(resp => {
|
||||
window.$http.put(`/comment/${commentId}`, reqData).then(resp => {
|
||||
let newComment = document.createElement('div');
|
||||
newComment.innerHTML = resp.data;
|
||||
this.editingComment.innerHTML = newComment.children[0].innerHTML;
|
||||
window.$events.emit('success', window.trans('entities.comment_updated_success'));
|
||||
window.$events.success(this.updatedText);
|
||||
window.components.init(this.editingComment);
|
||||
this.closeUpdateForm();
|
||||
this.editingComment = null;
|
||||
}).catch(window.$events.showValidationErrors).then(() => {
|
||||
this.hideLoading(form);
|
||||
});
|
||||
}
|
||||
@ -84,9 +101,9 @@ class PageComments {
|
||||
deleteComment(commentElem) {
|
||||
let id = commentElem.getAttribute('comment');
|
||||
this.showLoading(commentElem.querySelector('[comment-content]'));
|
||||
window.$http.delete(`/ajax/comment/${id}`).then(resp => {
|
||||
window.$http.delete(`/comment/${id}`).then(resp => {
|
||||
commentElem.parentNode.removeChild(commentElem);
|
||||
window.$events.emit('success', window.trans('entities.comment_deleted_success'));
|
||||
window.$events.success(this.deletedText);
|
||||
this.updateCount();
|
||||
this.hideForm();
|
||||
});
|
||||
@ -101,21 +118,24 @@ class PageComments {
|
||||
parent_id: this.parentId || null,
|
||||
};
|
||||
this.showLoading(this.form);
|
||||
window.$http.post(`/ajax/page/${this.pageId}/comment`, reqData).then(resp => {
|
||||
window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => {
|
||||
let newComment = document.createElement('div');
|
||||
newComment.innerHTML = resp.data;
|
||||
let newElem = newComment.children[0];
|
||||
this.container.appendChild(newElem);
|
||||
window.components.init(newElem);
|
||||
window.$events.emit('success', window.trans('entities.comment_created_success'));
|
||||
window.$events.success(this.createdText);
|
||||
this.resetForm();
|
||||
this.updateCount();
|
||||
}).catch(err => {
|
||||
window.$events.showValidationErrors(err);
|
||||
this.hideLoading(this.form);
|
||||
});
|
||||
}
|
||||
|
||||
updateCount() {
|
||||
let count = this.container.children.length;
|
||||
this.elem.querySelector('[comments-title]').textContent = window.trans_choice('entities.comment_count', count, {count});
|
||||
this.elem.querySelector('[comments-title]').textContent = window.trans_plural(this.countText, count, {count});
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
@ -129,7 +149,7 @@ class PageComments {
|
||||
showForm() {
|
||||
this.formContainer.style.display = 'block';
|
||||
this.formContainer.parentNode.style.display = 'block';
|
||||
this.elem.querySelector('[comment-add-button-container]').style.display = 'none';
|
||||
this.addButtonContainer.style.display = 'none';
|
||||
this.formInput.focus();
|
||||
this.formInput.scrollIntoView({behavior: "smooth"});
|
||||
}
|
||||
@ -137,14 +157,12 @@ class PageComments {
|
||||
hideForm() {
|
||||
this.formContainer.style.display = 'none';
|
||||
this.formContainer.parentNode.style.display = 'none';
|
||||
const addButtonContainer = this.elem.querySelector('[comment-add-button-container]');
|
||||
if (this.getCommentCount() > 0) {
|
||||
this.elem.appendChild(addButtonContainer)
|
||||
this.elem.appendChild(this.addButtonContainer)
|
||||
} else {
|
||||
const countBar = this.elem.querySelector('[comment-count-bar]');
|
||||
countBar.appendChild(addButtonContainer);
|
||||
this.commentCountBar.appendChild(this.addButtonContainer);
|
||||
}
|
||||
addButtonContainer.style.display = 'block';
|
||||
this.addButtonContainer.style.display = 'block';
|
||||
}
|
||||
|
||||
getCommentCount() {
|
||||
@ -154,15 +172,15 @@ class PageComments {
|
||||
setReply(commentElem) {
|
||||
this.showForm();
|
||||
this.parentId = Number(commentElem.getAttribute('local-id'));
|
||||
this.elem.querySelector('[comment-form-reply-to]').style.display = 'block';
|
||||
let replyLink = this.elem.querySelector('[comment-form-reply-to] a');
|
||||
this.replyToRow.style.display = 'block';
|
||||
const replyLink = this.replyToRow.querySelector('a');
|
||||
replyLink.textContent = `#${this.parentId}`;
|
||||
replyLink.href = `#comment${this.parentId}`;
|
||||
}
|
||||
|
||||
removeReplyTo() {
|
||||
this.parentId = null;
|
||||
this.elem.querySelector('[comment-form-reply-to]').style.display = 'none';
|
||||
this.replyToRow.style.display = 'none';
|
||||
}
|
||||
|
||||
showLoading(formElem) {
|
||||
|
@ -7,11 +7,10 @@ window.baseUrl = function(path) {
|
||||
};
|
||||
|
||||
// Set events and http services on window
|
||||
import Events from "./services/events"
|
||||
import events from "./services/events"
|
||||
import httpInstance from "./services/http"
|
||||
const eventManager = new Events();
|
||||
window.$http = httpInstance;
|
||||
window.$events = eventManager;
|
||||
window.$events = events;
|
||||
|
||||
// Translation setup
|
||||
// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
|
||||
@ -19,6 +18,7 @@ import Translations from "./services/translations"
|
||||
const translator = new Translations();
|
||||
window.trans = translator.get.bind(translator);
|
||||
window.trans_choice = translator.getPlural.bind(translator);
|
||||
window.trans_plural = translator.parsePlural.bind(translator);
|
||||
|
||||
// Load Components
|
||||
import components from "./components"
|
||||
|
@ -1,55 +1,66 @@
|
||||
const listeners = {};
|
||||
const stack = [];
|
||||
|
||||
/**
|
||||
* Simple global events manager
|
||||
* Emit a custom event for any handlers to pick-up.
|
||||
* @param {String} eventName
|
||||
* @param {*} eventData
|
||||
*/
|
||||
class Events {
|
||||
constructor() {
|
||||
this.listeners = {};
|
||||
this.stack = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a custom event for any handlers to pick-up.
|
||||
* @param {String} eventName
|
||||
* @param {*} eventData
|
||||
* @returns {Events}
|
||||
*/
|
||||
emit(eventName, eventData) {
|
||||
this.stack.push({name: eventName, data: eventData});
|
||||
if (typeof this.listeners[eventName] === 'undefined') return this;
|
||||
let eventsToStart = this.listeners[eventName];
|
||||
for (let i = 0; i < eventsToStart.length; i++) {
|
||||
let event = eventsToStart[i];
|
||||
event(eventData);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to a custom event and run the given callback when that event occurs.
|
||||
* @param {String} eventName
|
||||
* @param {Function} callback
|
||||
* @returns {Events}
|
||||
*/
|
||||
listen(eventName, callback) {
|
||||
if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
|
||||
this.listeners[eventName].push(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event for public use.
|
||||
* Sends the event via the native DOM event handling system.
|
||||
* @param {Element} targetElement
|
||||
* @param {String} eventName
|
||||
* @param {Object} eventData
|
||||
*/
|
||||
emitPublic(targetElement, eventName, eventData) {
|
||||
const event = new CustomEvent(eventName, {
|
||||
detail: eventData,
|
||||
bubbles: true
|
||||
});
|
||||
targetElement.dispatchEvent(event);
|
||||
function emit(eventName, eventData) {
|
||||
stack.push({name: eventName, data: eventData});
|
||||
if (typeof listeners[eventName] === 'undefined') return this;
|
||||
let eventsToStart = listeners[eventName];
|
||||
for (let i = 0; i < eventsToStart.length; i++) {
|
||||
let event = eventsToStart[i];
|
||||
event(eventData);
|
||||
}
|
||||
}
|
||||
|
||||
export default Events;
|
||||
/**
|
||||
* Listen to a custom event and run the given callback when that event occurs.
|
||||
* @param {String} eventName
|
||||
* @param {Function} callback
|
||||
* @returns {Events}
|
||||
*/
|
||||
function listen(eventName, callback) {
|
||||
if (typeof listeners[eventName] === 'undefined') listeners[eventName] = [];
|
||||
listeners[eventName].push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event for public use.
|
||||
* Sends the event via the native DOM event handling system.
|
||||
* @param {Element} targetElement
|
||||
* @param {String} eventName
|
||||
* @param {Object} eventData
|
||||
*/
|
||||
function emitPublic(targetElement, eventName, eventData) {
|
||||
const event = new CustomEvent(eventName, {
|
||||
detail: eventData,
|
||||
bubbles: true
|
||||
});
|
||||
targetElement.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify of a http error.
|
||||
* Check for standard scenarios such as validation errors and
|
||||
* formats an error notification accordingly.
|
||||
* @param {Error} error
|
||||
*/
|
||||
function showValidationErrors(error) {
|
||||
if (!error.status) return;
|
||||
if (error.status === 422 && error.data) {
|
||||
const message = Object.values(error.data).flat().join('\n');
|
||||
emit('error', message);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
emit,
|
||||
emitPublic,
|
||||
listen,
|
||||
success: (msg) => emit('success', msg),
|
||||
error: (msg) => emit('error', msg),
|
||||
showValidationErrors,
|
||||
}
|
@ -69,7 +69,10 @@ async function dataRequest(method, url, data = null) {
|
||||
|
||||
// Send data as JSON if a plain object
|
||||
if (typeof data === 'object' && !(data instanceof FormData)) {
|
||||
options.headers = {'Content-Type': 'application/json'};
|
||||
options.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
};
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,19 @@ class Translator {
|
||||
*/
|
||||
getPlural(key, count, replacements) {
|
||||
const text = this.getTransText(key);
|
||||
const splitText = text.split('|');
|
||||
return this.parsePlural(text, count, replacements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given translation and find the correct plural option
|
||||
* to use. Similar format at laravel's 'trans_choice' helper.
|
||||
* @param {String} translation
|
||||
* @param {Number} count
|
||||
* @param {Object} replacements
|
||||
* @returns {String}
|
||||
*/
|
||||
parsePlural(translation, count, replacements) {
|
||||
const splitText = translation.split('|');
|
||||
const exactCountRegex = /^{([0-9]+)}/;
|
||||
const rangeRegex = /^\[([0-9]+),([0-9*]+)]/;
|
||||
let result = null;
|
||||
|
Reference in New Issue
Block a user