add front-comments
This commit is contained in:
parent
c3ea78cab5
commit
c9f4af7e58
53 changed files with 2921 additions and 1 deletions
89
site/plugins/front-comments/src/front/classes/AddBtn.js
Normal file
89
site/plugins/front-comments/src/front/classes/AddBtn.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import CommentPanel from "./edition-panels/CommentPanel.js";
|
||||
import Store from "../store.js";
|
||||
|
||||
/**
|
||||
* Class representing the Add Button.
|
||||
*/
|
||||
class AddButton {
|
||||
/**
|
||||
* Create an AddButton.
|
||||
* @param {string} selector - The CSS selector for the button.
|
||||
*/
|
||||
constructor(position) {
|
||||
this._button = this._createButton(position);
|
||||
this._initEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new button element with the specified HTML content.
|
||||
* @return {HTMLButtonElement} The created button element.
|
||||
* @private
|
||||
*/
|
||||
_createButton(position) {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = `<button class="fc__btn fc__btn-add fc__btn-add--${position} fc__btn-add--free" title="New comment">
|
||||
<img class="fc__icon fc__plus" src="${Store.filesPath}/icons/chat-box-plus.svg">
|
||||
</button>`;
|
||||
const button = div.firstChild;
|
||||
document.body.appendChild(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the event listeners for the button.
|
||||
* @private
|
||||
*/
|
||||
_initEventListeners() {
|
||||
this._button.addEventListener("click", this._handleClick.bind(this));
|
||||
window.addEventListener("mousemove", this._handleMouseMove.bind(this));
|
||||
window.addEventListener("keydown", this._handleKeyDown.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the click event on the button.
|
||||
* @private
|
||||
*/
|
||||
_handleClick(event) {
|
||||
if (this._button.classList.contains("fc__btn-add--move")) {
|
||||
const commentPanel = new CommentPanel();
|
||||
commentPanel.create();
|
||||
this._resetPosition();
|
||||
}
|
||||
this._button.classList.toggle("fc__btn-add--move");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the mousemove event on the window.
|
||||
* @private
|
||||
*/
|
||||
_handleMouseMove(event) {
|
||||
if (this._button.classList.contains("fc__btn-add--move")) {
|
||||
const iconSize = 48;
|
||||
this._button.style.left = event.clientX - iconSize / 2 + "px";
|
||||
this._button.style.top =
|
||||
event.clientY + window.scrollY - iconSize / 2 + "px";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the keydown event on the window.
|
||||
* @private
|
||||
*/
|
||||
_handleKeyDown(event) {
|
||||
if (event.key === "Escape") {
|
||||
this._button.classList.remove("fc__btn-add--move");
|
||||
this._resetPosition();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the position of the button.
|
||||
* @private
|
||||
*/
|
||||
_resetPosition() {
|
||||
this._button.style.left = "";
|
||||
this._button.style.top = "";
|
||||
}
|
||||
}
|
||||
|
||||
export default AddButton;
|
||||
224
site/plugins/front-comments/src/front/classes/Suggestion.js
Normal file
224
site/plugins/front-comments/src/front/classes/Suggestion.js
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
import dayjs from "dayjs";
|
||||
import Store from "../store.js";
|
||||
|
||||
class Suggestion {
|
||||
constructor(
|
||||
author,
|
||||
context,
|
||||
target,
|
||||
suggestion,
|
||||
fieldName,
|
||||
id = Date.now(),
|
||||
date = dayjs().format("DD/MM/YY"),
|
||||
time = dayjs().format("HH:mm")
|
||||
) {
|
||||
this.author = author;
|
||||
this.context = context;
|
||||
this.target = target;
|
||||
this.suggestion = suggestion;
|
||||
this.fieldName = fieldName;
|
||||
this.id = id;
|
||||
this.date = date;
|
||||
this.time = time;
|
||||
this.node;
|
||||
this.highlightTargetInField();
|
||||
}
|
||||
|
||||
highlightTargetInField() {
|
||||
const highlight = document.querySelector(".fc__suggestion--edit");
|
||||
if (highlight) {
|
||||
this.node = highlight;
|
||||
highlight.classList.remove("fc__suggestion--edit");
|
||||
highlight.classList.add("fc__suggestion");
|
||||
} else {
|
||||
const { target, fieldName } = this;
|
||||
const fieldElement = this.getFieldElement(fieldName);
|
||||
const textNodes = this.getTextNodes(fieldElement);
|
||||
this.highlightTarget(textNodes, target);
|
||||
}
|
||||
|
||||
if (this.node) {
|
||||
this.node.addEventListener("mouseenter", () => {
|
||||
this._bubble = this.createBubble();
|
||||
this.node.appendChild(this._bubble);
|
||||
});
|
||||
this.node.addEventListener("mouseleave", () => {
|
||||
this.node.removeChild(this._bubble);
|
||||
});
|
||||
} else {
|
||||
this.remove();
|
||||
}
|
||||
}
|
||||
|
||||
getFieldElement(fieldName) {
|
||||
return document.querySelector(`[data-field-name="${fieldName}"]`);
|
||||
}
|
||||
|
||||
getTextNodes(fieldElement) {
|
||||
const textNodes = [];
|
||||
const childNodes = Array.from(fieldElement.childNodes);
|
||||
|
||||
childNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== "") {
|
||||
textNodes.push(node);
|
||||
} else if (node.hasChildNodes()) {
|
||||
textNodes.push(...this.getChildTextNodes(node));
|
||||
}
|
||||
});
|
||||
|
||||
return textNodes;
|
||||
}
|
||||
|
||||
highlightTarget(textNodes, target) {
|
||||
textNodes.forEach((textNode) => {
|
||||
const targetIndex = textNode.textContent.indexOf(target);
|
||||
|
||||
if (
|
||||
targetIndex >= 0 &&
|
||||
this.matchContext(textNode, target, targetIndex)
|
||||
) {
|
||||
this.node = this.insertHighlightedTarget(textNode, target, targetIndex);
|
||||
} else {
|
||||
// this.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
matchContext(textNode, target, targetIndex) {
|
||||
const beforeMatch = textNode.textContent.slice(
|
||||
Math.max(0, targetIndex - 30),
|
||||
targetIndex
|
||||
);
|
||||
const afterMatch = textNode.textContent.slice(
|
||||
targetIndex + target.length,
|
||||
targetIndex + target.length + 30
|
||||
);
|
||||
|
||||
return (
|
||||
beforeMatch === this.context.before && afterMatch === this.context.after
|
||||
);
|
||||
}
|
||||
|
||||
insertHighlightedTarget(textNode, target, targetIndex) {
|
||||
const highlightedTarget = this.createHighlightedTarget(target);
|
||||
const beforeTarget = this.createTextNode(
|
||||
textNode.textContent.slice(0, targetIndex)
|
||||
);
|
||||
const afterTarget = this.createTextNode(
|
||||
textNode.textContent.slice(targetIndex + target.length)
|
||||
);
|
||||
|
||||
textNode.parentNode.insertBefore(beforeTarget, textNode);
|
||||
textNode.parentNode.insertBefore(highlightedTarget, textNode);
|
||||
textNode.parentNode.insertBefore(afterTarget, textNode);
|
||||
textNode.remove();
|
||||
return highlightedTarget;
|
||||
}
|
||||
|
||||
createHighlightedTarget(target) {
|
||||
const highlightedTarget = document.createElement("span");
|
||||
highlightedTarget.classList.add("fc__suggestion");
|
||||
highlightedTarget.textContent = target;
|
||||
return highlightedTarget;
|
||||
}
|
||||
|
||||
createTextNode(text) {
|
||||
return document.createTextNode(text);
|
||||
}
|
||||
|
||||
getChildTextNodes(node) {
|
||||
const textNodes = [];
|
||||
const childNodes = Array.from(node.childNodes);
|
||||
|
||||
childNodes.forEach((childNode) => {
|
||||
if (
|
||||
childNode.nodeType === Node.TEXT_NODE &&
|
||||
childNode.textContent.trim() !== ""
|
||||
) {
|
||||
textNodes.push(childNode);
|
||||
} else if (childNode.hasChildNodes()) {
|
||||
textNodes.push(...this.getChildTextNodes(childNode));
|
||||
}
|
||||
});
|
||||
|
||||
return textNodes;
|
||||
}
|
||||
|
||||
createBubble() {
|
||||
const bubble = document.createElement("div");
|
||||
bubble.className = "fc__suggestion-bubble";
|
||||
bubble.innerHTML = `<p>${this.suggestion}</p>`;
|
||||
|
||||
bubble.appendChild(this.createBtns());
|
||||
return bubble;
|
||||
}
|
||||
|
||||
createBtns() {
|
||||
const btns = document.createElement("div");
|
||||
btns.classList.add("fc__suggestion-bubble__btns");
|
||||
const decline = document.createElement("button");
|
||||
decline.classList.add("fc__btn");
|
||||
decline.innerHTML = `<img class="fc__icon" src="${Store.filePath}/front-comments/icons/decline.svg" />`;
|
||||
|
||||
const accept = document.createElement("button");
|
||||
accept.classList.add("fc__btn");
|
||||
accept.innerHTML = `<img class="fc__icon" src="${Store.filePath}/front-comments/icons/accept.svg" />`;
|
||||
|
||||
decline.addEventListener("click", () => {
|
||||
this.remove();
|
||||
});
|
||||
|
||||
accept.addEventListener("click", () => {
|
||||
this.push();
|
||||
});
|
||||
|
||||
btns.appendChild(decline);
|
||||
btns.appendChild(accept);
|
||||
return btns;
|
||||
}
|
||||
|
||||
remove() {
|
||||
Store.suggestions = Store.suggestions.filter(
|
||||
(suggestion) => suggestion.id != this.id
|
||||
);
|
||||
if (this.node !== undefined) {
|
||||
this.node.parentNode.removeChild(this.node);
|
||||
}
|
||||
Store.save("suggestions");
|
||||
}
|
||||
|
||||
push() {
|
||||
const field = this.getFieldElement(this.fieldName);
|
||||
|
||||
const data = {};
|
||||
data[this.fieldName] = field.dataset.content.replace(
|
||||
this.target,
|
||||
this.suggestion
|
||||
);
|
||||
|
||||
const init = {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"X-CSRF": Store.csrf,
|
||||
},
|
||||
body: JSON.stringify(data).replaceAll("_", ""),
|
||||
};
|
||||
fetch("/api/pages/" + Store.page, init)
|
||||
.then((response) => response.json())
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
console.log("Page comments successfully updated.");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
Store.suggestions = Store.suggestions.filter(
|
||||
(suggestion) => suggestion.id != this.id
|
||||
);
|
||||
Store.save("suggestions");
|
||||
this.node.outerHTML = this.suggestion;
|
||||
}
|
||||
}
|
||||
|
||||
export default Suggestion;
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
import Store from "../../store.js";
|
||||
import dayjs from "dayjs";
|
||||
import Helpers from "../../composables/helpers.js";
|
||||
|
||||
/**
|
||||
* Represents a comment on a webpage.
|
||||
*/
|
||||
class Comment {
|
||||
/**
|
||||
* Create a Comment.
|
||||
* @param {Object} position - The positions where the comment should be displayed.
|
||||
*/
|
||||
constructor(
|
||||
position,
|
||||
author,
|
||||
message,
|
||||
id,
|
||||
date,
|
||||
time,
|
||||
windowWidth,
|
||||
userAgent,
|
||||
team
|
||||
) {
|
||||
this.position = position;
|
||||
this.author = author;
|
||||
this.message = message;
|
||||
this.id = id;
|
||||
this.date = date;
|
||||
this.time = time;
|
||||
this.windowWidth = windowWidth;
|
||||
this.userAgent = userAgent;
|
||||
this.team = team;
|
||||
this.node;
|
||||
}
|
||||
|
||||
toBubble() {
|
||||
const bubble = this._injectNode(`<button
|
||||
id="${this.id}"
|
||||
class="fc__bubble fc__btn"
|
||||
style="left: ${this.position.left}; top: ${this.position.top};">
|
||||
${this.author.slice(0, 1).toUpperCase()}
|
||||
</button>`);
|
||||
|
||||
if (this.team) {
|
||||
bubble.classList.add("fc__team-icon");
|
||||
bubble.classList.add(`fc__team-icon--${this.team}`);
|
||||
}
|
||||
|
||||
bubble.addEventListener("mouseenter", () => {
|
||||
setTimeout(() => {
|
||||
this.expand();
|
||||
}, 10);
|
||||
});
|
||||
|
||||
this.node = bubble;
|
||||
return bubble;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the comment message.
|
||||
*/
|
||||
expand() {
|
||||
const windowWidthInfoElement = this.windowWidth
|
||||
? `<li class="fc__context-item">
|
||||
<b>Largeur fenêtre</b> : ${this.windowWidth}px <button class="fc__open-window" title="Ouvrir une fenêtre de cette largeur">voir</button>
|
||||
</li>`
|
||||
: "";
|
||||
const userAgentInfoElement = this.userAgent
|
||||
? `<li class="fc__context-item">
|
||||
<b>Agent utilisateur</b> : <br>
|
||||
${this.userAgent}
|
||||
</li>`
|
||||
: "";
|
||||
const contextInfoElement =
|
||||
this.userAgent || this.windowWidth
|
||||
? `
|
||||
<div class="fc__context">
|
||||
<input id="collapsible" class="fc__toggle" type="checkbox">
|
||||
<label for="collapsible" class="fc__label-toggle" title="État du navigateur au moment de l'ajout du commentaire">Contexte</label>
|
||||
<div class="fc__collapsible-content">
|
||||
<ul class="fc__content-inner">
|
||||
${windowWidthInfoElement}
|
||||
${userAgentInfoElement}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: "";
|
||||
|
||||
const comment = this._injectNode(`<div
|
||||
class="fc__comment"
|
||||
style="left: ${this.position.left}; top: ${this.position.top};"
|
||||
>
|
||||
<button class="fc__btn fc__comment-delete" title="Remove comment">
|
||||
<img class="fc__icon" src="${Store.filesPath}/icons/delete.svg">
|
||||
</button>
|
||||
<span class="fc__author">${this.author}</span><br><span class="fc__datetime">${this.date} à ${this.time}</span><br>
|
||||
|
||||
<p>${this.message}</p>
|
||||
${contextInfoElement}
|
||||
</div>`);
|
||||
|
||||
Helpers.fixOffscreen(comment);
|
||||
|
||||
const deleteBtn = comment.querySelector(".fc__comment-delete");
|
||||
|
||||
deleteBtn.addEventListener("click", () => {
|
||||
this.remove();
|
||||
});
|
||||
|
||||
const openWindowBtn = document.querySelector(".fc__open-window");
|
||||
|
||||
if (openWindowBtn) {
|
||||
openWindowBtn.addEventListener("click", () => {
|
||||
window.open(
|
||||
window.location.href,
|
||||
"",
|
||||
`width=${this.windowWidth}, height=800`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
comment.addEventListener("mouseleave", () => {
|
||||
setTimeout(() => {
|
||||
this.toBubble();
|
||||
}, 10);
|
||||
});
|
||||
|
||||
this.node = comment;
|
||||
return comment;
|
||||
}
|
||||
|
||||
_injectNode(htmlString) {
|
||||
if (this.node) {
|
||||
document.body.removeChild(this.node);
|
||||
}
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = htmlString;
|
||||
const node = div.firstChild;
|
||||
document.body.appendChild(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the comment message.
|
||||
*/
|
||||
async remove() {
|
||||
const init = {
|
||||
method: "PATCH",
|
||||
};
|
||||
fetch(`/comments/delete/${this.id}/${Store.page.uri}.json`, init)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
Store.comments = Store.comments.filter((item) => item.id != this.id);
|
||||
document.body.removeChild(this.node);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Comment;
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import Store from "../../store.js";
|
||||
import dayjs from "dayjs";
|
||||
import Helpers from "../../composables/helpers.js";
|
||||
import Comment from "./Comment.js";
|
||||
|
||||
class CommentBuilder {
|
||||
constructor() {
|
||||
this.position = {};
|
||||
this.author = "";
|
||||
this.team = "";
|
||||
this.message = "";
|
||||
this.id = Date.now();
|
||||
this.date = dayjs().format("DD/MM/YY");
|
||||
this.time = dayjs().format("HH:mm");
|
||||
this.windowWidth = window.innerWidth;
|
||||
this.userAgent = navigator.userAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} position - An object with two properties: top and left.
|
||||
* @param {string} position.top - The vertical position of the comment in CSS format (e.g. "10px", "50%").
|
||||
* @param {string} position.left - The horizontal position of the comment in CSS format (e.g. "10px", "50%").
|
||||
* @returns {CommentBuilder} The CommentBuilder instance for method chaining.
|
||||
*/
|
||||
setPosition(position) {
|
||||
this.position = position;
|
||||
return this;
|
||||
}
|
||||
|
||||
setAuthor(author) {
|
||||
this.author = author;
|
||||
return this;
|
||||
}
|
||||
|
||||
setTeam(team) {
|
||||
this.team = team;
|
||||
return this;
|
||||
}
|
||||
|
||||
setMessage(message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
setId(id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
setDate(date) {
|
||||
this.date = date;
|
||||
return this;
|
||||
}
|
||||
|
||||
setTime(time) {
|
||||
this.time = time;
|
||||
return this;
|
||||
}
|
||||
|
||||
setWindowWidth(windowWidth) {
|
||||
this.windowWidth = windowWidth;
|
||||
return this;
|
||||
}
|
||||
|
||||
setUserAgent(userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
return this;
|
||||
}
|
||||
|
||||
build() {
|
||||
if (!this.position || !this.author || !this.message) {
|
||||
console.error(
|
||||
"Missing required parameters for Comment: position, author, message",
|
||||
this
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const comment = new Comment(
|
||||
this.position,
|
||||
this.author,
|
||||
this.message,
|
||||
this.id,
|
||||
this.date,
|
||||
this.time,
|
||||
this.windowWidth,
|
||||
this.userAgent,
|
||||
this.team
|
||||
);
|
||||
|
||||
return comment;
|
||||
}
|
||||
}
|
||||
|
||||
export default CommentBuilder;
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import Panel from "./Panel.js";
|
||||
import Store from "../../store.js";
|
||||
import Comment from "../comments/Comment.js";
|
||||
import CommentBuilder from "../comments/CommentBuilder.js";
|
||||
|
||||
class CommentPanel extends Panel {
|
||||
create() {
|
||||
super.create();
|
||||
document
|
||||
.querySelector(".fc__edition-panel__save-btn")
|
||||
.addEventListener("click", () => {
|
||||
this._createComment();
|
||||
});
|
||||
this._textArea.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
this._createComment();
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
document.body.removeChild(this._panel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async _createComment() {
|
||||
const comment = new CommentBuilder()
|
||||
.setAuthor(Store.author)
|
||||
.setTeam(Store.team)
|
||||
.setMessage(this._textArea.value)
|
||||
.setPosition({
|
||||
top: this._getPos().top,
|
||||
left: this._getPos().left,
|
||||
})
|
||||
.build();
|
||||
|
||||
try {
|
||||
Store.comments.push(comment);
|
||||
await Store.save("comments");
|
||||
comment.toBubble();
|
||||
document.body.removeChild(this._panel);
|
||||
} catch (error) {
|
||||
Store.comments = Store.comments.filter(
|
||||
(storedComment) => storedComment.id !== comment.id
|
||||
);
|
||||
document.body.removeChild(comment.node);
|
||||
console.error("Create comment failed:", error);
|
||||
}
|
||||
|
||||
return comment;
|
||||
}
|
||||
}
|
||||
|
||||
export default CommentPanel;
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import Comment from "../comments/Comment.js";
|
||||
import Store from "../../store.js";
|
||||
import Helpers from "../../composables/helpers.js";
|
||||
|
||||
class Panel {
|
||||
constructor() {
|
||||
this._panel;
|
||||
this._textArea;
|
||||
}
|
||||
|
||||
create() {
|
||||
this._panel = this._createPanel();
|
||||
this._textArea = this._createTextArea();
|
||||
const btns = this._createBtns();
|
||||
this._panel.appendChild(this._textArea);
|
||||
this._panel.appendChild(btns);
|
||||
document.body.appendChild(this._panel);
|
||||
this._textArea.focus();
|
||||
|
||||
Helpers.fixOffscreen(this._panel);
|
||||
|
||||
document
|
||||
.querySelector(".fc__edition-panel__remove-btn")
|
||||
.addEventListener("click", () => {
|
||||
document.body.removeChild(this._panel);
|
||||
});
|
||||
}
|
||||
|
||||
_createPanel() {
|
||||
const panel = document.createElement("div");
|
||||
panel.classList.add("fc__edition-panel");
|
||||
|
||||
const panelWidth = 240;
|
||||
const panelHeight = 300;
|
||||
|
||||
panel.style.left = this._getMousePos().x + "px";
|
||||
panel.style.top = this._getMousePos().y + window.scrollY + "px";
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
_createBtns() {
|
||||
const btns = document.createElement("div");
|
||||
btns.classList.add("fc__edition-panel__btns");
|
||||
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.classList.add("fc__edition-panel__remove-btn");
|
||||
removeBtn.textContent = "supprimer";
|
||||
|
||||
const saveBtn = document.createElement("button");
|
||||
saveBtn.classList.add("fc__edition-panel__save-btn");
|
||||
saveBtn.textContent = "enregistrer";
|
||||
|
||||
btns.appendChild(removeBtn);
|
||||
btns.appendChild(saveBtn);
|
||||
|
||||
return btns;
|
||||
}
|
||||
|
||||
_getMousePos() {
|
||||
return {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
};
|
||||
}
|
||||
|
||||
_createTextArea() {
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.classList.add("fc__text");
|
||||
return textArea;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the position of the panel relative to the viewport.
|
||||
* @returns {Object} An object containing the top and left position.
|
||||
* @property {string} top - The vertical position of the comment in pixels (px).
|
||||
* @property {string} left - The horizontal position of the comment in viewport width units (vw).
|
||||
*/
|
||||
_getPos() {
|
||||
const relativeLeft =
|
||||
this._panel.offsetLeft / ((window.innerWidth + window.pageXOffset) / 100);
|
||||
|
||||
return {
|
||||
top: this._panel.style.top,
|
||||
left: relativeLeft + "vw",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default Panel;
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import Panel from "./Panel.js";
|
||||
import Store from "../../store.js";
|
||||
import Suggestion from "../Suggestion.js";
|
||||
|
||||
class SuggestionPanel extends Panel {
|
||||
constructor(field) {
|
||||
super();
|
||||
this._field = field;
|
||||
this._selection = window.getSelection();
|
||||
this._target = this._selection.toString();
|
||||
}
|
||||
create() {
|
||||
this._highlight();
|
||||
this._context = this._getContext();
|
||||
super.create();
|
||||
document
|
||||
.querySelector(".fc__edition-panel__save-btn")
|
||||
.addEventListener("click", () => {
|
||||
this._createSuggestion();
|
||||
});
|
||||
this._textArea.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
this._createSuggestion();
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
document.body.removeChild(this._panel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_createSuggestion() {
|
||||
const suggestion = new Suggestion(
|
||||
Store.author,
|
||||
this._context,
|
||||
this._target,
|
||||
this._textArea.value,
|
||||
this._field.dataset.fieldName
|
||||
);
|
||||
Store.suggestions.push(suggestion);
|
||||
Store.save("suggestions");
|
||||
document.body.removeChild(this._panel);
|
||||
return suggestion;
|
||||
}
|
||||
|
||||
_highlight() {
|
||||
if (this._selection.toString() !== "") {
|
||||
const range = this._selection.getRangeAt(0);
|
||||
const span = document.createElement("span");
|
||||
span.className = "fc__suggestion--edit";
|
||||
range.surroundContents(span);
|
||||
}
|
||||
}
|
||||
|
||||
_getContext() {
|
||||
if (this._selection.toString() !== "") {
|
||||
const textNode = this._selection.anchorNode;
|
||||
const textContent = textNode.textContent;
|
||||
const selectedText = this._selection.toString();
|
||||
const index = textContent.indexOf(selectedText);
|
||||
const before = textContent.slice(Math.max(0, index - 30), index);
|
||||
const after = textContent.slice(
|
||||
index + selectedText.length,
|
||||
index + selectedText.length + 30
|
||||
);
|
||||
const context = {
|
||||
before: before,
|
||||
after: after,
|
||||
};
|
||||
return context;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SuggestionPanel;
|
||||
30
site/plugins/front-comments/src/front/composables/helpers.js
Normal file
30
site/plugins/front-comments/src/front/composables/helpers.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
class Helpers {
|
||||
static fixOffscreen(node) {
|
||||
const margin = 16;
|
||||
|
||||
if (!node) {
|
||||
console.error("fixOffscreen: node does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
const XDiff =
|
||||
window.innerWidth +
|
||||
window.pageXOffset -
|
||||
(node.offsetLeft + node.offsetWidth + window.pageXOffset);
|
||||
|
||||
const YDiff =
|
||||
window.innerHeight +
|
||||
window.pageYOffset -
|
||||
(node.offsetTop + node.offsetHeight);
|
||||
|
||||
if (XDiff <= 0) {
|
||||
node.style.transform = `translateX(${XDiff - margin}px)`;
|
||||
}
|
||||
|
||||
if (YDiff <= 0) {
|
||||
node.style.transform += `translateY(${YDiff - margin}px)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Helpers;
|
||||
73
site/plugins/front-comments/src/front/index.js
Normal file
73
site/plugins/front-comments/src/front/index.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import Comment from "./classes/comments/Comment.js";
|
||||
import Panel from "./classes/edition-panels/Panel.js";
|
||||
import Store from "./store.js";
|
||||
import AddBtn from "./classes/AddBtn.js";
|
||||
import SuggestionPanel from "./classes/edition-panels/SuggestionPanel.js";
|
||||
import Suggestion from "./classes/Suggestion.js";
|
||||
import CommentBuilder from "./classes/comments/CommentBuilder.js";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
Store.author = FCAuthor;
|
||||
Store.team = FCTeam.length > 0 ? FCTeam : undefined;
|
||||
Store.page.id = FCPageId;
|
||||
Store.page.uri = FCPageUri;
|
||||
Store.csrf = FCCsrf;
|
||||
Store.filesPath = FCFilesPath;
|
||||
|
||||
try {
|
||||
FCComments.forEach((item) => {
|
||||
try {
|
||||
const comment = new CommentBuilder()
|
||||
.setPosition(item.position)
|
||||
.setAuthor(item.author)
|
||||
.setMessage(item.message)
|
||||
.setId(item.id)
|
||||
.setDate(item.date)
|
||||
.setTime(item.time)
|
||||
.setWindowWidth(item.windowWidth)
|
||||
.setUserAgent(item.userAgent)
|
||||
.setTeam(item?.team)
|
||||
.build();
|
||||
|
||||
comment.toBubble();
|
||||
|
||||
Store.comments.push(comment);
|
||||
} catch (error) {
|
||||
console.error("Plugin Front Comments : can't parse comments : ", error);
|
||||
console.log("Comments : ", FCComments);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Can't parse comments : ", error);
|
||||
console.log("Comments : ", FCComments);
|
||||
}
|
||||
|
||||
const addBtn = new AddBtn(FCPosition);
|
||||
|
||||
// const suggestions = JSON.parse(FCSuggestions);
|
||||
// suggestions.forEach((item) => {
|
||||
// const suggestion = new Suggestion(
|
||||
// item.author,
|
||||
// item.context,
|
||||
// item.target,
|
||||
// item.suggestion,
|
||||
// item.fieldName,
|
||||
// item.id,
|
||||
// item.date,
|
||||
// item.time
|
||||
// );
|
||||
// Store.suggestions.push(suggestion);
|
||||
// });
|
||||
// document.querySelectorAll(".fc__field").forEach((field) => {
|
||||
// field.addEventListener("mouseup", (event) => {
|
||||
// const selection = window.getSelection().toString();
|
||||
// if (
|
||||
// selection.length > 0 &&
|
||||
// !event.target.classList.contains("fc__suggestion-bubble")
|
||||
// ) {
|
||||
// const suggestionPanel = new SuggestionPanel(field);
|
||||
// suggestionPanel.create();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
});
|
||||
40
site/plugins/front-comments/src/front/store.js
Normal file
40
site/plugins/front-comments/src/front/store.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
const Store = {
|
||||
comments: [],
|
||||
suggestions: [],
|
||||
author: null,
|
||||
page: {
|
||||
id: null,
|
||||
uri: null,
|
||||
},
|
||||
csrf: null,
|
||||
save(key) {
|
||||
const data = {};
|
||||
data[key] = this[key];
|
||||
const init = {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"X-CSRF": this.csrf,
|
||||
},
|
||||
body: JSON.stringify(data).replaceAll("_", ""),
|
||||
};
|
||||
|
||||
return fetch("/api/pages/" + this.page.id, init)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return Promise.reject(
|
||||
"Front comments plugin - can't add comment: " + response.statusText
|
||||
);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
console.log("Front comments plugin - comment successfully added.");
|
||||
})
|
||||
.catch((error) => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default Store;
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
<template>
|
||||
<k-inside>
|
||||
<template v-if="pages.length > 0">
|
||||
<h1 class="k-header-title">Pages</h1>
|
||||
<section v-for="page in pages" :key="page.uri">
|
||||
<h2 class="k-label">{{ page.title }}</h2>
|
||||
<div class="k-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="k-comment-author">
|
||||
{{ $t("adrienpayet.front-comments.author") }}
|
||||
</th>
|
||||
<th class="k-comment-message">
|
||||
{{ $t("adrienpayet.front-comments.message") }}
|
||||
</th>
|
||||
<th class="k-comment-date">
|
||||
{{ $t("adrienpayet.front-comments.date") }}
|
||||
</th>
|
||||
<th class="k-comment-time">
|
||||
{{ $t("adrienpayet.front-comments.time") }}
|
||||
</th>
|
||||
<th class="k-comment-actions"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="comment in page.comments" :key="comment.id">
|
||||
<td class="k-comment-author">
|
||||
<div class="author-wrapper">
|
||||
<k-icon v-if="comment?.team === 'code'" type="code" />
|
||||
<k-icon v-if="comment?.team === 'design'" type="palette" />
|
||||
<k-icon v-if="comment?.team === 'content'" type="pen" />
|
||||
{{ comment.author }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="k-comment-message" v-html="comment.message"></td>
|
||||
<td class="k-comment-date">
|
||||
{{ comment.date }}
|
||||
</td>
|
||||
<td class="k-comment-time">
|
||||
{{ comment.time }}
|
||||
</td>
|
||||
<td class="k-comment-actions">
|
||||
<k-options-dropdown
|
||||
:options="'comments/' + comment.id + '/' + page.uri"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<h1 v-else class="k-header-title">
|
||||
{{ $t("adrienpayet.front-comments.no-comment") }}
|
||||
</h1>
|
||||
</k-inside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { pages, csrf } = defineProps({
|
||||
pages: Array,
|
||||
csrf: String,
|
||||
});
|
||||
|
||||
sortByDate();
|
||||
|
||||
function convertToDate(dateString, timeString) {
|
||||
const [day, month, year] = dateString.split("/");
|
||||
const [hour, minute] = timeString.split(":");
|
||||
return new Date(`20${year}-${month}-${day}T${hour}:${minute}:00`);
|
||||
}
|
||||
|
||||
function sortByDate() {
|
||||
const convertCommentDate = (comment) =>
|
||||
convertToDate(comment.date, comment.time);
|
||||
|
||||
pages.forEach((page) => {
|
||||
page.comments.sort((commentA, commentB) => {
|
||||
const dateA = convertCommentDate(commentA);
|
||||
const dateB = convertCommentDate(commentB);
|
||||
return dateB - dateA;
|
||||
});
|
||||
});
|
||||
|
||||
pages.sort((pageA, pageB) => {
|
||||
const lastCommentA = pageA.comments[pageA.comments.length - 1];
|
||||
const lastCommentB = pageB.comments[pageB.comments.length - 1];
|
||||
const dateA = convertCommentDate(lastCommentA);
|
||||
const dateB = convertCommentDate(lastCommentB);
|
||||
return dateB - dateA;
|
||||
});
|
||||
}
|
||||
|
||||
function remove(commentId, pageUri) {
|
||||
const pageIndex = pages.findIndex((page) =>
|
||||
page.comments.some((comment) => comment.id === commentId)
|
||||
);
|
||||
const commentIndex = pages[pageIndex].comments.findIndex(
|
||||
(comment) => comment.id === commentId
|
||||
);
|
||||
|
||||
const updatedComments = pages[pageIndex].comments.filter(
|
||||
(comment) => comment.id !== commentId
|
||||
);
|
||||
|
||||
pages[pageIndex].comments = updatedComments;
|
||||
|
||||
const init = {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"X-CSRF": csrf,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
comments: updatedComments,
|
||||
}),
|
||||
};
|
||||
|
||||
fetch("/api/pages/" + pageUri.replace("/", "+"), init)
|
||||
.then((response) => response.json())
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
console.log("Page successfully updated.");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
h2 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.k-comment-author .k-icon {
|
||||
display: inline-block;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.k-table .k-table-index-column {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.k-table td {
|
||||
padding: 0.5rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.k-comment-author {
|
||||
width: 15rem;
|
||||
}
|
||||
|
||||
.author-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.k-comment-message {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.k-comment-date {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.k-comment-time {
|
||||
width: 5rem;
|
||||
}
|
||||
|
||||
.k-comment-actions {
|
||||
width: 2rem;
|
||||
}
|
||||
</style>
|
||||
11
site/plugins/front-comments/src/panel/index.js
Normal file
11
site/plugins/front-comments/src/panel/index.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import KCommentsView from "./components/KCommentsView.vue";
|
||||
|
||||
panel.plugin("adrienpayet/front-comments", {
|
||||
icons: {
|
||||
gitlab:
|
||||
'<path fill="none" d="M0 0h24v24H0z"></path><path d="M5.54429 2.67305C5.81644 2.49995 6.13587 2.41612 6.45799 2.43329C6.78102 2.4505 7.09056 2.56841 7.34318 2.77049L7.34405 2.77119C7.59044 2.96879 7.76998 3.2372 7.85866 3.5399L9.30537 7.96754H14.6944L16.1411 3.5399C16.2298 3.23722 16.4093 2.96879 16.6557 2.77116L16.6604 2.76745C16.9128 2.56777 17.2209 2.45133 17.5424 2.43423C17.8638 2.41712 18.1826 2.50023 18.4547 2.67197L18.4571 2.67347C18.7307 2.84735 18.9427 3.10328 19.0624 3.40486L19.0664 3.41491L21.5393 9.86622C21.9619 10.9712 22.0136 12.1836 21.6865 13.3205C21.3594 14.4574 20.6715 15.457 19.7263 16.1685L12.9955 21.2331L12.9945 21.2338C12.7066 21.4513 12.3554 21.5692 11.9943 21.5692C11.6332 21.5692 11.2819 21.4513 10.9939 21.2337L4.26254 16.1683C3.32063 15.4562 2.63541 14.4574 2.30989 13.3224C1.98437 12.1873 2.03616 10.9772 2.45747 9.8741L4.93724 3.40497C5.0571 3.10297 5.26966 2.84673 5.54429 2.67305ZM6.35534 4.73567L4.16029 10.4639C3.87993 11.2013 3.82298 12.0676 4.04049 12.8261C4.25704 13.5811 4.71123 14.2461 5.33544 14.7225L11.9943 19.7329L18.6484 14.7265C19.2789 14.2502 19.7379 13.5822 19.9563 12.8227C20.1751 12.0624 20.1148 11.1847 19.8328 10.4455L17.6444 4.73558L16.0001 9.76791H7.9996L6.35534 4.73567Z"></path>',
|
||||
},
|
||||
components: {
|
||||
"k-comments-view": KCommentsView,
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue