Skip to content

Commit

Permalink
feat: improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
ttypic committed Aug 21, 2023
1 parent ff523a9 commit 86c3c46
Show file tree
Hide file tree
Showing 12 changed files with 462 additions and 130 deletions.
307 changes: 249 additions & 58 deletions demo/package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"deploy:production": "npm run build && netlify deploy --prod"
},
"dependencies": {
"@ably-labs/react-hooks": "file:../../react-hooks",
"@ably-labs/react-hooks": "3.0.0-canary.1",
"@ably-labs/spaces": "file:../",
"ably": "^1.2.43",
"classnames": "^2.3.2",
Expand All @@ -24,7 +24,8 @@
"random-words": "^2.0.0",
"react": "^18.2.0",
"react-contenteditable": "^3.3.7",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"sanitize-html": "^2.11.0"
},
"devDependencies": {
"@types/lodash.assign": "^4.2.7",
Expand All @@ -33,6 +34,7 @@
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@types/react-helmet": "^6.1.6",
"@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
Expand Down
68 changes: 68 additions & 0 deletions demo/src/components/EditableText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useCallback, useEffect, useRef } from 'react';
import ContentEditable, { ContentEditableEvent } from 'react-contenteditable';
import * as sanitize from 'sanitize-html';

interface EditableTextProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'children'> {
as: string;
disabled: boolean;
value: string;
onChange(nextValue: string): void;
maxChars?: number;
}

export const EditableText: React.FC<EditableTextProps> = ({
as,
disabled,
maxChars = 300,
value,
onChange,
...restProps
}) => {
const elementRef = useRef<HTMLElement | null>(null);
const handleTextChange = useCallback(
(evt: ContentEditableEvent) => {
const nextValue = sanitize(evt.target.value, {
allowedTags: [],
}).substring(0, maxChars);
onChange(nextValue);
},
[onChange],
);

const handleKeyPress = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
const { key } = event;
const enterPressed = key === 'Enter';
const deleteButtonPressed = key === 'Backspace' || key === 'Delete';
const replacing = window.getSelection()?.toString() !== '';
const limitReached = elementRef.current?.innerText.length ?? 0 >= maxChars;
if (enterPressed || (limitReached && !replacing && !deleteButtonPressed)) {
event.stopPropagation();
event.preventDefault();
return false;
}

return undefined;
},
[maxChars],
);

useEffect(() => {
if (!disabled) {
elementRef.current?.focus();
}
}, [disabled]);

return (
<ContentEditable
tagName={as}
innerRef={elementRef}
disabled={disabled}
html={value}
onKeyDown={handleKeyPress}
onKeyUp={handleKeyPress}
onChange={handleTextChange}
{...restProps}
/>
);
};
16 changes: 9 additions & 7 deletions demo/src/components/Image.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cn from 'classnames';
import { useElementSelect, useMembers, useLockLabelCallback } from '../hooks';
import { useElementSelect, useMembers } from '../hooks';
import { findActiveMember, getMemberFirstName, getOutlineClasses } from '../utils';

interface Props extends React.HTMLAttributes<HTMLImageElement> {
Expand All @@ -8,29 +8,31 @@ interface Props extends React.HTMLAttributes<HTMLImageElement> {
className?: string;
id: string;
slide: string;
locatable?: boolean;
}

export const Image = ({ src, children, className, id, slide }: Props) => {
export const Image = ({ src, children, className, id, slide, locatable = true }: Props) => {
const { members, self } = useMembers();
const { handleSelect } = useElementSelect(id);
const activeMember = findActiveMember(id, slide, members);
const outlineClasses = getOutlineClasses(activeMember);
const label = useLockLabelCallback(slide, id, self?.connectionId) || getMemberFirstName(activeMember);
const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember);
const memberName = getMemberFirstName(activeMember);
const label = self?.connectionId === activeMember?.connectionId ? 'You' : memberName;

return (
<div
data-before={label}
className={cn('relative xs:my-4 md:my-0', className, {
[`outline-2 outline before:content-[attr(data-before)] before:absolute before:-top-[22px] before:-left-[2px] before:px-[10px] before:text-sm before:text-white before:rounded-t-lg before:normal-case ${outlineClasses}`]:
!!activeMember,
[`outline-2 outline before:content-[attr(data-before)] before:absolute before:-top-[22px] before:-left-[2px] before:px-[10px] before:text-sm before:text-white before:rounded-t-lg before:normal-case ${outlineClasses} before:${stickyLabelClasses}`]:
activeMember,
})}
>
<img
id={id}
data-id="slide-image-placeholder"
className="cursor-pointer block"
src={src}
onClick={handleSelect}
onClick={locatable ? handleSelect : undefined}
/>
{children ? children : null}
</div>
Expand Down
50 changes: 30 additions & 20 deletions demo/src/components/Paragraph.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useState } from 'react';
import cn from 'classnames';
import { useElementSelect, useMembers, useLockAndStatus } from '../hooks';
import { findActiveMember, getMemberFirstName, getOutlineClasses } from '../utils';
import { useChannel } from '@ably-labs/react-hooks';
import { useState } from 'react';
import ContentEditable from 'react-contenteditable';
import { useElementSelect, useMembers, useLockStatus } from '../hooks';
import { findActiveMember, getMemberFirstName, getOutlineClasses, getSpaceNameFromUrl } from '../utils';
import { StickyLabel } from './StickyLabel';
import { LockFilledSvg } from './svg/LockedFilled.tsx';
import { EditableText } from './EditableText.tsx';
import { buildLockId } from '../utils/locking.ts';

interface Props extends React.HTMLAttributes<HTMLParagraphElement> {
id: string;
Expand All @@ -13,44 +16,51 @@ interface Props extends React.HTMLAttributes<HTMLParagraphElement> {
}

export const Paragraph = ({ variant = 'regular', id, slide, className, children, ...props }: Props) => {
const spaceName = getSpaceNameFromUrl();
const { members, self } = useMembers();
const { handleSelect } = useElementSelect(id);
const activeMember = findActiveMember(id, slide, members);
const outlineClasses = getOutlineClasses(activeMember);
const { label: lockingLabel, locked, lockedByYou } = useLockAndStatus(slide, id, self?.connectionId);
const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember);
const { locked, lockedByYou } = useLockStatus(slide, id, self?.connectionId);
const memberName = getMemberFirstName(activeMember);
const label = lockingLabel || memberName;
const { channel } = useChannel(`[?rewind=1]title-${id}-changes`, (message) => {
if (message.connectionId === self?.connectionId) return;
const channelName = `[?rewind=1]${spaceName}-${buildLockId(slide, id)}`;
const { channel } = useChannel(channelName, (message) => {
setContent(message.data);
});
const [content, setContent] = useState(children);
const editIsNotAllowed = locked && !lockedByYou && !!activeMember;

return (
<div
{...props}
className="relative"
onClick={handleSelect}
>
<ContentEditable
tag="p"
<StickyLabel
visible={!!activeMember}
className={`${stickyLabelClasses} flex flex-row items-center`}
>
{lockedByYou ? 'You' : memberName}
{locked && !lockedByYou && !!activeMember && <LockFilledSvg className="text-white" />}
</StickyLabel>
<EditableText
as="p"
id={id}
data-before={label}
disabled={!activeMember || !lockedByYou}
html={content}
onChange={(evt) => {
const nextValue = evt.target.value;
value={content}
onChange={(nextValue) => {
setContent(nextValue);
channel.publish('update', nextValue);
}}
className={cn(
'text-ably-avatar-stack-demo-slide-text cursor-pointer relative',
'text-ably-avatar-stack-demo-slide-text break-all',
{
'xs:w-auto text-xs xs:text-base md:text-lg xs:my-4 md:my-0': variant === 'regular',
'text-[13px] p-0 leading-6': variant === 'aside',
[`outline-2 outline before:content-[attr(data-before)] before:absolute before:-top-[22px] before:-left-[2px] before:px-[10px] before:text-sm before:text-white before:rounded-t-lg before:normal-case ${outlineClasses}`]:
!!activeMember,
'cursor-not-allowed': locked && !lockedByYou && !!activeMember,
'bg-slate-200': locked && !lockedByYou && !!activeMember,
[`outline-2 outline ${outlineClasses}`]: !!activeMember,
'cursor-pointer': !editIsNotAllowed,
'cursor-not-allowed': editIsNotAllowed,
'bg-slate-200': editIsNotAllowed,
},
className,
)}
Expand Down
18 changes: 18 additions & 0 deletions demo/src/components/StickyLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';

interface StickyLabelProps {
visible: boolean;
className?: string;
children: React.ReactNode;
}
export const StickyLabel: React.FC<StickyLabelProps> = ({ visible, className, children }) => {
if (!visible) return null;

return (
<div
className={`absolute -top-[22px] -left-[2px] px-[10px] text-sm text-white rounded-t-lg normal-case ${className}`}
>
{children}
</div>
);
};
55 changes: 33 additions & 22 deletions demo/src/components/Title.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React, { useState } from 'react';
import ContentEditable from 'react-contenteditable';
import cn from 'classnames';
import { useChannel } from '@ably-labs/react-hooks';

import { useElementSelect, useLockAndStatus, useMembers } from '../hooks';
import { findActiveMember, getMemberFirstName, getOutlineClasses } from '../utils';
import { useElementSelect, useLockStatus, useMembers } from '../hooks';
import { findActiveMember, getMemberFirstName, getOutlineClasses, getSpaceNameFromUrl } from '../utils';
import { LockFilledSvg } from './svg/LockedFilled.tsx';
import { StickyLabel } from './StickyLabel.tsx';
import { EditableText } from './EditableText.tsx';
import { buildLockId } from '../utils/locking.ts';

interface Props extends React.HTMLAttributes<HTMLHeadingElement> {
id: string;
Expand All @@ -14,49 +17,57 @@ interface Props extends React.HTMLAttributes<HTMLHeadingElement> {
}

export const Title = ({ variant = 'h1', className, id, slide, children, ...props }: Props) => {
const spaceName = getSpaceNameFromUrl();
const { members, self } = useMembers();
const { handleSelect } = useElementSelect(id, true);
const activeMember = findActiveMember(id, slide, members);
const outlineClasses = getOutlineClasses(activeMember);
const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember);
const memberName = getMemberFirstName(activeMember);
const { label: lockingLabel, locked, lockedByYou } = useLockAndStatus(slide, id, self?.connectionId);
const label = lockingLabel || memberName;
const { channel } = useChannel(`[?rewind=1]title-${id}-changes`, (message) => {
if (message.connectionId === self?.connectionId) return;
const { locked, lockedByYou } = useLockStatus(slide, id, self?.connectionId);
const channelName = `[?rewind=1]${spaceName}-${buildLockId(slide, id)}`;
const { channel } = useChannel(channelName, (message) => {
setContent(message.data);
});
const [content, setContent] = useState(children);
const editIsNotAllowed = locked && !lockedByYou && !!activeMember;

return (
<div
{...props}
className="relative"
onClick={handleSelect}
>
<ContentEditable
<StickyLabel
visible={!!activeMember}
className={`${stickyLabelClasses} flex flex-row items-center`}
>
{lockedByYou ? 'You' : memberName}
{locked && !lockedByYou && !!activeMember && <LockFilledSvg className="text-white" />}
</StickyLabel>
<EditableText
id={id}
tagName={variant}
as={variant}
disabled={!activeMember || !lockedByYou}
data-before={label}
html={content}
maxChars={70}
value={content}
onChange={(nextValue) => {
setContent(nextValue);
channel.publish('update', nextValue);
}}
className={cn(
'relative cursor-pointer',
'relative break-all',
{
'font-semibold text-ably-avatar-stack-demo-slide-text my-2 xs:text-3xl md:text-4xl': variant === 'h1',
'font-semibold text-ably-avatar-stack-demo-slide-text md:text-2xl': variant === 'h2',
'font-medium uppercase text-ably-avatar-stack-demo-slide-title-highlight xs:text-xs xs:my-4 md:my-0 md:text-md':
variant === 'h3',
[`outline-2 outline before:content-[attr(data-before)] before:absolute before:-top-[22px] before:-left-[2px] before:px-[10px] before:text-sm before:text-white before:rounded-t-lg before:normal-case ${outlineClasses}`]:
!!activeMember,
'cursor-not-allowed': locked && !lockedByYou && !!activeMember,
'bg-slate-200': locked && !lockedByYou && !!activeMember,
[`outline-2 outline ${outlineClasses}`]: !!activeMember,
'cursor-pointer': !editIsNotAllowed,
'cursor-not-allowed': editIsNotAllowed,
'bg-slate-200': editIsNotAllowed,
},
className,
)}
onChange={(evt) => {
const nextValue = evt.target.value;
setContent(nextValue);
channel.publish('update', nextValue);
}}
/>
</div>
);
Expand Down
Loading

0 comments on commit 86c3c46

Please sign in to comment.