Skip to content

Commit 91f79a0

Browse files
authored
Merge pull request #2415 from tf/badge-improvements
Comment badge improvements
2 parents bcf2f5c + f968494 commit 91f79a0

15 files changed

Lines changed: 256 additions & 154 deletions

entry_types/scrolled/package/src/frontend/inlineEditing/ContentElementDecorator.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ function DefaultSelectionRect(props) {
6262
scrollPoint={isSelected}
6363
drag={drag}
6464
dragHandleTitle={t('pageflow_scrolled.inline_editing.drag_content_element')}
65-
full={props.width === widths.full || props.customMargin}
65+
full={props.width === widths.full}
66+
customMargin={props.customMargin}
6667
inset={props.position === 'backdrop'}
6768
commentBadge={features.isEnabled('commenting') &&
6869
<ThreadsBadge subjectType="ContentElement"
@@ -89,8 +90,12 @@ function DefaultSelectionRect(props) {
8990
function renderMarginIndicators(props) {
9091
return (
9192
<>
92-
<MarginIndicator marginValue={props.itemProps?.marginTop} position="top" />
93-
<MarginIndicator marginValue={props.itemProps?.marginBottom} position="bottom" />
93+
<MarginIndicator marginValue={props.itemProps?.marginTop}
94+
position="top"
95+
tooltipInset={props.width === widths.full || props.customMargin} />
96+
<MarginIndicator marginValue={props.itemProps?.marginBottom}
97+
position="bottom"
98+
tooltipInset={props.width === widths.full || props.customMargin} />
9499
</>
95100
);
96101
}

entry_types/scrolled/package/src/frontend/inlineEditing/EditableText/BadgeColumn.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {useContentElementAttributes} from '../../useContentElementAttributes';
99
import {useEditorSelection} from '../EditorState';
1010
import {highlightOverlapsSelection} from './highlightOverlapsSelection';
1111

12+
import styles from './BadgeColumn.module.css';
13+
1214
export function BadgeColumn({highlights, anchors}) {
1315
const editor = useSlate();
1416
// Treat `editor.selection` as a live cursor only while the editor
@@ -37,7 +39,7 @@ function PositionedBadge({highlight, editorSelection, anchors}) {
3739
});
3840

3941
const {refs, floatingStyles, hasAnchor} =
40-
useAnchoredFloating(highlight.key, anchors);
42+
useAnchoredFloating(highlight.key, anchors, {placement: 'left-start'});
4143

4244
if (!hasAnchor) return null;
4345

@@ -49,7 +51,7 @@ function PositionedBadge({highlight, editorSelection, anchors}) {
4951

5052
return (
5153
<FloatingPortal root={portalRoot}>
52-
<div ref={refs.setFloating} style={floatingStyles}>
54+
<div ref={refs.setFloating} className={styles.box} style={floatingStyles}>
5355
<Badge counter={1} mode={mode} onClick={() => select()} />
5456
</div>
5557
</FloatingPortal>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
@value (
2+
darkContentTextColor, lightContentTextColor
3+
) from "pageflow-scrolled/values/colors.module.css";
4+
5+
.box {
6+
display: flex;
7+
align-items: center;
8+
justify-content: center;
9+
width: 30px;
10+
height: 30px;
11+
}
12+
13+
.onDark {
14+
color: lightContentTextColor;
15+
}
16+
17+
.onLight {
18+
color: darkContentTextColor;
19+
}

entry_types/scrolled/package/src/frontend/inlineEditing/EditableText/HoveringToolbar.js

Lines changed: 15 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
import React, {useCallback, useEffect, useState} from 'react';
2-
import {Editor, Range, Transforms} from 'slate';
1+
import React, {useCallback, useState} from 'react';
2+
import {Editor, Transforms} from 'slate';
33
import {ReactEditor, useSlate} from 'slate-react';
44

5-
import {features} from 'pageflow/frontend';
65
import {useFloating, FloatingPortal, shift, offset} from '@floating-ui/react';
76

87
import {Toolbar} from '../Toolbar';
98
import {useI18n} from '../../i18n';
109
import {useSelectLinkDestination} from '../useSelectLinkDestination';
1110
import {useFloatingPortalRoot} from '../../FloatingPortalRootProvider';
12-
import {useContentElementAttributes} from '../../useContentElementAttributes';
13-
import {useEditorSelection} from '../EditorState';
11+
import {useEffectiveSelection} from './useEffectiveSelection';
1412
import {isMarkActive, toggleMark} from './marks';
1513

1614
import BoldIcon from '../images/bold.svg';
@@ -20,16 +18,13 @@ import StrikethroughIcon from '../images/strikethrough.svg';
2018
import SubIcon from '../images/sub.svg';
2119
import SupIcon from '../images/sup.svg';
2220
import LinkIcon from '../images/link.svg';
23-
import AddCommentIcon from '../images/addComment.svg';
2421

2522
export function HoveringToolbar({children}) {
2623
const editor = useSlate()
2724
const {t} = useI18n({locale: 'ui'});
2825
const selectLinkDestination = useSelectLinkDestination();
29-
const startNewThread = useStartNewThread(editor);
3026

3127
const [isOpen, setIsOpen] = useState(false);
32-
const [isDragging, setIsDragging] = useState(false);
3328

3429
const {refs, floatingStyles} = useFloating({
3530
placement: 'bottom-start',
@@ -44,87 +39,39 @@ export function HoveringToolbar({children}) {
4439
]
4540
});
4641

47-
useEffect(() => {
48-
const {selection} = editor
49-
50-
if (
51-
isDragging ||
52-
!selection ||
53-
!ReactEditor.isFocused(editor) ||
54-
Range.isCollapsed(selection) ||
55-
Editor.string(editor, selection) === ''
56-
) {
42+
useEffectiveSelection(editor, useCallback(selection => {
43+
if (!selection) {
5744
setIsOpen(false);
58-
return
45+
return;
5946
}
6047

61-
const domRange = ReactEditor.toDOMRange(editor, editor.selection);
48+
const domRange = ReactEditor.toDOMRange(editor, selection);
6249

6350
refs.setPositionReference({
6451
getBoundingClientRect: () => domRange.getBoundingClientRect(),
6552
getClientRects: () => domRange.getClientRects()
6653
});
6754

6855
setIsOpen(true);
69-
}, [refs, editor, editor.selection, isDragging]);
70-
71-
const handleMouseDown = useCallback(() => {
72-
setIsDragging(true);
73-
74-
function handleMouseUp() {
75-
// When clicking inside a selection, the selection sometimes
76-
// only resets after mouseup has fired. Prevent toolbar from
77-
// shortly being hidden and shown again before finally being
78-
// hidden when the selection resets.
79-
setTimeout(() => {
80-
setIsDragging(false);
81-
}, 10)
82-
}
83-
84-
document.addEventListener('mouseup', handleMouseUp);
85-
86-
return () => {
87-
document.removeEventListener('mouseup', handleMouseUp);
88-
}
89-
}, []);
56+
}, [editor, refs]));
9057

9158
const floatingPortalRoot = useFloatingPortalRoot();
9259

9360
return (
94-
<div onMouseDown={handleMouseDown}>
61+
<>
9562
{isOpen &&
9663
<FloatingPortal root={floatingPortalRoot}>
9764
<div ref={refs.setFloating}
9865
style={floatingStyles}>
99-
{renderToolbar(editor, t, selectLinkDestination, startNewThread)}
66+
{renderToolbar(editor, t, selectLinkDestination)}
10067
</div>
10168
</FloatingPortal>}
10269
{children}
103-
</div>
70+
</>
10471
);
10572
}
10673

107-
// Returns a function that opens the new-thread form for the current
108-
// selection, or null when commenting is disabled for this element.
109-
function useStartNewThread(editor) {
110-
const {contentElementPermaId, inlineComments} = useContentElementAttributes();
111-
const commentingEnabled = features.isEnabled('commenting') && inlineComments;
112-
const {select: selectNewThread} = useEditorSelection({
113-
type: 'newThread',
114-
id: contentElementPermaId
115-
});
116-
117-
if (!commentingEnabled) return null;
118-
119-
return () => selectNewThread({
120-
type: 'newThread',
121-
id: contentElementPermaId,
122-
subjectType: 'ContentElement',
123-
range: editor.selection
124-
});
125-
}
126-
127-
function renderToolbar(editor, t, selectLinkDestination, startNewThread) {
74+
function renderToolbar(editor, t, selectLinkDestination) {
12875
const buttons = [
12976
{
13077
name: 'bold',
@@ -162,21 +109,16 @@ function renderToolbar(editor, t, selectLinkDestination, startNewThread) {
162109
t('pageflow_scrolled.inline_editing.remove_link') :
163110
t('pageflow_scrolled.inline_editing.insert_link'),
164111
icon: LinkIcon
165-
},
166-
...(startNewThread ? [{
167-
name: 'comment',
168-
text: t('pageflow_scrolled.inline_editing.add_comment'),
169-
icon: AddCommentIcon
170-
}] : [])
112+
}
171113
].map(button => ({...button, active: isButtonActive(editor, button.name)}));
172114

173115
return (
174116
<Toolbar buttons={buttons}
175-
onButtonClick={name => handleButtonClick(editor, name, selectLinkDestination, startNewThread)}/>
117+
onButtonClick={name => handleButtonClick(editor, name, selectLinkDestination)}/>
176118
);
177119
}
178120

179-
function handleButtonClick(editor, format, selectLinkDestination, startNewThread) {
121+
function handleButtonClick(editor, format, selectLinkDestination) {
180122
if (format === 'link') {
181123
if (isLinkActive(editor)) {
182124
unwrapLink(editor);
@@ -187,9 +129,6 @@ function handleButtonClick(editor, format, selectLinkDestination, startNewThread
187129
}, () => {});
188130
}
189131
}
190-
else if (format === 'comment') {
191-
startNewThread();
192-
}
193132
else {
194133
toggleMark(editor, format);
195134
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React, {useCallback, useState} from 'react';
2+
import classNames from 'classnames';
3+
import {FloatingPortal, useFloating} from '@floating-ui/react';
4+
import {ReactEditor, useSlate} from 'slate-react';
5+
6+
import {Badge, alignToContainerEdge} from 'pageflow-scrolled/review';
7+
import {useFloatingPortalRoot} from '../../FloatingPortalRootProvider';
8+
import {useDarkBackground} from '../../backgroundColor';
9+
import {useEffectiveSelection} from './useEffectiveSelection';
10+
import {useStartNewThread} from './useStartNewThread';
11+
12+
import styles from './BadgeColumn.module.css';
13+
14+
export function PendingSelectionBadge({containerRef}) {
15+
const editor = useSlate();
16+
const portalRoot = useFloatingPortalRoot();
17+
const [isVisible, setIsVisible] = useState(false);
18+
const darkBackground = useDarkBackground();
19+
const startNewThread = useStartNewThread(editor);
20+
21+
const {refs, floatingStyles} = useFloating({
22+
placement: 'left-start',
23+
middleware: [
24+
alignToContainerEdge(containerRef, {mainAxisOffset: 32})
25+
]
26+
});
27+
28+
useEffectiveSelection(editor, useCallback(selection => {
29+
if (!selection) {
30+
setIsVisible(false);
31+
return;
32+
}
33+
34+
const domRange = ReactEditor.toDOMRange(editor, selection);
35+
refs.setPositionReference({
36+
getBoundingClientRect: () => domRange.getBoundingClientRect(),
37+
getClientRects: () => domRange.getClientRects()
38+
});
39+
setIsVisible(true);
40+
}, [editor, refs]));
41+
42+
if (!isVisible) return null;
43+
44+
return (
45+
<FloatingPortal root={portalRoot}>
46+
<div ref={refs.setFloating}
47+
className={classNames(styles.box, darkBackground ? styles.onDark : styles.onLight)}
48+
style={floatingStyles}>
49+
<Badge counter={0} mode="icon" onClick={startNewThread} />
50+
</div>
51+
</FloatingPortal>
52+
);
53+
}

entry_types/scrolled/package/src/frontend/inlineEditing/EditableText/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {useContentElementEditorCommandSubscription} from '../../useContentElemen
1111
import {useContentElementEditorState} from '../../useContentElementEditorState';
1212
import {TextPlaceholder} from '../TextPlaceholder';
1313
import {BadgeColumn} from './BadgeColumn';
14+
import {PendingSelectionBadge} from './PendingSelectionBadge';
1415
import {useCommenting} from './useCommenting';
1516

1617
import {withCustomInsertBreak} from './withCustomInsertBreak';
@@ -198,7 +199,10 @@ export const EditableText = React.memo(function EditableText({
198199
</HoveringToolbar>
199200
</LinkTooltipProvider>
200201
{commentingEnabled &&
201-
<BadgeColumn highlights={highlights} anchors={anchors} />}
202+
<>
203+
<BadgeColumn highlights={highlights} anchors={anchors} />
204+
<PendingSelectionBadge containerRef={anchors.containerRef} />
205+
</>}
202206
</Slate>
203207
<TextPlaceholder text={placeholder}
204208
className={placeholderClassName}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {useEffect, useState} from 'react';
2+
import {Editor, Range} from 'slate';
3+
import {ReactEditor} from 'slate-react';
4+
5+
export function useEffectiveSelection(editor, onChange) {
6+
const isDragging = useEditorDragging(editor);
7+
8+
useEffect(() => {
9+
const {selection} = editor;
10+
11+
if (
12+
isDragging ||
13+
!selection ||
14+
!ReactEditor.isFocused(editor) ||
15+
Range.isCollapsed(selection) ||
16+
Editor.string(editor, selection) === ''
17+
) {
18+
onChange(null);
19+
return;
20+
}
21+
22+
onChange(selection);
23+
}, [editor, editor.selection, isDragging, onChange]);
24+
}
25+
26+
function useEditorDragging(editor) {
27+
const [isDragging, setIsDragging] = useState(false);
28+
29+
useEffect(() => {
30+
const editorEl = ReactEditor.toDOMNode(editor, editor);
31+
32+
function handleMouseDown() {
33+
setIsDragging(true);
34+
35+
function handleMouseUp() {
36+
// Selection sometimes only resets after mouseup has fired
37+
// when clicking inside an existing selection.
38+
setTimeout(() => setIsDragging(false), 10);
39+
}
40+
41+
document.addEventListener('mouseup', handleMouseUp, {once: true});
42+
}
43+
44+
editorEl.addEventListener('mousedown', handleMouseDown);
45+
return () => editorEl.removeEventListener('mousedown', handleMouseDown);
46+
}, [editor]);
47+
48+
return isDragging;
49+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {features} from 'pageflow/frontend';
2+
3+
import {useContentElementAttributes} from '../../useContentElementAttributes';
4+
import {useEditorSelection} from '../EditorState';
5+
6+
export function useStartNewThread(editor) {
7+
const {contentElementPermaId, inlineComments} = useContentElementAttributes();
8+
const commentingEnabled = features.isEnabled('commenting') && inlineComments;
9+
const {select: selectNewThread} = useEditorSelection({
10+
type: 'newThread',
11+
id: contentElementPermaId
12+
});
13+
14+
if (!commentingEnabled) return null;
15+
16+
return () => selectNewThread({
17+
type: 'newThread',
18+
id: contentElementPermaId,
19+
subjectType: 'ContentElement',
20+
range: editor.selection
21+
});
22+
}

0 commit comments

Comments
 (0)