Skip to content

Commit

Permalink
Cursors
Browse files Browse the repository at this point in the history
  • Loading branch information
cskrov committed Aug 21, 2024
1 parent 80903f3 commit 35aab20
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 64 deletions.
59 changes: 0 additions & 59 deletions frontend/src/components/smart-editor/tabbed-editors/cursors.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
interface BaseColor {
red: number;
green: number;
blue: number;
}

const MAP: Map<string, BaseColor> = new Map();

// #C30000
const RED: BaseColor = { red: 195, green: 0, blue: 0 };
// #06893A
const GREEN: BaseColor = { red: 6, green: 137, blue: 58 };
// #66CBEC
const LIGHT_BLUE: BaseColor = { red: 102, green: 203, blue: 236 };
// #0067C5
const BLUE: BaseColor = { red: 0, green: 103, blue: 197 };
// #FF9100
const ORANGE: BaseColor = { red: 255, green: 145, blue: 0 };
// #005B82
const DEEP_BLUE: BaseColor = { red: 0, green: 91, blue: 130 };
// #634689
const PURPLE: BaseColor = { red: 99, green: 70, blue: 137 };

const COLORS = [RED, GREEN, LIGHT_BLUE, BLUE, ORANGE, DEEP_BLUE, PURPLE];
const COLOR_MAX = COLORS.length;

export const getColor = (key: string, opacity: number): string => {
const existing = MAP.get(key);

if (existing === undefined) {
const randomColorIndex = Math.floor(Math.random() * COLOR_MAX);
const baseColor = COLORS[randomColorIndex]!;

MAP.set(key, baseColor);

return formatColor(baseColor, opacity);
}

return formatColor(existing, opacity);
};

const formatColor = (color: BaseColor, opacity: number): string =>
`rgba(${color.red}, ${color.green}, ${color.blue}, ${opacity})`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { RelativeRange } from '@slate-yjs/core';
import { UnknownObject } from '@udecode/plate-common';
import { CursorData, CursorOverlayProps, CursorProps, useCursorOverlayPositions } from '@udecode/plate-cursor';
import { useContext, useRef } from 'react';
import { styled } from 'styled-components';
import { StaticDataContext } from '@app/components/app/static-data-context';
import { getColor } from '@app/components/smart-editor/tabbed-editors/cursors/cursor-colors';

export interface UserCursor extends CursorData, UnknownObject {
navn: string;
navIdent: string;
}

interface YjsCursor {
selection: RelativeRange;
data: UserCursor;
}

export const isYjsCursor = (value: unknown): value is YjsCursor =>
typeof value === 'object' && value !== null && 'selection' in value && 'data' in value;

const Cursor = ({ caretPosition, data, disableCaret, disableSelection, selectionRects }: CursorProps<UserCursor>) => {
const { style, selectionStyle = style, navIdent, navn } = data ?? {};

const labelRef = useRef<HTMLDivElement>(null);

const caretColor = getColor(navIdent ?? '', 1);
const selectionColor = getColor(navIdent ?? '', 0.2);

return (
<>
{disableSelection === true
? null
: selectionRects.map((position, i) => (
<StyledSelection key={i} style={{ ...selectionStyle, ...position }} $color={selectionColor} />
))}
{disableCaret === true || caretPosition === null ? null : (
<StyledCaret style={{ ...caretPosition, ...style }} $color={caretColor}>
<CaretLabel ref={labelRef} $color={caretColor}>
{navn} ({navIdent})
</CaretLabel>
</StyledCaret>
)}
</>
);
};

interface ColorProps {
$color: string;
}

const StyledSelection = styled.div<ColorProps>`
position: absolute;
z-index: 10;
background-color: ${({ $color }) => $color};
`;

const StyledCaret = styled.div<ColorProps>`
pointer-events: none;
position: absolute;
z-index: 10;
width: 1px;
background-color: ${({ $color }) => $color};
`;

const CaretLabel = styled.div<ColorProps>`
position: absolute;
bottom: 100%;
left: 0;
background-color: ${({ $color }) => $color};
color: white;
font-size: 0.75em;
padding: 0.25em;
border-radius: var(--a-border-radius-medium);
white-space: nowrap;
`;

export const CursorOverlay = (props: CursorOverlayProps<UserCursor>) => {
const { user } = useContext(StaticDataContext);
const { cursors } = useCursorOverlayPositions(props);

return (
<>
{cursors
.filter(({ data }) => data?.navIdent !== user.navIdent)
.map((cursor) => (
<Cursor key={cursor.key as string} {...cursor} />
))}
</>
);
};
69 changes: 65 additions & 4 deletions frontend/src/components/smart-editor/tabbed-editors/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
/* eslint-disable max-lines */
import { ClockDashedIcon } from '@navikt/aksel-icons';
import { skipToken } from '@reduxjs/toolkit/query';
import { relativeRangeToSlateRange } from '@slate-yjs/core';
import { Plate, isCollapsed, isText } from '@udecode/plate-common';
import { CursorState } from '@udecode/plate-cursor';
import { Profiler, useContext, useEffect, useState } from 'react';
import { BasePoint, Path, Range } from 'slate';
import { styled } from 'styled-components';
import { XmlText } from 'yjs';
import { StaticDataContext } from '@app/components/app/static-data-context';
import { NewComment } from '@app/components/smart-editor/comments/new-comment';
import { SmartEditorContext } from '@app/components/smart-editor/context';
import { GodeFormuleringer } from '@app/components/smart-editor/gode-formuleringer/gode-formuleringer';
import { History } from '@app/components/smart-editor/history/history';
import { useCanEditDocument } from '@app/components/smart-editor/hooks/use-can-edit-document';
import { Content } from '@app/components/smart-editor/tabbed-editors/content';
import { CursorOverlay } from '@app/components/smart-editor/tabbed-editors/cursors';
import { CursorOverlay, UserCursor, isYjsCursor } from '@app/components/smart-editor/tabbed-editors/cursors/cursors';
import { PositionedRight } from '@app/components/smart-editor/tabbed-editors/positioned-right';
import { StickyRight } from '@app/components/smart-editor/tabbed-editors/sticky-right';
import { DocumentErrorComponent } from '@app/error-boundary/document-error';
Expand All @@ -25,7 +30,7 @@ import { StatusBar } from '@app/plate/status-bar/status-bar';
import { FloatingSaksbehandlerToolbar } from '@app/plate/toolbar/toolbars/floating-toolbar';
import { SaksbehandlerToolbar } from '@app/plate/toolbar/toolbars/saksbehandler-toolbar';
import { SaksbehandlerTableToolbar } from '@app/plate/toolbar/toolbars/table-toolbar';
import { EditorValue, RichTextEditor } from '@app/plate/types';
import { EditorValue, RichTextEditor, useMyPlateEditorRef } from '@app/plate/types';
import { useGetMySignatureQuery, useGetSignatureQuery } from '@app/redux-api/bruker';
import { useLazyGetDocumentQuery } from '@app/redux-api/oppgaver/queries/documents';
import { ISmartDocument } from '@app/types/documents/documents';
Expand All @@ -38,6 +43,7 @@ export const Editor = ({ smartDocument }: EditorProps) => {
const { id, templateId } = smartDocument;
const [getDocument, { isLoading }] = useLazyGetDocumentQuery();
const { newCommentSelection, showAnnotationsAtOrigin } = useContext(SmartEditorContext);
const { user } = useContext(StaticDataContext);
const canEdit = useCanEditDocument(templateId);
const [showHistory, setShowHistory] = useState(false);
const { data: oppgave } = useOppgave();
Expand Down Expand Up @@ -69,7 +75,7 @@ export const Editor = ({ smartDocument }: EditorProps) => {
initialValue={smartDocument.content}
id={id}
readOnly={!canEdit}
plugins={collaborationSaksbehandlerPlugins(oppgave.id, id, smartDocument)}
plugins={collaborationSaksbehandlerPlugins(oppgave.id, id, smartDocument, user)}
decorate={([node, path]) => {
if (newCommentSelection === null || isCollapsed(newCommentSelection) || !isText(node)) {
return [];
Expand Down Expand Up @@ -144,6 +150,61 @@ const EditorWithNewCommentAndFloatingToolbar = ({ id }: { id: string }) => {
const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null);
const lang = useSmartEditorSpellCheckLanguage();

const editor = useMyPlateEditorRef(id);

type CursorsState = Record<string, CursorState<UserCursor>>;
const [cursors, setCursors] = useState<CursorsState>({});

useEffect(() => {
const onChange = (
{ added, removed, updated }: { added: number[]; removed: number[]; updated: number[] },
doc: unknown,
) => {
console.log('onChange doc', doc);

if (added.length > 0) {
console.log('added', added);
}

if (removed.length > 0) {
console.log('removed', removed);
}

const states = editor.awareness.getStates();

setCursors((cursorsState) => {
const newCursorsState = { ...cursorsState };

for (const a of added.concat(updated)) {
const cursor = states.get(a);

if (isYjsCursor(cursor)) {
newCursorsState[a] = {
...cursor,
selection: relativeRangeToSlateRange(
editor.yjs.provider.document.get('content', XmlText),
editor,
cursor.selection,
),
};
}
}

for (const r of removed) {
delete newCursorsState[r];
}

return newCursorsState;
});
};

editor.awareness.on('change', onChange);

return () => {
editor.awareness.off('change', onChange);
};
}, [editor, editor.awareness]);

useEffect(() => {
setSheetRef(containerElement);
}, [containerElement, setSheetRef]);
Expand All @@ -157,7 +218,7 @@ const EditorWithNewCommentAndFloatingToolbar = ({ id }: { id: string }) => {

<PlateEditor id={id} readOnly={!canEdit} lang={lang} />

<CursorOverlay containerRef={{ current: containerElement }} refreshOnResize />
<CursorOverlay containerRef={{ current: containerElement }} refreshOnResize cursors={cursors} />
</Sheet>
);
};
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/plate/plugins/plugin-sets/saksbehandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { defaultPlugins } from '@app/plate/plugins/plugin-sets/default';
import { createRedigerbarMaltekstPlugin } from '@app/plate/plugins/redigerbar-maltekst';
import { createRegelverkContainerPlugin, createRegelverkPlugin } from '@app/plate/plugins/regelverk';
import { createSignaturePlugin } from '@app/plate/plugins/signature';
import { IUserData } from '@app/types/bruker';
import { ISmartDocument } from '@app/types/documents/documents';

const components: Record<string, PlatePluginComponent> = {
Expand Down Expand Up @@ -112,6 +113,7 @@ export const collaborationSaksbehandlerPlugins = (
behandlingId: string,
dokumentId: string,
smartDocument: ISmartDocument,
{ navIdent, navn }: IUserData,
) => {
const sharedRoot = new Y.XmlText();
sharedRoot.applyDelta(slateNodesToInsertDelta(smartDocument.content));
Expand All @@ -121,6 +123,9 @@ export const collaborationSaksbehandlerPlugins = (
...saksbehandlerPlugins,
createYjsPlugin({
options: {
cursorOptions: {
data: { navIdent, navn },
},
hocuspocusProviderOptions: {
url: `/collaboration/behandlinger/${behandlingId}/dokumenter/${dokumentId}`,
name: dokumentId,
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/plate/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable max-lines */
/* eslint-disable import/no-unused-modules */
import { CursorEditor, YjsEditor } from '@slate-yjs/core';
import { AutoformatRule } from '@udecode/plate-autoformat';
import {
PlateEditor,
Expand All @@ -22,6 +23,7 @@ import {
TTableElement,
TTableRowElement,
} from '@udecode/plate-table';
import { CursorEditorProps, PlateYjsEditorProps } from '@udecode/plate-yjs';
import {
ELEMENT_CURRENT_DATE,
ELEMENT_EMPTY_VOID,
Expand Down Expand Up @@ -275,5 +277,6 @@ export type EditorPlatePlugin<P = PluginOptions> = PlatePlugin<P>;

export type EditorAutoformatRule = AutoformatRule;

export const useMyPlateEditorRef = (id?: PlateId) => useEditorRef<EditorValue, RichTextEditor>(id);
export const useMyPlateEditorRef = (id?: PlateId) =>
useEditorRef<EditorValue, RichTextEditor & CursorEditor & CursorEditorProps & YjsEditor & PlateYjsEditorProps>(id);
export const useMyPlateEditorState = (id?: PlateId) => useEditorState<EditorValue, RichTextEditor>(id);

0 comments on commit 35aab20

Please sign in to comment.