Skip to content

Commit e8f4418

Browse files
committed
Comments: Split out page comment reference logic to own component
Started support for editor view. Moved comment elements to be added relative to content area instad of specific target reference element. Added relocating on screen size change.
1 parent ecda4e1 commit e8f4418

File tree

8 files changed

+256
-156
lines changed

8 files changed

+256
-156
lines changed

resources/js/components/editor-toolbox.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export class EditorToolbox extends Component {
1010
this.toggleButton = this.$refs.toggle;
1111
this.editorWrapEl = this.container.closest('.page-editor');
1212

13+
// State
14+
this.open = false;
15+
this.tab = '';
16+
1317
this.setupListeners();
1418

1519
// Set the first tab as active on load
@@ -34,6 +38,8 @@ export class EditorToolbox extends Component {
3438
const isOpen = this.container.classList.contains('open');
3539
this.toggleButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
3640
this.editorWrapEl.classList.toggle('toolbox-open', isOpen);
41+
this.open = isOpen;
42+
this.emitState();
3743
}
3844

3945
setActiveTab(tabName, openToolbox = false) {
@@ -54,6 +60,13 @@ export class EditorToolbox extends Component {
5460
if (openToolbox && !this.container.classList.contains('open')) {
5561
this.toggle();
5662
}
63+
64+
this.tab = tabName;
65+
this.emitState();
66+
}
67+
68+
emitState() {
69+
this.$emit('change', {tab: this.tab, open: this.open});
5770
}
5871

5972
}

resources/js/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export {NewUserPassword} from './new-user-password';
3636
export {Notification} from './notification';
3737
export {OptionalInput} from './optional-input';
3838
export {PageComment} from './page-comment';
39+
export {PageCommentReference} from './page-comment-reference';
3940
export {PageComments} from './page-comments';
4041
export {PageDisplay} from './page-display';
4142
export {PageEditor} from './page-editor';
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import {Component} from "./component";
2+
import {findTargetNodeAndOffset, hashElement} from "../services/dom";
3+
import {el} from "../wysiwyg/utils/dom";
4+
import commentIcon from "@icons/comment.svg";
5+
import closeIcon from "@icons/close.svg";
6+
import {scrollAndHighlightElement} from "../services/util";
7+
8+
/**
9+
* Track the close function for the current open marker so it can be closed
10+
* when another is opened so we only show one marker comment thread at one time.
11+
*/
12+
let openMarkerClose: Function|null = null;
13+
14+
export class PageCommentReference extends Component {
15+
protected link: HTMLLinkElement;
16+
protected reference: string;
17+
protected markerWrap: HTMLElement|null = null;
18+
19+
protected viewCommentText: string;
20+
protected jumpToThreadText: string;
21+
protected closeText: string;
22+
23+
setup() {
24+
this.link = this.$el as HTMLLinkElement;
25+
this.reference = this.$opts.reference;
26+
this.viewCommentText = this.$opts.viewCommentText;
27+
this.jumpToThreadText = this.$opts.jumpToThreadText;
28+
this.closeText = this.$opts.closeText;
29+
30+
// Show within page display area if seen
31+
const pageContentArea = document.querySelector('.page-content');
32+
if (pageContentArea instanceof HTMLElement) {
33+
this.updateMarker(pageContentArea);
34+
}
35+
36+
// Handle editor view to show on comments toolbox view
37+
window.addEventListener('editor-toolbox-change', (event) => {
38+
const tabName: string = (event as {detail: {tab: string, open: boolean}}).detail.tab;
39+
const isOpen = (event as {detail: {tab: string, open: boolean}}).detail.open;
40+
if (tabName === 'comments' && isOpen) {
41+
this.showForEditor();
42+
} else {
43+
this.hideMarker();
44+
}
45+
});
46+
}
47+
48+
protected showForEditor() {
49+
const contentWrap = document.querySelector('.editor-content-wrap');
50+
if (contentWrap instanceof HTMLElement) {
51+
this.updateMarker(contentWrap);
52+
}
53+
54+
const onChange = () => {
55+
this.hideMarker();
56+
setTimeout(() => {
57+
window.$events.remove('editor-html-change', onChange);
58+
}, 1);
59+
};
60+
61+
window.$events.listen('editor-html-change', onChange);
62+
}
63+
64+
protected updateMarker(contentContainer: HTMLElement) {
65+
// Reset link and existing marker
66+
this.link.classList.remove('outdated', 'missing');
67+
if (this.markerWrap) {
68+
this.markerWrap.remove();
69+
}
70+
71+
const [refId, refHash, refRange] = this.reference.split(':');
72+
const refEl = document.getElementById(refId);
73+
if (!refEl) {
74+
this.link.classList.add('outdated', 'missing');
75+
return;
76+
}
77+
78+
const refCloneToAssess = refEl.cloneNode(true) as HTMLElement;
79+
const toRemove = refCloneToAssess.querySelectorAll('[data-lexical-text]');
80+
refCloneToAssess.removeAttribute('style');
81+
for (const el of toRemove) {
82+
el.after(...el.childNodes);
83+
el.remove();
84+
}
85+
86+
const actualHash = hashElement(refCloneToAssess);
87+
if (actualHash !== refHash) {
88+
this.link.classList.add('outdated');
89+
}
90+
91+
const marker = el('button', {
92+
type: 'button',
93+
class: 'content-comment-marker',
94+
title: this.viewCommentText,
95+
});
96+
marker.innerHTML = <string>commentIcon;
97+
marker.addEventListener('click', event => {
98+
this.showCommentAtMarker(marker);
99+
});
100+
101+
this.markerWrap = el('div', {
102+
class: 'content-comment-highlight',
103+
}, [marker]);
104+
105+
contentContainer.append(this.markerWrap);
106+
this.positionMarker(refEl, refRange);
107+
108+
this.link.href = `#${refEl.id}`;
109+
this.link.addEventListener('click', (event: MouseEvent) => {
110+
event.preventDefault();
111+
scrollAndHighlightElement(refEl);
112+
});
113+
114+
window.addEventListener('resize', () => {
115+
this.positionMarker(refEl, refRange);
116+
});
117+
}
118+
119+
protected positionMarker(targetEl: HTMLElement, range: string) {
120+
if (!this.markerWrap) {
121+
return;
122+
}
123+
124+
const markerParent = this.markerWrap.parentElement as HTMLElement;
125+
const parentBounds = markerParent.getBoundingClientRect();
126+
let targetBounds = targetEl.getBoundingClientRect();
127+
const [rangeStart, rangeEnd] = range.split('-');
128+
if (rangeStart && rangeEnd) {
129+
const range = new Range();
130+
const relStart = findTargetNodeAndOffset(targetEl, Number(rangeStart));
131+
const relEnd = findTargetNodeAndOffset(targetEl, Number(rangeEnd));
132+
if (relStart && relEnd) {
133+
range.setStart(relStart.node, relStart.offset);
134+
range.setEnd(relEnd.node, relEnd.offset);
135+
targetBounds = range.getBoundingClientRect();
136+
}
137+
}
138+
139+
const relLeft = targetBounds.left - parentBounds.left;
140+
const relTop = (targetBounds.top - parentBounds.top) + markerParent.scrollTop;
141+
142+
this.markerWrap.style.left = `${relLeft}px`;
143+
this.markerWrap.style.top = `${relTop}px`;
144+
this.markerWrap.style.width = `${targetBounds.width}px`;
145+
this.markerWrap.style.height = `${targetBounds.height}px`;
146+
}
147+
148+
protected hideMarker() {
149+
// Hide marker and close existing marker windows
150+
if (openMarkerClose) {
151+
openMarkerClose();
152+
}
153+
this.markerWrap?.remove();
154+
}
155+
156+
protected showCommentAtMarker(marker: HTMLElement): void {
157+
// Hide marker and close existing marker windows
158+
if (openMarkerClose) {
159+
openMarkerClose();
160+
}
161+
marker.hidden = true;
162+
163+
// Locate relevant comment
164+
const commentBox = this.link.closest('.comment-box') as HTMLElement;
165+
166+
// Build comment window
167+
const readClone = (commentBox.closest('.comment-branch') as HTMLElement).cloneNode(true) as HTMLElement;
168+
const toRemove = readClone.querySelectorAll('.actions, form');
169+
for (const el of toRemove) {
170+
el.remove();
171+
}
172+
173+
const close = el('button', {type: 'button', title: this.closeText});
174+
close.innerHTML = (closeIcon as string);
175+
const jump = el('button', {type: 'button', 'data-action': 'jump'}, [this.jumpToThreadText]);
176+
177+
const commentWindow = el('div', {
178+
class: 'content-comment-window'
179+
}, [
180+
el('div', {
181+
class: 'content-comment-window-actions',
182+
}, [jump, close]),
183+
el('div', {
184+
class: 'content-comment-window-content comment-container-compact comment-container-super-compact',
185+
}, [readClone]),
186+
]);
187+
188+
marker.parentElement?.append(commentWindow);
189+
190+
// Handle interaction within window
191+
const closeAction = () => {
192+
commentWindow.remove();
193+
marker.hidden = false;
194+
window.removeEventListener('click', windowCloseAction);
195+
openMarkerClose = null;
196+
};
197+
198+
const windowCloseAction = (event: MouseEvent) => {
199+
if (!(marker.parentElement as HTMLElement).contains(event.target as HTMLElement)) {
200+
closeAction();
201+
}
202+
};
203+
window.addEventListener('click', windowCloseAction);
204+
205+
openMarkerClose = closeAction;
206+
close.addEventListener('click', closeAction.bind(this));
207+
jump.addEventListener('click', () => {
208+
closeAction();
209+
commentBox.scrollIntoView({behavior: 'smooth'});
210+
const highlightTarget = commentBox.querySelector('.header') as HTMLElement;
211+
highlightTarget.classList.add('anim-highlight');
212+
highlightTarget.addEventListener('animationend', () => highlightTarget.classList.remove('anim-highlight'))
213+
});
214+
215+
// Position window within bounds
216+
const commentWindowBounds = commentWindow.getBoundingClientRect();
217+
const contentBounds = document.querySelector('.page-content')?.getBoundingClientRect();
218+
if (contentBounds && commentWindowBounds.right > contentBounds.right) {
219+
const diff = commentWindowBounds.right - contentBounds.right;
220+
commentWindow.style.left = `-${diff}px`;
221+
}
222+
}
223+
}

0 commit comments

Comments
 (0)