Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ function DefaultSelectionRect(props) {
scrollPoint={isSelected}
drag={drag}
dragHandleTitle={t('pageflow_scrolled.inline_editing.drag_content_element')}
full={props.width === widths.full || props.customMargin}
full={props.width === widths.full}
customMargin={props.customMargin}
inset={props.position === 'backdrop'}
commentBadge={features.isEnabled('commenting') &&
<ThreadsBadge subjectType="ContentElement"
Expand All @@ -89,8 +90,12 @@ function DefaultSelectionRect(props) {
function renderMarginIndicators(props) {
return (
<>
<MarginIndicator marginValue={props.itemProps?.marginTop} position="top" />
<MarginIndicator marginValue={props.itemProps?.marginBottom} position="bottom" />
<MarginIndicator marginValue={props.itemProps?.marginTop}
position="top"
tooltipInset={props.width === widths.full || props.customMargin} />
<MarginIndicator marginValue={props.itemProps?.marginBottom}
position="bottom"
tooltipInset={props.width === widths.full || props.customMargin} />
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {useContentElementAttributes} from '../../useContentElementAttributes';
import {useEditorSelection} from '../EditorState';
import {highlightOverlapsSelection} from './highlightOverlapsSelection';

import styles from './BadgeColumn.module.css';

export function BadgeColumn({highlights, anchors}) {
const editor = useSlate();
// Treat `editor.selection` as a live cursor only while the editor
Expand Down Expand Up @@ -37,7 +39,7 @@ function PositionedBadge({highlight, editorSelection, anchors}) {
});

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

if (!hasAnchor) return null;

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

return (
<FloatingPortal root={portalRoot}>
<div ref={refs.setFloating} style={floatingStyles}>
<div ref={refs.setFloating} className={styles.box} style={floatingStyles}>
<Badge counter={1} mode={mode} onClick={() => select()} />
</div>
</FloatingPortal>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@value (
darkContentTextColor, lightContentTextColor
) from "pageflow-scrolled/values/colors.module.css";

.box {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
}

.onDark {
color: lightContentTextColor;
}

.onLight {
color: darkContentTextColor;
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import React, {useCallback, useEffect, useState} from 'react';
import {Editor, Range, Transforms} from 'slate';
import React, {useCallback, useState} from 'react';
import {Editor, Transforms} from 'slate';
import {ReactEditor, useSlate} from 'slate-react';

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

import {Toolbar} from '../Toolbar';
import {useI18n} from '../../i18n';
import {useSelectLinkDestination} from '../useSelectLinkDestination';
import {useFloatingPortalRoot} from '../../FloatingPortalRootProvider';
import {useContentElementAttributes} from '../../useContentElementAttributes';
import {useEditorSelection} from '../EditorState';
import {useEffectiveSelection} from './useEffectiveSelection';
import {isMarkActive, toggleMark} from './marks';

import BoldIcon from '../images/bold.svg';
Expand All @@ -20,16 +18,13 @@ import StrikethroughIcon from '../images/strikethrough.svg';
import SubIcon from '../images/sub.svg';
import SupIcon from '../images/sup.svg';
import LinkIcon from '../images/link.svg';
import AddCommentIcon from '../images/addComment.svg';

export function HoveringToolbar({children}) {
const editor = useSlate()
const {t} = useI18n({locale: 'ui'});
const selectLinkDestination = useSelectLinkDestination();
const startNewThread = useStartNewThread(editor);

const [isOpen, setIsOpen] = useState(false);
const [isDragging, setIsDragging] = useState(false);

const {refs, floatingStyles} = useFloating({
placement: 'bottom-start',
Expand All @@ -44,87 +39,39 @@ export function HoveringToolbar({children}) {
]
});

useEffect(() => {
const {selection} = editor

if (
isDragging ||
!selection ||
!ReactEditor.isFocused(editor) ||
Range.isCollapsed(selection) ||
Editor.string(editor, selection) === ''
) {
useEffectiveSelection(editor, useCallback(selection => {
if (!selection) {
setIsOpen(false);
return
return;
}

const domRange = ReactEditor.toDOMRange(editor, editor.selection);
const domRange = ReactEditor.toDOMRange(editor, selection);

refs.setPositionReference({
getBoundingClientRect: () => domRange.getBoundingClientRect(),
getClientRects: () => domRange.getClientRects()
});

setIsOpen(true);
}, [refs, editor, editor.selection, isDragging]);

const handleMouseDown = useCallback(() => {
setIsDragging(true);

function handleMouseUp() {
// When clicking inside a selection, the selection sometimes
// only resets after mouseup has fired. Prevent toolbar from
// shortly being hidden and shown again before finally being
// hidden when the selection resets.
setTimeout(() => {
setIsDragging(false);
}, 10)
}

document.addEventListener('mouseup', handleMouseUp);

return () => {
document.removeEventListener('mouseup', handleMouseUp);
}
}, []);
}, [editor, refs]));

const floatingPortalRoot = useFloatingPortalRoot();

return (
<div onMouseDown={handleMouseDown}>
<>
{isOpen &&
<FloatingPortal root={floatingPortalRoot}>
<div ref={refs.setFloating}
style={floatingStyles}>
{renderToolbar(editor, t, selectLinkDestination, startNewThread)}
{renderToolbar(editor, t, selectLinkDestination)}
</div>
</FloatingPortal>}
{children}
</div>
</>
);
}

// Returns a function that opens the new-thread form for the current
// selection, or null when commenting is disabled for this element.
function useStartNewThread(editor) {
const {contentElementPermaId, inlineComments} = useContentElementAttributes();
const commentingEnabled = features.isEnabled('commenting') && inlineComments;
const {select: selectNewThread} = useEditorSelection({
type: 'newThread',
id: contentElementPermaId
});

if (!commentingEnabled) return null;

return () => selectNewThread({
type: 'newThread',
id: contentElementPermaId,
subjectType: 'ContentElement',
range: editor.selection
});
}

function renderToolbar(editor, t, selectLinkDestination, startNewThread) {
function renderToolbar(editor, t, selectLinkDestination) {
const buttons = [
{
name: 'bold',
Expand Down Expand Up @@ -162,21 +109,16 @@ function renderToolbar(editor, t, selectLinkDestination, startNewThread) {
t('pageflow_scrolled.inline_editing.remove_link') :
t('pageflow_scrolled.inline_editing.insert_link'),
icon: LinkIcon
},
...(startNewThread ? [{
name: 'comment',
text: t('pageflow_scrolled.inline_editing.add_comment'),
icon: AddCommentIcon
}] : [])
}
].map(button => ({...button, active: isButtonActive(editor, button.name)}));

return (
<Toolbar buttons={buttons}
onButtonClick={name => handleButtonClick(editor, name, selectLinkDestination, startNewThread)}/>
onButtonClick={name => handleButtonClick(editor, name, selectLinkDestination)}/>
);
}

function handleButtonClick(editor, format, selectLinkDestination, startNewThread) {
function handleButtonClick(editor, format, selectLinkDestination) {
if (format === 'link') {
if (isLinkActive(editor)) {
unwrapLink(editor);
Expand All @@ -187,9 +129,6 @@ function handleButtonClick(editor, format, selectLinkDestination, startNewThread
}, () => {});
}
}
else if (format === 'comment') {
startNewThread();
}
else {
toggleMark(editor, format);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, {useCallback, useState} from 'react';
import classNames from 'classnames';
import {FloatingPortal, useFloating} from '@floating-ui/react';
import {ReactEditor, useSlate} from 'slate-react';

import {Badge, alignToContainerEdge} from 'pageflow-scrolled/review';
import {useFloatingPortalRoot} from '../../FloatingPortalRootProvider';
import {useDarkBackground} from '../../backgroundColor';
import {useEffectiveSelection} from './useEffectiveSelection';
import {useStartNewThread} from './useStartNewThread';

import styles from './BadgeColumn.module.css';

export function PendingSelectionBadge({containerRef}) {
const editor = useSlate();
const portalRoot = useFloatingPortalRoot();
const [isVisible, setIsVisible] = useState(false);
const darkBackground = useDarkBackground();
const startNewThread = useStartNewThread(editor);

const {refs, floatingStyles} = useFloating({
placement: 'left-start',
middleware: [
alignToContainerEdge(containerRef, {mainAxisOffset: 32})
]
});

useEffectiveSelection(editor, useCallback(selection => {
if (!selection) {
setIsVisible(false);
return;
}

const domRange = ReactEditor.toDOMRange(editor, selection);
refs.setPositionReference({
getBoundingClientRect: () => domRange.getBoundingClientRect(),
getClientRects: () => domRange.getClientRects()
});
setIsVisible(true);
}, [editor, refs]));

if (!isVisible) return null;

return (
<FloatingPortal root={portalRoot}>
<div ref={refs.setFloating}
className={classNames(styles.box, darkBackground ? styles.onDark : styles.onLight)}
style={floatingStyles}>
<Badge counter={0} mode="icon" onClick={startNewThread} />
</div>
</FloatingPortal>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {useContentElementEditorCommandSubscription} from '../../useContentElemen
import {useContentElementEditorState} from '../../useContentElementEditorState';
import {TextPlaceholder} from '../TextPlaceholder';
import {BadgeColumn} from './BadgeColumn';
import {PendingSelectionBadge} from './PendingSelectionBadge';
import {useCommenting} from './useCommenting';

import {withCustomInsertBreak} from './withCustomInsertBreak';
Expand Down Expand Up @@ -198,7 +199,10 @@ export const EditableText = React.memo(function EditableText({
</HoveringToolbar>
</LinkTooltipProvider>
{commentingEnabled &&
<BadgeColumn highlights={highlights} anchors={anchors} />}
<>
<BadgeColumn highlights={highlights} anchors={anchors} />
<PendingSelectionBadge containerRef={anchors.containerRef} />
</>}
</Slate>
<TextPlaceholder text={placeholder}
className={placeholderClassName}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {useEffect, useState} from 'react';
import {Editor, Range} from 'slate';
import {ReactEditor} from 'slate-react';

export function useEffectiveSelection(editor, onChange) {
const isDragging = useEditorDragging(editor);

useEffect(() => {
const {selection} = editor;

if (
isDragging ||
!selection ||
!ReactEditor.isFocused(editor) ||
Range.isCollapsed(selection) ||
Editor.string(editor, selection) === ''
) {
onChange(null);
return;
}

onChange(selection);
}, [editor, editor.selection, isDragging, onChange]);
}

function useEditorDragging(editor) {
const [isDragging, setIsDragging] = useState(false);

useEffect(() => {
const editorEl = ReactEditor.toDOMNode(editor, editor);

function handleMouseDown() {
setIsDragging(true);

function handleMouseUp() {
// Selection sometimes only resets after mouseup has fired
// when clicking inside an existing selection.
setTimeout(() => setIsDragging(false), 10);
}

document.addEventListener('mouseup', handleMouseUp, {once: true});
}

editorEl.addEventListener('mousedown', handleMouseDown);
return () => editorEl.removeEventListener('mousedown', handleMouseDown);
}, [editor]);

return isDragging;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {features} from 'pageflow/frontend';

import {useContentElementAttributes} from '../../useContentElementAttributes';
import {useEditorSelection} from '../EditorState';

export function useStartNewThread(editor) {
const {contentElementPermaId, inlineComments} = useContentElementAttributes();
const commentingEnabled = features.isEnabled('commenting') && inlineComments;
const {select: selectNewThread} = useEditorSelection({
type: 'newThread',
id: contentElementPermaId
});

if (!commentingEnabled) return null;

return () => selectNewThread({
type: 'newThread',
id: contentElementPermaId,
subjectType: 'ContentElement',
range: editor.selection
});
}
Loading
Loading