Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Textarea and Input support #13

Open
slowshi opened this issue Oct 18, 2023 · 3 comments
Open

Textarea and Input support #13

slowshi opened this issue Oct 18, 2023 · 3 comments

Comments

@slowshi
Copy link

slowshi commented Oct 18, 2023

Textarea and Input support

Overview

<textarea> and <input> text is not supported at the moment. This would be good for rich text editor support. I'm currently using this code locally to make it work but there could be another solution that doesn't involve adding and removing a div to get the coordinates.

      const getDummyRect = (el: HTMLTextAreaElement | HTMLInputElement): DOMRect => {
        const dummyDiv = document.createElement("div");
        dummyDiv.textContent = el.value;
        var computedStyle = window.getComputedStyle(el);
        Array.from(computedStyle).forEach(function (key) {
          return dummyDiv.style.setProperty(key, computedStyle.getPropertyValue(key), computedStyle.getPropertyPriority(key));
        });
        dummyDiv.style.position = "absolute";
        dummyDiv.style.visibility = "hidden";
        const textareaRect = el.getBoundingClientRect();
      
        const scrolledTop = textareaRect.top + window.scrollY;
        const scrolledLeft = textareaRect.left + window.scrollX;
        dummyDiv.style.top = `${scrolledTop}px`;
        dummyDiv.style.left = `${scrolledLeft}px`;
        dummyDiv.style.width = `${textareaRect.width}px`;
        dummyDiv.style.height = `${textareaRect.height}px`;
      
        document.body.appendChild(dummyDiv);
        const range = document.createRange();
        range.setStart(dummyDiv.firstChild!, el.selectionStart || 0);
        range.setEnd(dummyDiv.firstChild!, el.selectionEnd || 0);
      
        const rect = range.getBoundingClientRect();
        document.body.removeChild(dummyDiv);
      
        return rect;
      }

      const handleSelection = () => {
        if (pointerTypeRef.current !== 'mouse') return
        const selection = document.getSelection()
        if (!selection) return
        const node = ref.current
        const wasSelectionInsideTrigger = node?.contains(selection.anchorNode)
        if (!wasSelectionInsideTrigger) {
          hasOpenedRef.current = false
          return
        }
        const activeEl = document.activeElement as HTMLInputElement | HTMLTextAreaElement;
        let isCollapsed = true;
        let selectedText = "";
        if (activeEl && (activeEl.tagName === 'TEXTAREA' || activeEl.tagName === 'INPUT')) {
          const start = activeEl.selectionStart ?? 0;
          const end = activeEl.selectionEnd ?? 0;
          isCollapsed = start === end;
          selectedText = activeEl.value.substring(start, end);
        } else {
          isCollapsed = selection.isCollapsed;
          selectedText = selection.toString();
        }

        if (isCollapsed) {
          hasOpenedRef.current = false
          return
        }

        const hasTextSelected = selectedText.trim() !== ''
        if (hasTextSelected) {
          if (!hasOpenedRef.current) onOpen(() => onOpenChange(true))
          hasOpenedRef.current = true
          if (activeEl && (activeEl.tagName === 'TEXTAREA' || activeEl.tagName === 'INPUT')) {
            const rect = getDummyRect(activeEl)
            onVirtualRefChange({
                getBoundingClientRect: () => rect,
                getClientRects: () => ({
                    0: rect,
                    length: 1,
                    item: (index: number) => index === 0 ? rect : null,
                    [Symbol.iterator]: function* () {
                        yield rect;
                    }
                }),
            });
          } else {
            const range = selection?.getRangeAt(0)
            onVirtualRefChange({
              getBoundingClientRect: () => range.getBoundingClientRect(),
              getClientRects: () => range.getClientRects(),
            })
          }
        }
      }

The above code is for whileSelected. Here would be the updates for the default functionality.

          context.onOpen(() => {
            const selection = document.getSelection()
            if (!selection) return
            const trigger = ref.current
            const wasSelectionInsideTrigger = trigger?.contains(selection.anchorNode)
            if (!wasSelectionInsideTrigger) return

            const activeEl = document.activeElement as HTMLInputElement | HTMLTextAreaElement
            let isCollapsed = true
            let selectedText = ''
            if (activeEl && (activeEl.tagName === 'TEXTAREA' || activeEl.tagName === 'INPUT')) {
              const start = activeEl.selectionStart ?? 0
              const end = activeEl.selectionEnd ?? 0
              isCollapsed = start === end
              selectedText = activeEl.value.substring(start, end)
            } else {
              isCollapsed = selection.isCollapsed
              selectedText = selection.toString()
            }

            if (isCollapsed) return
            if (selectedText.trim() === '') return
            context.onOpenChange(true)

            if (activeEl && (activeEl.tagName === 'TEXTAREA' || activeEl.tagName === 'INPUT')) {
              const rect = getDummyRect(activeEl)
              context.onVirtualRefChange({
                getBoundingClientRect: () => rect,
                getClientRects: () => ({
                  0: rect,
                  length: 1,
                  item: (index: number) => (index === 0 ? rect : null),
                  [Symbol.iterator]: function* () {
                    yield rect
                  },
                }),
              })
            } else {
              const range = selection.getRangeAt(0)
              context.onVirtualRefChange({
                getBoundingClientRect: () => range.getBoundingClientRect(),
                getClientRects: () => range.getClientRects(),
              })
            }
          })
@joaom00
Copy link
Owner

joaom00 commented Oct 21, 2023

It would be a good addition to the library!

but there could be another solution that doesn't involve adding and removing a div to get the coordinates.

I don't know if it's possible to get the selection rect in the input without using this approach. Do you have anything else in mind?

@slowshi
Copy link
Author

slowshi commented Oct 23, 2023

No, I don't I am just doing the solution above to work with textarea and text inputs.

@preshonyee
Copy link

@slowshi can you please share a codesandbox of your solution in action?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants