zenoamaro / react-quill

A Quill component for React.
https://zenoamaro.github.io/react-quill
MIT License
6.79k stars 926 forks source link

Difficulty Inserting HTML into Quill React Component #951

Open kamruzzaman-gain opened 9 months ago

kamruzzaman-gain commented 9 months ago

Issue Summary:

Unable to insert html into Quill React Component.

Environment:

React version: "18.2.0" react-quill: "2.0.0" next:"14.1.0"

Code snippet:

My Component file:

'use client';

import { useLazyQuery } from '@apollo/client';
import dynamic from 'next/dynamic';
import React, { useRef, useEffect, useState } from 'react';

import { getUserFullName, toastAlert } from '@/utils';

import 'react-quill/dist/quill.snow.css';

import { GET_ORGANIZATION_USERS } from '@/components/user-and-roles/graphql/queries/getOrganizationUsers.gql';
import { useSelector } from 'react-redux';
import { size } from 'lodash';
import { useDebounce } from '@/helpers/hooks';

const loadReactQuill = async () => {
  const { default: RQ } = await import('react-quill');
  const reactQuillWithRef = ({ forwardedRef, ...props }) => <RQ ref={forwardedRef} {...props} />;

  return reactQuillWithRef;
};

const ReactQuill = dynamic(loadReactQuill, {
  ssr: false
});

const AppEditor = ({
  readOnly = false,
  onChange,
  allowMention = false,
  value = '',
  placeholder = '',
  toolbarBottom = false,
  setMentionedIds = []
}) => {
  const editorRef = useRef(null);
  const quillContainerRef = useRef(null);
  const [showMentionList, setShowMentionList] = useState(false);
  const [mentionListPosition, setMentionListPosition] = useState({
    top: 0,
    left: 0
  });
  const [cursorIndex, setCursorIndex] = useState({});
  const userData = useSelector((state) => state.auth.user);

  const [agentsState, setAgentsState] = useState({
    data: [],
    metaData: {},
    loading: true,
    loaded: false,
    searchKeyword: '',
    optionData: {
      limit: 50,
      offset: 0,
      order: [['created_at', 'desc']]
    }
  });

  const [getAgents] = useLazyQuery(GET_ORGANIZATION_USERS, {
    errorPolicy: 'all',
    fetchPolicy: 'no-cache',
    variables: {
      queryData: { roleName: 'org_agent', search_keyword: agentsState.searchKeyword },
      optionData: agentsState.optionData
    },

    onCompleted: (data) => {
      const currentUserId = userData?.user_id;

      const preparedData =
        data?.getOrganizationUsers?.data?.map((org_user) => {
          let title =
            org_user?.user?.first_name || org_user?.user.last_name
              ? getUserFullName(org_user?.user?.first_name, org_user?.user?.last_name)
              : org_user?.user?.email;

          if (org_user?.user?.id === currentUserId) {
            title += ' (You)';
          }
          return { ...org_user, key: org_user?.id, title };
        }) || [];
      setAgentsState((prevState) => ({
        ...prevState,
        data:
          agentsState.optionData.offset === 0 ? preparedData : [...prevState.data, ...preparedData],
        metaData: data?.getOrganizationUsers?.meta_data || {},
        loading: false,
        loaded: true
      }));
    },
    onError: (error) => {
      console.error(error);
      setAgentsState((prevState) => ({ ...prevState, loading: false }));
    }
  });

  const updateQueryData = useDebounce((value) => {
    setAgentsState((prev) => ({
      ...prev,
      loading: true,
      searchKeyword: value,
      data: [],
      optionData: { ...prev.optionData, offset: 0 },
      metaData: {}
    }));
  }, 500);

  const handleContentChange = (content, delta, source, editor) => {
    if (allowMention && source === 'user') {
      const cursorPosition = editor.getSelection()?.index;
      if (cursorPosition) {
        const textBeforeCursor = editor.getText(0, cursorPosition);
        const atIndex = textBeforeCursor.lastIndexOf('@');
        const spaceIndex = textBeforeCursor.indexOf(' ', atIndex);
        if (atIndex > -1 && spaceIndex < 0) {
          const wordEnd = textBeforeCursor.length;
          const searchTerm = textBeforeCursor.substring(atIndex + 1, wordEnd);
          setCursorIndex({ startText: atIndex, endText: wordEnd });
          updateQueryData(searchTerm);
          setShowMentionList(true);
          const bounds = editor.getBounds(cursorPosition);
          setMentionListPosition({ top: bounds.bottom, left: bounds.left });
        } else {
          setShowMentionList(false);
        }
      }
    }
    onChange(content);
  };

  const handleSelectMention = (mention) => {
    setMentionedIds((prev) => [...prev, mention.id]);
    const editor = editorRef.current.getEditor();
    const range = editor.getSelection(true);
    if (range) {
      updateQueryData('');
      editor.deleteText(cursorIndex.startText, cursorIndex.endText);
      editor.clipboard.dangerouslyPasteHTML(
        cursorIndex.startText,
        `<p><strong id='${mention.id}'>@${mention.title}</strong></p>`
      );
      setShowMentionList(false);

      onChange(editor.root.innerHTML);
    }
  };
  useEffect(() => {
    const handleClickOutside = (event) => {
      if (quillContainerRef.current && !quillContainerRef.current.contains(event.target)) {
        setShowMentionList(false);
      }
    };

    document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, []);

  useEffect(() => {
    if (editorRef.current) {
      const editor = editorRef.current.getEditor();
      if (editor && editor.container) {
        quillContainerRef.current = editor.container;
      }
    }
  }, [editorRef]);

  useEffect(() => {
    getAgents();
  }, []);

  const modules = {
    toolbar: {
      container: [
        [{ header: [1, 2, false] }],
        ['bold', 'italic', 'underline', 'strike', 'blockquote'],
        [({ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' })],
        ['link', 'image', 'video']
      ]
    }
  };

  const formats = [
    'header',
    'bold',
    'italic',
    'underline',
    'strike',
    'blockquote',
    'list',
    'bullet',
    'indent',
    'link',
    'image',
    'video'
  ];

  const editorClassName = toolbarBottom ? 'editor-toolbar-bottom' : '';

  return (
    <div className={`react-quill-editor ${editorClassName} relative`}>
      <ReactQuill
        forwardedRef={editorRef}
        theme="snow"
        value={value}
        onChange={handleContentChange}
        readOnly={readOnly}
        modules={modules}
        formats={formats}
        placeholder={placeholder}
      />
      {showMentionList && (
        <div
          className="absolute z-10 mt-1 bg-white rounded border border-gray-300 shadow-lg"
          style={{
            top: mentionListPosition.top,
            left: mentionListPosition.left
          }}
        >
          {size(agentsState.data) > 0 &&
            agentsState.data.map((mention) => (
              <div
                key={mention.id}
                className="px-4 py-2 cursor-pointer hover:bg-blue-100"
                onMouseDown={(e) => {
                  e.preventDefault();
                  handleSelectMention(mention);
                }}
              >
                {mention.title}
              </div>
            ))}
        </div>
      )}
    </div>
  );
};

export default AppEditor;

I have alos tried with parchment package but this also not work.

Here the Code sinppet

const loadReactQuill = async () => {
  const { default: RQ } = await import('react-quill');

  const Parchment = RQ.Quill.import('parchment');
  const Block = Parchment.query('block');

  class ButtonBlot extends Block {
    static create(value) {
      const node = super.create(value);

      node.setAttribute('class', 'button-class');
      node.setAttribute('style', 'button-style');

      return node;
    }
  }

  ButtonBlot.blotName = 'button';
  ButtonBlot.tagName = 'button';

  RQ.Quill.register(ButtonBlot);

  const reactQuillWithRef = ({ forwardedRef, ...props }) => <RQ ref={forwardedRef} {...props} />;

  return reactQuillWithRef;
};
goodafteryoon commented 7 months ago

Did you solve this problem?