admin管理员组

文章数量:1415145

When you drag a file from your OS filesystem over a textarea or text input, a cursor appears near the mouse pointer (this is different from positionStart), showing the user where the dragged content would be inserted.

UPDATE: here is an image, I'm dragging a file (test.sh) over the text input. You can see the drop cursor if the middle of the "text" word. The selection cursor is at the end of the string (not visible on this picture).

(Chrome's default behavior is to open the dropped file, but I'm overriding this behavior in the drop event. I want to insert the name of the file in the textarea.)

I'm trying to get this position (in terms of index in the textarea value string) when drop occurs. Any idea?

When you drag a file from your OS filesystem over a textarea or text input, a cursor appears near the mouse pointer (this is different from positionStart), showing the user where the dragged content would be inserted.

UPDATE: here is an image, I'm dragging a file (test.sh) over the text input. You can see the drop cursor if the middle of the "text" word. The selection cursor is at the end of the string (not visible on this picture).

(Chrome's default behavior is to open the dropped file, but I'm overriding this behavior in the drop event. I want to insert the name of the file in the textarea.)

I'm trying to get this position (in terms of index in the textarea value string) when drop occurs. Any idea?

Share Improve this question edited Jan 13, 2021 at 13:51 Blacksad asked Jan 10, 2021 at 14:41 BlacksadBlacksad 15.5k16 gold badges74 silver badges81 bronze badges 2
  • I'm not sure how the cursor you refer to looks like, can you add an image? – OfirD Commented Jan 13, 2021 at 12:01
  • 1 If you'd be willing to forgo the exact index requirement, you can listen for the drop event upon the textarea, call e.preventDefault, get the file name, and insert that name anywhere you'd like (well, almost anywhere - with the exception of the exact cursor index :) ). – OfirD Commented Jan 14, 2021 at 12:37
Add a ment  | 

5 Answers 5

Reset to default 1 +100

Phew, what you want to do isn't easy, since there is no way to reference this specific caret!

Off the top of my head, you could only implement this through heavy workarounds: What you can obtain upon drop occuring is the mouse cursor position.

You would have to make an invisible div-clone identical to the textarea in shape, text-size, margins, etc that automatically gets filled with the text from your textarea.

Next you'd have to create a span for each possible caret position (i.e. 1 span for every character of text) inside this div and get each span's x and y coordinates and save them into an array.

.txtarea {
  width: 300px;
  height: 150px;
  font-size: 12px;
  font-family: Arial;
  padding: 5px;
  text-align: left;
  border: 1px solid red;
}
<textarea class="txtarea">Mytext</textarea>

<!--just an example, auto-fill this content through a JS oninput event -->
<div class="txtarea"><span>M</span><span>y</span><span>t</span><span>e</span><span>x</span><span>t</span></div>

Then, upon drop occuring, get the mouse coordinates of the event, pare them to the array of coordinates, approximate the closest x/y position and set a new selectionStart index in your textarea based on that, insert the file name, and then restore the previous selectionStart.

This is only a partial answer: The following works in IE11, but not in Chrome (didn't test in other browsers).

Just select some letters from the upper input and drag them into the bottom one:

let to = document.getElementById("to");
to.addEventListener("dragover", function (e) { 
  if (e.target.selectionStart) { 
     console.log(e.target.selectionStart); 
  }
});
<input type="text" id="from" value="select some letters from this sentence, and drag them into the other input element"/ style="width: 500px;"><br/>
<input type="text" id="to" value="drag over here"/>

Few notes for future research:

  • In Chrome, dragover event is fired only after focus is first set inside the input element.

  • In Chrome, the value of selectionStart is the value of the last selected text position within the target input (put differently: the last position of the cursor within the target input).

  • Setting type="number" fires with selectionStart assigned to null in Chrome, but not in IE11.

From Your picture, it looks like an input text (I admit, I was too lazy to code this for a textarea...) but - anyway - this is a nice question.

To get the text width, You need to clone the original element and loop over the text char by char. Just to mention an example, see some answers here on SO about the topic: "how to set the ellipsis text using JavaScript".

I also noticed that by moving the mouse pointer over the text, the caret is shifting left and right from the center of each char. At the end, the main problem here is: how to find the x of the middle of each char.

So, my code is splitted in two parts: text input clone and character position. After that, to position the caret, You can use an existing library like this: jQuery Caret Plugin or jQuery Caret - up to You - I would skip this part of the question.

I tested the code below in FF, Safari, IE & Chrome, there are obviously some small and annoying issues in the drag & drop behavior of these browser, but they seems to me irrelevant.

function getCaretPos(target, x) {
  var txt = target.value, doc = target.ownerDocument,
    clone = doc.createElement('span'), /* Make a clone */
    style = doc.defaultView.getComputedStyle(target, null),
    mL = style.getPropertyValue('margin-left'),
    pL = style.getPropertyValue('padding-left'),
    mouseX = x - parseFloat(mL) - parseFloat(pL);

  clone.style.fontFamily = style.getPropertyValue('font-family');
  clone.style.fontSize = style.getPropertyValue('font-size');
  clone.style.fontWeight = style.getPropertyValue('font-weight');
  clone.style.position = 'absolute'; /* Keep layout */
  clone.style.left = -9999; 
  target.parentNode.appendChild(clone);
  clone.style.width = 'auto'; 
  clone.style.whiteSpace = 'pre'; /* Keep whitespaces */
  clone.style.marginLeft = 0;
  clone.style.paddingLeft = 0;
  
  clone.innerText = txt; /* Start from full length */
  var i = txt.length, pos = -1, xL = 0, xC = 0, xR = clone.clientWidth;
  while (i--) { /* Find the caret pos */
    clone.innerText = txt.slice(0, i);
    xL = clone.clientWidth;
    xC = (0.5 * (xR + xL)) | 0; /* We need the center */
    if (xC < mouseX) { pos = i; break }
    xR = xL;
  }
  pos++; /* Restore the correct index */
  target.parentNode.removeChild(clone); /* Clean up */
  clone = null;
  return pos;
}

function onFileDragOver(e) {
  if(!window.chrome) e.preventDefault(); /* Show the caret in Chromium */
}

function onFileDrop(e) {
  e.preventDefault();
  var target = e.currentTarget, txt = target.value,
    pos = getCaretPos(target, e.offsetX),
    tok1 = txt.substr(0, pos), tok2 = txt.substr(pos);

  /* Get the drop-action result */
  var result = '', files = e.dataTransfer.files,
    data = e.dataTransfer.getData('text');

  for (var i = 0, f; (f = files[i]); i++) { result += f.name }
  target.value = tok1 + result + tok2;
  target.focus();
  /* Up to You how to position the caret */
  if(target.setSelectionRange) target.setSelectionRange(pos, pos);
  //$(target).caret(pos); 
}

$(document).ready(function () {
  var target = document.getElementById('search-input');
  target.addEventListener('dragover', onFileDragOver);
  target.addEventListener('drop', onFileDrop);
});
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
    <link rel="stylesheet" href="https://code.jquery./mobile/1.4.5/jquery.mobile-1.4.5.css" />
    <script src="https://cdnjs.cloudflare./ajax/libs/jquery/2.1.2/jquery.js"></script>
    <script src="https://ajax.googleapis./ajax/libs/jquerymobile/1.4.5/jquery.mobile.js"></script>
  </head>
  <body>
    <div data-role="page">
      <div data-role="header"><h1>Caret Position</h1></div>
      <div data-role="content">
        <div class="ui-field-contain">
          <label for="search-input">Products</label>
          <input type="search" name="search-input" id="search-input" value="target text box"
          spellcheck="false" autoplete="off" autocorrect="off" autocapitalize="none"/>
        </div>
      </div>
    </div>
  </body>
</html>

Try jQuery. What you have to do is get the text box with a query selector, then bind that to mousemove and get $(this).caret().start.

Example:

let cursorPosition = 0;

$("#textinput").bind("mousemove", function() {
  cursorPosition = $(this).caret().start;
});

// Do what you want with cursorPosition when file is dropped

And I think that's all you need to do here.

As long as you're willing to use jQuery you should be good.

This is a hack.

In your drop event, fire an async method shortly after your event pletion occurs. Below is the angular typescript syntax:

<!-- Template -->
<div
  [attr.contenteditable]="plaintext-only"
  (drop)="onDrop($event)">
</div>

  onDrop(event: DragEvent) {
    setTimeout(
      () => {
        const selection = this.document.getSelection();
        // do what you must with the selection here
        // e.g., I'm replacing the dropped text with the sanitized version
        // if (!selection || !selection.rangeCount) return;
        // selection.deleteFromDocument();
        // selection.getRangeAt(0).insertNode(this.document.createTextNode(sanitizedText));
        // selection.collapseToEnd();
      },
      1 // wait for a millisecond so that document's selection is replaced by pletion of this event
    );
  }

Note: you'll have to overwrite the native undo/redo if you use this code.

PS: bad actors might still get their scripts to run in other editors, notice this example deals with contenteditable plaintext only div, which should be fine.

本文标签: javascriptGet cursor position when a file is dropped in textarea in ChromeStack Overflow