Lexical: Use in ComposeEventModal, style improvements, types
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
2549e72843
commit
b15640603c
17 changed files with 100 additions and 123 deletions
|
@ -35,7 +35,6 @@ const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => {
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
if (onClick) onClick();
|
if (onClick) onClick();
|
||||||
|
|
||||||
|
|
||||||
if (item.to) {
|
if (item.to) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
history.push(item.to);
|
history.push(item.to);
|
||||||
|
|
|
@ -15,7 +15,6 @@ const Portal: React.FC<IPortal> = ({ children }) => {
|
||||||
setIsRendered(true);
|
setIsRendered(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
if (!isRendered) {
|
if (!isRendered) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,7 +99,6 @@ const findElementPosition = (el: HTMLElement) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const getPointerPosition = (el: HTMLElement, event: MouseEvent & TouchEvent): Point => {
|
const getPointerPosition = (el: HTMLElement, event: MouseEvent & TouchEvent): Point => {
|
||||||
const box = findElementPosition(el);
|
const box = findElementPosition(el);
|
||||||
const boxW = el.offsetWidth;
|
const boxW = el.offsetWidth;
|
||||||
|
|
|
@ -11,7 +11,6 @@ import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||||
// @ts-ignore: it doesn't have types
|
// @ts-ignore: it doesn't have types
|
||||||
import { ScrollContext } from 'react-router-scroll-4';
|
import { ScrollContext } from 'react-router-scroll-4';
|
||||||
|
|
||||||
|
|
||||||
import { loadInstance } from 'soapbox/actions/instance';
|
import { loadInstance } from 'soapbox/actions/instance';
|
||||||
import { fetchMe } from 'soapbox/actions/me';
|
import { fetchMe } from 'soapbox/actions/me';
|
||||||
import { loadSoapboxConfig, getSoapboxConfig } from 'soapbox/actions/soapbox';
|
import { loadSoapboxConfig, getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||||
|
|
|
@ -12,5 +12,4 @@ function parseEntitiesPath(expandedPath: ExpandedEntitiesPath) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export { parseEntitiesPath };
|
export { parseEntitiesPath };
|
|
@ -37,7 +37,6 @@ const UserIndex: React.FC = () => {
|
||||||
updateQuery();
|
updateQuery();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const hasMore = items.count() < total && !!next;
|
const hasMore = items.count() < total && !!next;
|
||||||
|
|
||||||
const showLoading = isLoading && items.isEmpty();
|
const showLoading = isLoading && items.isEmpty();
|
||||||
|
|
|
@ -2,7 +2,6 @@ import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||||
|
|
||||||
|
|
||||||
import { ChatContext } from 'soapbox/contexts/chat-context';
|
import { ChatContext } from 'soapbox/contexts/chat-context';
|
||||||
import { normalizeChatMessage, normalizeInstance } from 'soapbox/normalizers';
|
import { normalizeChatMessage, normalizeInstance } from 'soapbox/normalizers';
|
||||||
import { IAccount } from 'soapbox/queries/accounts';
|
import { IAccount } from 'soapbox/queries/accounts';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { defineMessages, FormattedMessage, MessageDescriptor, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import { length } from 'stringz';
|
import { length } from 'stringz';
|
||||||
|
|
||||||
|
@ -45,9 +45,6 @@ import type { Emoji } from 'soapbox/features/emoji';
|
||||||
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What\'s on your mind?' },
|
|
||||||
pollPlaceholder: { id: 'compose_form.poll_placeholder', defaultMessage: 'Add a poll topic…' },
|
|
||||||
eventPlaceholder: { id: 'compose_form.event_placeholder', defaultMessage: 'Post to this event' },
|
|
||||||
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here (optional)' },
|
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here (optional)' },
|
||||||
publish: { id: 'compose_form.publish', defaultMessage: 'Post' },
|
publish: { id: 'compose_form.publish', defaultMessage: 'Post' },
|
||||||
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
||||||
|
@ -74,12 +71,11 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
|
|
||||||
const compose = useCompose(id);
|
const compose = useCompose(id);
|
||||||
const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden);
|
const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden);
|
||||||
const isModalOpen = useAppSelector((state) => !!(state.modals.size && state.modals.last()!.modalType === 'COMPOSE'));
|
|
||||||
const maxTootChars = configuration.getIn(['statuses', 'max_characters']) as number;
|
const maxTootChars = configuration.getIn(['statuses', 'max_characters']) as number;
|
||||||
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
|
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
|
|
||||||
const { text, suggestions, spoiler, spoiler_text: spoilerText, privacy, focusDate, caretPosition, is_submitting: isSubmitting, is_changing_upload: isChangingUpload, is_uploading: isUploading, schedule: scheduledAt, group_id: groupId } = compose;
|
const { text, spoiler, spoiler_text: spoilerText, privacy, focusDate, caretPosition, is_submitting: isSubmitting, is_changing_upload: isChangingUpload, is_uploading: isUploading, schedule: scheduledAt, group_id: groupId } = compose;
|
||||||
const prevSpoiler = usePrevious(spoiler);
|
const prevSpoiler = usePrevious(spoiler);
|
||||||
|
|
||||||
const hasPoll = !!compose.poll;
|
const hasPoll = !!compose.poll;
|
||||||
|
@ -93,17 +89,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
const autosuggestTextareaRef = useRef<AutosuggestTextarea>(null);
|
const autosuggestTextareaRef = useRef<AutosuggestTextarea>(null);
|
||||||
const editorStateRef = useRef<string>(null);
|
const editorStateRef = useRef<string>(null);
|
||||||
|
|
||||||
const handleChange: React.ChangeEventHandler<HTMLTextAreaElement> = (e) => {
|
|
||||||
dispatch(changeCompose(id, e.target.value));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown: React.KeyboardEventHandler = (e) => {
|
|
||||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
|
||||||
handleSubmit();
|
|
||||||
e.preventDefault(); // Prevent bubbling to other ComposeForm instances
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getClickableArea = () => {
|
const getClickableArea = () => {
|
||||||
return clickableAreaRef ? clickableAreaRef.current : formRef.current;
|
return clickableAreaRef ? clickableAreaRef.current : formRef.current;
|
||||||
};
|
};
|
||||||
|
@ -139,11 +124,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
|
|
||||||
const handleSubmit = (e?: React.FormEvent<Element>) => {
|
const handleSubmit = (e?: React.FormEvent<Element>) => {
|
||||||
dispatch(changeCompose(id, editorStateRef.current!));
|
dispatch(changeCompose(id, editorStateRef.current!));
|
||||||
// if (text !== autosuggestTextareaRef.current?.textarea?.value) {
|
|
||||||
// // Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
|
|
||||||
// // Update the state to match the current text
|
|
||||||
// dispatch(changeCompose(id, autosuggestTextareaRef.current!.textarea!.value));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Submit disabled:
|
// Submit disabled:
|
||||||
const fulltext = [spoilerText, countableText(text)].join('');
|
const fulltext = [spoilerText, countableText(text)].join('');
|
||||||
|
@ -167,10 +147,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
dispatch(fetchComposeSuggestions(id, token as string));
|
dispatch(fetchComposeSuggestions(id, token as string));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSuggestionSelected = (tokenStart: number, token: string | null, value: string | undefined) => {
|
|
||||||
if (value) dispatch(selectComposeSuggestion(id, tokenStart, token, value, ['text']));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSpoilerSuggestionSelected = (tokenStart: number, token: string | null, value: AutoSuggestion) => {
|
const onSpoilerSuggestionSelected = (tokenStart: number, token: string | null, value: AutoSuggestion) => {
|
||||||
dispatch(selectComposeSuggestion(id, tokenStart, token, value, ['spoiler_text']));
|
dispatch(selectComposeSuggestion(id, tokenStart, token, value, ['spoiler_text']));
|
||||||
};
|
};
|
||||||
|
@ -245,7 +221,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
|
|
||||||
let publishText: string | JSX.Element = '';
|
let publishText: string | JSX.Element = '';
|
||||||
let publishIcon: string | undefined = undefined;
|
let publishIcon: string | undefined = undefined;
|
||||||
let textareaPlaceholder: MessageDescriptor;
|
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
publishText = intl.formatMessage(messages.saveChanges);
|
publishText = intl.formatMessage(messages.saveChanges);
|
||||||
|
@ -263,14 +238,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
publishText = intl.formatMessage(messages.schedule);
|
publishText = intl.formatMessage(messages.schedule);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event) {
|
|
||||||
textareaPlaceholder = messages.eventPlaceholder;
|
|
||||||
} else if (hasPoll) {
|
|
||||||
textareaPlaceholder = messages.pollPlaceholder;
|
|
||||||
} else {
|
|
||||||
textareaPlaceholder = messages.placeholder;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
|
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
|
||||||
{scheduledStatusCount > 0 && !event && !group && (
|
{scheduledStatusCount > 0 && !event && !group && (
|
||||||
|
@ -301,10 +268,14 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
<div>
|
<div>
|
||||||
<ComposeEditor
|
<ComposeEditor
|
||||||
ref={editorStateRef}
|
ref={editorStateRef}
|
||||||
condensed={condensed}
|
className='my-2'
|
||||||
onFocus={handleComposeFocus}
|
|
||||||
autoFocus={shouldAutoFocus}
|
|
||||||
composeId={id}
|
composeId={id}
|
||||||
|
condensed={condensed}
|
||||||
|
eventDiscussion={!!event}
|
||||||
|
autoFocus={shouldAutoFocus}
|
||||||
|
hasPoll={hasPoll}
|
||||||
|
onFocus={handleComposeFocus}
|
||||||
|
onPaste={onPaste}
|
||||||
/>
|
/>
|
||||||
{!condensed && (
|
{!condensed && (
|
||||||
<Stack space={4} className='compose-form__modifiers'>
|
<Stack space={4} className='compose-form__modifiers'>
|
||||||
|
@ -323,26 +294,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AutosuggestTextarea
|
|
||||||
ref={(isModalOpen && shouldCondense) ? undefined : autosuggestTextareaRef}
|
|
||||||
placeholder={intl.formatMessage(textareaPlaceholder)}
|
|
||||||
disabled={disabled}
|
|
||||||
value={text}
|
|
||||||
onChange={handleChange}
|
|
||||||
suggestions={suggestions}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onFocus={handleComposeFocus}
|
|
||||||
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
|
|
||||||
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
|
||||||
onSuggestionSelected={onSuggestionSelected}
|
|
||||||
onPaste={onPaste}
|
|
||||||
autoFocus={shouldAutoFocus}
|
|
||||||
condensed={condensed}
|
|
||||||
id='compose-textarea'
|
|
||||||
>
|
|
||||||
<></>
|
|
||||||
</AutosuggestTextarea>
|
|
||||||
|
|
||||||
<QuotedStatusContainer composeId={id} />
|
<QuotedStatusContainer composeId={id} />
|
||||||
|
|
||||||
{extra && <div className={clsx({ 'hidden': condensed })}>{extra}</div>}
|
{extra && <div className={clsx({ 'hidden': condensed })}>{extra}</div>}
|
||||||
|
|
|
@ -46,7 +46,29 @@ const StatePlugin = ({ composeId }: { composeId: string }) => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ComposeEditor = React.forwardRef<string, any>(({ composeId, condensed, onFocus, autoFocus }, editorStateRef) => {
|
interface IComposeEditor {
|
||||||
|
className?: string
|
||||||
|
composeId: string
|
||||||
|
condensed?: boolean
|
||||||
|
eventDiscussion?: boolean
|
||||||
|
hasPoll?: boolean
|
||||||
|
autoFocus?: boolean
|
||||||
|
onFocus?: React.FocusEventHandler<HTMLDivElement>
|
||||||
|
onPaste?: (files: FileList) => void
|
||||||
|
placeholder?: JSX.Element | string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
|
||||||
|
className,
|
||||||
|
composeId,
|
||||||
|
condensed,
|
||||||
|
eventDiscussion,
|
||||||
|
hasPoll,
|
||||||
|
autoFocus,
|
||||||
|
onFocus,
|
||||||
|
onPaste,
|
||||||
|
placeholder,
|
||||||
|
}, editorStateRef) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
|
|
||||||
|
@ -111,14 +133,29 @@ const ComposeEditor = React.forwardRef<string, any>(({ composeId, condensed, onF
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePaste: React.ClipboardEventHandler<HTMLDivElement> = (e) => {
|
||||||
|
if (onPaste && e.clipboardData && e.clipboardData.files.length === 1) {
|
||||||
|
onPaste(e.clipboardData.files);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let textareaPlaceholder = placeholder || <FormattedMessage id='compose_form.placeholder' defaultMessage="What's on your mind?" />;
|
||||||
|
|
||||||
|
if (eventDiscussion) {
|
||||||
|
textareaPlaceholder = <FormattedMessage id='compose_form.event_placeholder' defaultMessage='Post to this event' />;
|
||||||
|
} else if (hasPoll) {
|
||||||
|
textareaPlaceholder = <FormattedMessage id='compose_form.poll_placeholder' defaultMessage='Add a poll topic…' />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LexicalComposer initialConfig={initialConfig}>
|
<LexicalComposer initialConfig={initialConfig}>
|
||||||
<div className='lexical relative' data-markup>
|
<div className={clsx('lexical relative', className)} data-markup>
|
||||||
<RichTextPlugin
|
<RichTextPlugin
|
||||||
contentEditable={
|
contentEditable={
|
||||||
<div className='editor' ref={onRef} onFocus={onFocus}>
|
<div className='editor' ref={onRef} onFocus={onFocus} onPaste={handlePaste}>
|
||||||
<ContentEditable
|
<ContentEditable
|
||||||
className={clsx('mr-4 py-2 outline-none transition-[min-height] motion-reduce:transition-none', {
|
className={clsx('mr-4 outline-none transition-[min-height] motion-reduce:transition-none', {
|
||||||
'min-fh-[40px]': condensed,
|
'min-fh-[40px]': condensed,
|
||||||
'min-h-[100px]': !condensed,
|
'min-h-[100px]': !condensed,
|
||||||
})}
|
})}
|
||||||
|
@ -127,8 +164,10 @@ const ComposeEditor = React.forwardRef<string, any>(({ composeId, condensed, onF
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
placeholder={(
|
placeholder={(
|
||||||
<div className='pointer-events-none absolute top-2 select-none text-gray-600 dark:placeholder:text-gray-600'>
|
<div
|
||||||
<FormattedMessage id='compose_form.placeholder' defaultMessage="What's on your mind" />
|
className='pointer-events-none absolute top-0 select-none text-gray-600 dark:placeholder:text-gray-600'
|
||||||
|
>
|
||||||
|
{textareaPlaceholder}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
ErrorBoundary={LexicalErrorBoundary}
|
ErrorBoundary={LexicalErrorBoundary}
|
||||||
|
|
|
@ -51,7 +51,7 @@ export type MenuRenderFn = (
|
||||||
anchorElementRef: MutableRefObject<HTMLElement | null>,
|
anchorElementRef: MutableRefObject<HTMLElement | null>,
|
||||||
) => ReactPortal | JSX.Element | null;
|
) => ReactPortal | JSX.Element | null;
|
||||||
|
|
||||||
function tryToPositionRange(leadOffset: number, range: Range): boolean {
|
const tryToPositionRange = (leadOffset: number, range: Range): boolean => {
|
||||||
const domSelection = window.getSelection();
|
const domSelection = window.getSelection();
|
||||||
if (domSelection === null || !domSelection.isCollapsed) {
|
if (domSelection === null || !domSelection.isCollapsed) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -72,12 +72,12 @@ function tryToPositionRange(leadOffset: number, range: Range): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
function isSelectionOnEntityBoundary(
|
const isSelectionOnEntityBoundary = (
|
||||||
editor: LexicalEditor,
|
editor: LexicalEditor,
|
||||||
offset: number,
|
offset: number,
|
||||||
): boolean {
|
): boolean => {
|
||||||
if (offset !== 0) {
|
if (offset !== 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -91,21 +91,21 @@ function isSelectionOnEntityBoundary(
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
function startTransition(callback: () => void) {
|
const startTransition = (callback: () => void) => {
|
||||||
if (React.startTransition) {
|
if (React.startTransition) {
|
||||||
React.startTransition(callback);
|
React.startTransition(callback);
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Got from https://stackoverflow.com/a/42543908/2013580
|
// Got from https://stackoverflow.com/a/42543908/2013580
|
||||||
export function getScrollParent(
|
export const getScrollParent = (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
includeHidden: boolean,
|
includeHidden: boolean,
|
||||||
): HTMLElement | HTMLBodyElement {
|
): HTMLElement | HTMLBodyElement => {
|
||||||
let style = getComputedStyle(element);
|
let style = getComputedStyle(element);
|
||||||
const excludeStaticParent = style.position === 'absolute';
|
const excludeStaticParent = style.position === 'absolute';
|
||||||
const overflowRegex = includeHidden
|
const overflowRegex = includeHidden
|
||||||
|
@ -130,24 +130,24 @@ export function getScrollParent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return document.body;
|
return document.body;
|
||||||
}
|
};
|
||||||
|
|
||||||
function isTriggerVisibleInNearestScrollContainer(
|
const isTriggerVisibleInNearestScrollContainer = (
|
||||||
targetElement: HTMLElement,
|
targetElement: HTMLElement,
|
||||||
containerElement: HTMLElement,
|
containerElement: HTMLElement,
|
||||||
): boolean {
|
): boolean => {
|
||||||
const tRect = targetElement.getBoundingClientRect();
|
const tRect = targetElement.getBoundingClientRect();
|
||||||
const cRect = containerElement.getBoundingClientRect();
|
const cRect = containerElement.getBoundingClientRect();
|
||||||
return tRect.top > cRect.top && tRect.top < cRect.bottom;
|
return tRect.top > cRect.top && tRect.top < cRect.bottom;
|
||||||
}
|
};
|
||||||
|
|
||||||
// Reposition the menu on scroll, window resize, and element resize.
|
// Reposition the menu on scroll, window resize, and element resize.
|
||||||
export function useDynamicPositioning(
|
export const useDynamicPositioning = (
|
||||||
resolution: Resolution | null,
|
resolution: Resolution | null,
|
||||||
targetElement: HTMLElement | null,
|
targetElement: HTMLElement | null,
|
||||||
onReposition: () => void,
|
onReposition: () => void,
|
||||||
onVisibilityChange?: (isInView: boolean) => void,
|
onVisibilityChange?: (isInView: boolean) => void,
|
||||||
) {
|
) => {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (targetElement && resolution) {
|
if (targetElement && resolution) {
|
||||||
|
@ -161,9 +161,9 @@ export function useDynamicPositioning(
|
||||||
targetElement,
|
targetElement,
|
||||||
rootScrollParent,
|
rootScrollParent,
|
||||||
);
|
);
|
||||||
const handleScroll = function () {
|
const handleScroll = () => {
|
||||||
if (!ticking) {
|
if (!ticking) {
|
||||||
window.requestAnimationFrame(function () {
|
window.requestAnimationFrame(() => {
|
||||||
onReposition();
|
onReposition();
|
||||||
ticking = false;
|
ticking = false;
|
||||||
});
|
});
|
||||||
|
@ -194,22 +194,17 @@ export function useDynamicPositioning(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [targetElement, editor, onVisibilityChange, onReposition, resolution]);
|
}, [targetElement, editor, onVisibilityChange, onReposition, resolution]);
|
||||||
}
|
};
|
||||||
|
|
||||||
function LexicalPopoverMenu({
|
const LexicalPopoverMenu = ({ anchorElementRef, menuRenderFn }: {
|
||||||
anchorElementRef,
|
|
||||||
menuRenderFn,
|
|
||||||
}: {
|
|
||||||
anchorElementRef: MutableRefObject<HTMLElement>
|
anchorElementRef: MutableRefObject<HTMLElement>
|
||||||
menuRenderFn: MenuRenderFn
|
menuRenderFn: MenuRenderFn
|
||||||
}): JSX.Element | null {
|
}): JSX.Element | null => menuRenderFn(anchorElementRef);
|
||||||
return menuRenderFn(anchorElementRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useMenuAnchorRef(
|
const useMenuAnchorRef = (
|
||||||
resolution: Resolution | null,
|
resolution: Resolution | null,
|
||||||
setResolution: (r: Resolution | null) => void,
|
setResolution: (r: Resolution | null) => void,
|
||||||
): MutableRefObject<HTMLElement> {
|
): MutableRefObject<HTMLElement> => {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
const anchorElementRef = useRef<HTMLElement>(document.createElement('div'));
|
const anchorElementRef = useRef<HTMLElement>(document.createElement('div'));
|
||||||
const positionMenu = useCallback(() => {
|
const positionMenu = useCallback(() => {
|
||||||
|
@ -272,7 +267,7 @@ function useMenuAnchorRef(
|
||||||
);
|
);
|
||||||
|
|
||||||
return anchorElementRef;
|
return anchorElementRef;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type AutosuggestPluginProps = {
|
export type AutosuggestPluginProps = {
|
||||||
composeId: string
|
composeId: string
|
||||||
|
@ -280,11 +275,11 @@ export type AutosuggestPluginProps = {
|
||||||
setSuggestionsHidden: (value: boolean) => void
|
setSuggestionsHidden: (value: boolean) => void
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AutosuggestPlugin({
|
export const AutosuggestPlugin = ({
|
||||||
composeId,
|
composeId,
|
||||||
suggestionsHidden,
|
suggestionsHidden,
|
||||||
setSuggestionsHidden,
|
setSuggestionsHidden,
|
||||||
}: AutosuggestPluginProps): JSX.Element | null {
|
}: AutosuggestPluginProps): JSX.Element | null => {
|
||||||
const { suggestions } = useCompose(composeId);
|
const { suggestions } = useCompose(composeId);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
@ -473,4 +468,4 @@ export function AutosuggestPlugin({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import { $createMentionNode, MentionNode } from '../nodes/mention-node';
|
import { $createMentionNode, MentionNode } from '../nodes/mention-node';
|
||||||
|
|
||||||
|
|
||||||
import type { TextNode } from 'lexical';
|
import type { TextNode } from 'lexical';
|
||||||
|
|
||||||
export const MENTION_REGEX = new RegExp('(^|$|(?:^|\\s))([@])([a-z\\d_-]+(?:@[^@\\s]+)?)', 'i');
|
export const MENTION_REGEX = new RegExp('(^|$|(?:^|\\s))([@])([a-z\\d_-]+(?:@[^@\\s]+)?)', 'i');
|
||||||
|
|
|
@ -75,5 +75,4 @@ const EmojiPickerDropdownContainer = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default EmojiPickerDropdownContainer;
|
export default EmojiPickerDropdownContainer;
|
||||||
|
|
|
@ -11,7 +11,6 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||||
|
|
||||||
|
|
||||||
interface IIconPickerMenu {
|
interface IIconPickerMenu {
|
||||||
icons: Record<string, Array<string>>
|
icons: Record<string, Array<string>>
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -21,9 +21,10 @@ import { closeModal, openModal } from 'soapbox/actions/modals';
|
||||||
import { ADDRESS_ICONS } from 'soapbox/components/autosuggest-location';
|
import { ADDRESS_ICONS } from 'soapbox/components/autosuggest-location';
|
||||||
import LocationSearch from 'soapbox/components/location-search';
|
import LocationSearch from 'soapbox/components/location-search';
|
||||||
import { checkEventComposeContent } from 'soapbox/components/modal-root';
|
import { checkEventComposeContent } from 'soapbox/components/modal-root';
|
||||||
import { Button, Form, FormGroup, HStack, Icon, IconButton, Input, Modal, Spinner, Stack, Tabs, Text, Textarea, Toggle } from 'soapbox/components/ui';
|
import { Button, Form, FormGroup, HStack, Icon, IconButton, Input, Modal, Spinner, Stack, Tabs, Text, Toggle } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account-container';
|
import AccountContainer from 'soapbox/containers/account-container';
|
||||||
import { isCurrentOrFutureDate } from 'soapbox/features/compose/components/schedule-form';
|
import { isCurrentOrFutureDate } from 'soapbox/features/compose/components/schedule-form';
|
||||||
|
import ComposeEditor from 'soapbox/features/compose/editor';
|
||||||
import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
|
import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
|
||||||
import { DatePicker } from 'soapbox/features/ui/util/async-components';
|
import { DatePicker } from 'soapbox/features/ui/util/async-components';
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
@ -94,13 +95,14 @@ const ComposeEventModal: React.FC<IComposeEventModal> = ({ onClose }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const editorStateRef = useRef<string>(null);
|
||||||
|
|
||||||
const [tab, setTab] = useState<'edit' | 'pending'>('edit');
|
const [tab, setTab] = useState<'edit' | 'pending'>('edit');
|
||||||
|
|
||||||
const banner = useAppSelector((state) => state.compose_event.banner);
|
const banner = useAppSelector((state) => state.compose_event.banner);
|
||||||
const isUploading = useAppSelector((state) => state.compose_event.is_uploading);
|
const isUploading = useAppSelector((state) => state.compose_event.is_uploading);
|
||||||
|
|
||||||
const name = useAppSelector((state) => state.compose_event.name);
|
const name = useAppSelector((state) => state.compose_event.name);
|
||||||
const description = useAppSelector((state) => state.compose_event.status);
|
|
||||||
const startTime = useAppSelector((state) => state.compose_event.start_time);
|
const startTime = useAppSelector((state) => state.compose_event.start_time);
|
||||||
const endTime = useAppSelector((state) => state.compose_event.end_time);
|
const endTime = useAppSelector((state) => state.compose_event.end_time);
|
||||||
const approvalRequired = useAppSelector((state) => state.compose_event.approval_required);
|
const approvalRequired = useAppSelector((state) => state.compose_event.approval_required);
|
||||||
|
@ -114,10 +116,6 @@ const ComposeEventModal: React.FC<IComposeEventModal> = ({ onClose }) => {
|
||||||
dispatch(changeEditEventName(target.value));
|
dispatch(changeEditEventName(target.value));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChangeDescription: React.ChangeEventHandler<HTMLTextAreaElement> = ({ target }) => {
|
|
||||||
dispatch(changeEditEventDescription(target.value));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onChangeStartTime = (date: Date) => {
|
const onChangeStartTime = (date: Date) => {
|
||||||
dispatch(changeEditEventStartTime(date));
|
dispatch(changeEditEventStartTime(date));
|
||||||
};
|
};
|
||||||
|
@ -170,6 +168,7 @@ const ComposeEventModal: React.FC<IComposeEventModal> = ({ onClose }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
|
dispatch(changeEditEventDescription(editorStateRef.current!));
|
||||||
dispatch(submitEvent());
|
dispatch(submitEvent());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -238,11 +237,11 @@ const ComposeEventModal: React.FC<IComposeEventModal> = ({ onClose }) => {
|
||||||
labelText={<FormattedMessage id='compose_event.fields.description_label' defaultMessage='Event description' />}
|
labelText={<FormattedMessage id='compose_event.fields.description_label' defaultMessage='Event description' />}
|
||||||
hintText={<FormattedMessage id='compose_event.fields.description_hint' defaultMessage='Markdown syntax is supported' />}
|
hintText={<FormattedMessage id='compose_event.fields.description_hint' defaultMessage='Markdown syntax is supported' />}
|
||||||
>
|
>
|
||||||
<Textarea
|
<ComposeEditor
|
||||||
autoComplete='off'
|
ref={editorStateRef}
|
||||||
|
className='block w-full rounded-md border-gray-400 bg-white px-3 py-2 text-base text-gray-900 placeholder:text-gray-600 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:placeholder:text-gray-600 dark:focus:border-primary-500 dark:focus:ring-primary-500 sm:text-sm'
|
||||||
|
composeId='compose-event-modal'
|
||||||
placeholder={intl.formatMessage(messages.eventDescriptionPlaceholder)}
|
placeholder={intl.formatMessage(messages.eventDescriptionPlaceholder)}
|
||||||
value={description}
|
|
||||||
onChange={onChangeDescription}
|
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
|
|
|
@ -135,7 +135,6 @@ const normalizeLocked = (group: ImmutableMap<string, any>) => {
|
||||||
return group.set('locked', locked);
|
return group.set('locked', locked);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/** Rewrite `<p></p>` to empty string. */
|
/** Rewrite `<p></p>` to empty string. */
|
||||||
const fixNote = (group: ImmutableMap<string, any>) => {
|
const fixNote = (group: ImmutableMap<string, any>) => {
|
||||||
if (group.get('note') === '<p></p>') {
|
if (group.get('note') === '<p></p>') {
|
||||||
|
|
|
@ -54,6 +54,7 @@ import {
|
||||||
COMPOSE_EDITOR_STATE_SET,
|
COMPOSE_EDITOR_STATE_SET,
|
||||||
COMPOSE_SET_GROUP_TIMELINE_VISIBLE,
|
COMPOSE_SET_GROUP_TIMELINE_VISIBLE,
|
||||||
} from '../actions/compose';
|
} from '../actions/compose';
|
||||||
|
import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET } from '../actions/events';
|
||||||
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from '../actions/me';
|
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from '../actions/me';
|
||||||
import { SETTING_CHANGE, FE_NAME } from '../actions/settings';
|
import { SETTING_CHANGE, FE_NAME } from '../actions/settings';
|
||||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||||
|
@ -507,6 +508,10 @@ export default function compose(state = initialState, action: AnyAction) {
|
||||||
return updateCompose(state, 'default', compose => updateSetting(compose, action.path, action.value));
|
return updateCompose(state, 'default', compose => updateSetting(compose, action.path, action.value));
|
||||||
case COMPOSE_EDITOR_STATE_SET:
|
case COMPOSE_EDITOR_STATE_SET:
|
||||||
return updateCompose(state, action.id, compose => compose.set('editorState', action.editorState));
|
return updateCompose(state, action.id, compose => compose.set('editorState', action.editorState));
|
||||||
|
case EVENT_COMPOSE_CANCEL:
|
||||||
|
return updateCompose(state, 'event-compose-modal', compose => compose.set('text', ''));
|
||||||
|
case EVENT_FORM_SET:
|
||||||
|
return updateCompose(state, 'event-compose-modal', compose => compose.set('text', action.text));
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ const getScopes = (state: RootState) => {
|
||||||
return getInstanceScopes(state.instance);
|
return getInstanceScopes(state.instance);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getInstanceScopes,
|
getInstanceScopes,
|
||||||
getScopes,
|
getScopes,
|
||||||
|
|
Loading…
Reference in a new issue