selection-popover icon indicating copy to clipboard operation
selection-popover copied to clipboard

Textarea and Input support

Open slowshi opened this issue 10 months ago • 3 comments

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(),
              })
            }
          })

slowshi avatar Oct 18 '23 18:10 slowshi