react-native#TextInputSelectionChangeEventData TypeScript Examples

The following examples show how to use react-native#TextInputSelectionChangeEventData. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: mention-input.tsx    From react-native-controlled-mentions with MIT License 4 votes vote down vote up
MentionInput: FC<MentionInputProps> = (
  {
    value,
    onChange,

    partTypes = [],

    inputRef: propInputRef,

    containerStyle,

    onSelectionChange,

    ...textInputProps
  },
) => {
  const textInput = useRef<TextInput | null>(null);

  const [selection, setSelection] = useState({start: 0, end: 0});

  const {
    plainText,
    parts,
  } = useMemo(() => parseValue(value, partTypes), [value, partTypes]);

  const handleSelectionChange = (event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
    setSelection(event.nativeEvent.selection);

    onSelectionChange && onSelectionChange(event);
  };

  /**
   * Callback that trigger on TextInput text change
   *
   * @param changedText
   */
  const onChangeInput = (changedText: string) => {
    onChange(generateValueFromPartsAndChangedText(parts, plainText, changedText));
  };

  /**
   * We memoize the keyword to know should we show mention suggestions or not
   */
  const keywordByTrigger = useMemo(() => {
    return getMentionPartSuggestionKeywords(
      parts,
      plainText,
      selection,
      partTypes,
    );
  }, [parts, plainText, selection, partTypes]);

  /**
   * Callback on mention suggestion press. We should:
   * - Get updated value
   * - Trigger onChange callback with new value
   */
  const onSuggestionPress = (mentionType: MentionPartType) => (suggestion: Suggestion) => {
    const newValue = generateValueWithAddedSuggestion(
      parts,
      mentionType,
      plainText,
      selection,
      suggestion,
    );

    if (!newValue) {
      return;
    }

    onChange(newValue);

    /**
     * Move cursor to the end of just added mention starting from trigger string and including:
     * - Length of trigger string
     * - Length of mention name
     * - Length of space after mention (1)
     *
     * Not working now due to the RN bug
     */
    // const newCursorPosition = currentPart.position.start + triggerPartIndex + trigger.length +
    // suggestion.name.length + 1;

    // textInput.current?.setNativeProps({selection: {start: newCursorPosition, end: newCursorPosition}});
  };

  const handleTextInputRef = (ref: TextInput) => {
    textInput.current = ref as TextInput;

    if (propInputRef) {
      if (typeof propInputRef === 'function') {
        propInputRef(ref);
      } else {
        (propInputRef as MutableRefObject<TextInput>).current = ref as TextInput;
      }
    }
  };

  const renderMentionSuggestions = (mentionType: MentionPartType) => (
    <React.Fragment key={mentionType.trigger}>
      {mentionType.renderSuggestions && mentionType.renderSuggestions({
        keyword: keywordByTrigger[mentionType.trigger],
        onSuggestionPress: onSuggestionPress(mentionType),
      })}
    </React.Fragment>
  );

  return (
    <View style={containerStyle}>
      {(partTypes
        .filter(one => (
          isMentionPartType(one)
          && one.renderSuggestions != null
          && !one.isBottomMentionSuggestionsRender
        )) as MentionPartType[])
        .map(renderMentionSuggestions)
      }

      <TextInput
        multiline

        {...textInputProps}

        ref={handleTextInputRef}

        onChangeText={onChangeInput}
        onSelectionChange={handleSelectionChange}
      >
        <Text>
          {parts.map(({text, partType, data}, index) => partType ? (
            <Text
              key={`${index}-${data?.trigger ?? 'pattern'}`}
              style={partType.textStyle ?? defaultMentionTextStyle}
            >
              {text}
            </Text>
          ) : (
            <Text key={index}>{text}</Text>
          ))}
        </Text>
      </TextInput>

      {(partTypes
        .filter(one => (
          isMentionPartType(one)
          && one.renderSuggestions != null
          && one.isBottomMentionSuggestionsRender
        )) as MentionPartType[])
        .map(renderMentionSuggestions)
      }
    </View>
  );
}
Example #2
Source File: MentionInput.tsx    From lowkey-react-native-mentions-input with MIT License 4 votes vote down vote up
MentionsInput = React.forwardRef(
  (
    {
      suggestedUsersComponent,
      textInputStyle,
      onFocusStateChange = () => {},
      onTextChange = () => {},
      onMarkdownChange = () => {},
      placeholder = 'Write a message...',
      placeholderTextColor,
      multiline,
      textInputTextStyle,
      leftComponent = <></>,
      rightComponent = <></>,
      innerComponent = <></>,
      users,
      ...props
    }: Props,
    ref
  ) => {
    const [isOpen, SetIsOpen] = useState(false);
    const [suggestedUsers, SetSuggesedUsers] = useState<SuggestedUsers[]>([]);
    const [matches, SetMatches] = useState<any[]>([]);
    const [mentions, SetMentions] = useState<any[]>([]);
    const [currentCursorPosition, SetCurrentCursorPosition] = useState(0);

    useEffect(() => {
      if (props.value === '' && (mentions.length > 0 || matches.length > 0)) {
        SetMatches([]);
        SetMentions([]);
        SetCurrentCursorPosition(1);
      }
    }, [matches, mentions, props.value]);

    const transformTag = useCallback((value: string) => {
      return value
        .replace(/\s+/g, '')
        .toLowerCase()
        .normalize('NFD')
        .replace(/[\u0300-\u036f]/g, '');
    }, []);

    const handleSuggestionsOpen = useCallback(
      (values: RegExpMatchArray[], currentCursorPosition: number) => {
        let shouldPresentSuggestions = false;
        let newSuggestedUsers: Array<SuggestedUsers> = [];

        users.map(
          (user, index) =>
            (newSuggestedUsers[index] = {
              ...user,
              startPosition: 0,
            })
        );

        values.map((match) => {
          if (match === null) {
            return;
          }
          const matchStartPosition = match.index;
          if (typeof matchStartPosition === 'undefined') {
            return;
          }
          const matchEndPosition = matchStartPosition + match[0].length;

          if (
            currentCursorPosition > matchStartPosition &&
            currentCursorPosition <= matchEndPosition
          ) {
            shouldPresentSuggestions = true;
            newSuggestedUsers = newSuggestedUsers
              .filter((user) =>
                user.name
                  .toLowerCase()
                  .includes(match[0].substring(1).toLowerCase())
              )
              .map((user) => {
                user.startPosition = matchStartPosition;
                return user;
              });
          }
        });
        const isSameSuggestedUser =
          suggestedUsers.length === newSuggestedUsers.length &&
          suggestedUsers.every(
            (value, index) =>
              value.id === newSuggestedUsers[index].id &&
              value.startPosition == newSuggestedUsers[index].startPosition
          );

        SetIsOpen(shouldPresentSuggestions);
        if (!isSameSuggestedUser) {
          SetSuggesedUsers(newSuggestedUsers);
        }
      },

      [users, suggestedUsers]
    );

    const formatMarkdown = useCallback(
      (markdown: string) => {
        let parseHeadIndex = 0;
        let markdownArray = [];

        if (mentions.length === 0) {
          markdownArray.push({
            type: 'text',
            data: markdown,
          });
        }

        mentions.map((mention, index) => {
          let match = matches.find((m) => {
            return (
              m.index === mention.user.startPosition &&
              m[0] === `@${mention.user.name}`
            );
          });
          if (typeof match === 'undefined') {
            return;
          }
          markdownArray.push({
            type: 'text',
            data: markdown.substring(
              parseHeadIndex,
              mention.user.startPosition
            ),
          });
          markdownArray.push({
            type: 'mention',
            data: `<@${mention.user.name}::${mention.user.id}>`,
          });
          parseHeadIndex =
            mention.user.startPosition + mention.user.name.length + 1;

          if (index === mentions.length - 1) {
            markdownArray.push({
              type: 'text',
              data: markdown.substring(parseHeadIndex, markdown.length),
            });
          }
        });

        markdown = '';

        markdownArray.map((m) => {
          if (m.type === 'text') {
            markdown = markdown + encodeURIComponent(m.data);
          } else if (m.type === 'mention') {
            markdown = markdown + m.data;
          }
        });
        onMarkdownChange(markdown);
      },
      [onMarkdownChange, mentions, matches]
    );

    const handleDelete = useCallback(
      ({ nativeEvent }: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
        if (nativeEvent.key === 'Backspace') {
          mentions.map((mention, index) => {
            const matchStartPosition = mention.user.startPosition;
            const matchEndPosition =
              matchStartPosition + mention.user.name.length + 1;
            if (
              currentCursorPosition > matchStartPosition &&
              currentCursorPosition <= matchEndPosition
            ) {
              const newMentions = mentions;
              newMentions.splice(index, 1);
              SetMentions(newMentions);
            }
          });
        }
      },
      [mentions, currentCursorPosition]
    );

    const onSelectionChange = useCallback(
      ({
        nativeEvent,
      }: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
        if (nativeEvent.selection.start === nativeEvent.selection.end) {
          SetCurrentCursorPosition(nativeEvent.selection.start);
        }
      },
      []
    );

    const handleMentions = useCallback(
      (newText: string, currentCursorPosition: number) => {
        const pattern = PATTERNS.USERNAME_MENTION;

        let newMatches = [...matchAll(newText, pattern)];
        let newMentions = newText.length > 0 ? mentions : [];

        newMentions.map((mention) => {
          const matchStartPosition = mention.user.startPosition;

          if (decodeURI(newText).length - decodeURI(props.value).length > 0) {
            if (
              matchStartPosition + (newText.length - props.value.length) >
                currentCursorPosition &&
              currentCursorPosition !== props.value.length
            ) {
              mention.user.startPosition =
                mention.user.startPosition +
                (newText.length - props.value.length);
            }
          } else {
            if (matchStartPosition >= currentCursorPosition) {
              mention.user.startPosition =
                mention.user.startPosition +
                (newText.length - props.value.length);
            }
          }
          return mention;
        });

        onTextChange(newText);
        formatMarkdown(newText);

        const isSameMatch =
          matches.length === newMatches.length &&
          matches.every((value, index) => value === newMatches[index]);

        SetMentions(newMentions);

        if (!isSameMatch) {
          SetMatches(newMatches);
        }
      },
      [mentions, onTextChange, formatMarkdown, props.value, matches]
    );

    const onChangeText = useCallback(
      (newText: string) => {
        handleMentions(newText, currentCursorPosition);
      },
      [handleMentions, currentCursorPosition]
    );

    const handleAddMentions = useCallback(
      (user: {
        id: number;
        name: string;
        avatar: string;
        startPosition: number;
      }) => {
        const startPosition = user.startPosition;
        const mention = mentions.find(
          (m) => m.user.startPosition === startPosition
        );
        if (mention) {
          return;
        }

        const match = matches.find((m) => m.index === startPosition);
        let newMentions = mentions;
        const userName = transformTag(user.name);
        const newText =
          props.value.substring(0, match.index) +
          `@${userName} ` +
          props.value.substring(
            match.index + match[0].length,
            props.value.length
          );

        newMentions.push({
          user: {
            ...user,
            name: userName,
            startPosition: startPosition,
            test: 1000,
          },
        });
        newMentions.sort((a, b) =>
          a.user.startPosition > b.user.startPosition
            ? 1
            : b.user.startPosition > a.user.startPosition
            ? -1
            : 0
        );

        SetMentions(newMentions);
        SetIsOpen(false);
        const newCursor = match.index + user.name.length + 1;
        SetCurrentCursorPosition(newCursor);
        setTimeout(() => {
          handleMentions(newText, newCursor);
        }, 100);
      },
      [mentions, matches, transformTag, props.value, handleMentions]
    );

    const onFocus = useCallback(() => {
      onFocusStateChange(true);
    }, [onFocusStateChange]);

    const onBlur = useCallback(() => {
      onFocusStateChange(false);
    }, [onFocusStateChange]);

    useEffect(() => {
      formatMarkdown(props.value);
    }, [props.value, formatMarkdown]);

    useEffect(() => {
      let timeout = setTimeout(() => {
        handleSuggestionsOpen(matches, currentCursorPosition);
      }, 100);

      return () => clearTimeout(timeout);
    }, [handleSuggestionsOpen, matches, currentCursorPosition]);

    return (
      <View>
        <View>
          {isOpen && suggestedUsersComponent(suggestedUsers, handleAddMentions)}
          <View style={styles.inputContainerRow}>
            <View>{leftComponent}</View>
            <View style={[textInputStyle, styles.row]}>
              <TextInput
                {...props}
                onFocus={onFocus}
                onBlur={onBlur}
                placeholder={placeholder}
                placeholderTextColor={placeholderTextColor}
                multiline={multiline}
                value={decodeURI(props.value.replace(/%/g, encodeURI('%')))}
                onChangeText={onChangeText}
                onKeyPress={handleDelete}
                style={[
                  textInputTextStyle,
                  styles.flex,
                  { paddingBottom: multiline ? 5 : 0 },
                ]}
                onSelectionChange={onSelectionChange}
                //@ts-ignore
                ref={ref}
              />
              <View style={styles.innerContainer}>{innerComponent}</View>
            </View>
            {rightComponent}
          </View>
        </View>
      </View>
    );
  }
)