Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a flexible way to modify the contents of an editable element?

Subject:

I am creating a Google Chrome extension that interacts with web pages via a content script and an event page.

I have context menu options that appear if a user clicks on an element that is categorized as editable by the chrome.contextMenus API.

The options act like shortcuts for commonly entered text. When the user clicks an option, some text is placed inside the element at the position of the cursor. If the user has text highlighted, it is deleted.


Problem:

Not all editable elements can be modified the same way.

If the element is a simple textarea the desired result can be achieved by implementing this solution:

  • Replacing selected text in the textarea

However, I can not assume that I am interacting with a normal textarea.

Possible nuances include:

  • The text area being hidden inside an Iframe, which complicates the process of finding the element to interact with (document.activeElement may return the Iframe, rather than the element that actually contains the text).

  • The <textarea> not being a <textarea>/<input> at all, but rather a contentEditable <div>. The .value approach will not work in this case.

So I am searching for a flexible way to do this that can handle all edge cases elegantly.


Some solutions I have tried:

  • option 1 :
    I originally planned on storing the value in the system clipboard. Then, I could conceivably just use document.execCommand('paste') to modify the element. However, after trying it, this approach seems to have the same drawbacks as my initial approach. (See this question)

    Additionally, this approach would delete whatever was in the clipboard before the operation. This is undesirable and any solution that utilizes this approach must provide a work around.

  • option 2 :
    Another option that I have considered is dispatching keyboard events for each character in the string. However, with this solution, you still run into the Iframe problem and it doesn't allow you do use special Unicode characters. ┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻
like image 354
Luke Avatar asked Jan 20 '15 21:01

Luke


1 Answers

Your problem consists of two subproblems:

  1. Identify the target element of the contextmenu action.
  2. Insert a custom text fragment at the caret (remove any selection if it is present).

Subproblem 1: Identifying the target element

  • If crbug.com/39507 is resolved, then getting the element is easy. This feature request is almost 5 year old without any progress, so don't get your hopes high on this one. Alternative methods require you to break problem 1 in two more subproblems: Identifying the target frame, and then select the target DOM element.

There are several APIs that help with identifying the frame (use a combination of them, choose whichever combination fits best in your situation):

  • The contextMenus.onClicked event provides the tab's ID (tab.id) as a property in an instance of tabs.Tab and the frame's URL (frameUrl) in a separate object.
  • The chrome.tabs.executeScript method can directly run a script in a frame.
    (currently only the top-level frame or all frames, targetting a specific frame is work in progress - crbug.com/63979, planned for Chrome 42).
    Until targetting a specific frame is implemented, you could insert the content script in every frame and compare the URL with frameUrl (or use a combination of the next methods).
  • Assuming that you have already inserted a content script with a chrome.runtime.onMessage listener, use chrome.tabs.sendMessage to send a message to a specific frame identified by frameId (since Chrome 41).
  • Use the chrome.webNavigation.getAllFrames method to get a list of all frames in a tab for a given tabId, then get the frameId of the target frame by filtering the list of frames by a known frameUrl.
  • (in the future (Chrome 42?), contextMenus.onClicked will get the frameId).

Okay, assuming that you have the correct frame, you can simply use document.activeElement to get the target element, because input elements get focused upon click.

Subproblem 2: Inserting a text fragment at the caret

If the target element is a <textarea> or <input>, then you can simply use

// Assume: elem is an input or textarea element.
var YOURSTRING = 'whatever';
var start = elem.selectionStart;
var end = elem.selectionEnd;
elem.value = elem.value.slice(0, start) + YOURSTRING + elem.value.substr(end);
// Set cursor after selected text
elem.selectionStart = start + YOURSTRING.length;
elem.selectionEnd = elem.selectionStart;

Otherwise, you need to know whether there is a content editable element, and if so, remove any selection if existent, and finally put your desired text over there.

var elem = document.activeElement;
if (elem && elem.isContentEditable) {
    // YOURSTRING as defined before
    var newNode = document.createTextNode(YOURSTRING);

    var sel = window.getSelection();
    // Remove previous selection, if any.
    sel.deleteFromDocument();
    // If there is no range in the selection, add a new one, with the
    // caret set to the end of the input element to avoid the next error:
    //"Failed to execute 'getRangeAt' on 'Selection': 0 is not a valid index."
    if (sel.rangeCount === 0) {
        sel.addRange(document.createRange());
        sel.getRangeAt(0).collapse(elem, 1);
    }
    // Insert new text
    var range = sel.getRangeAt(0);
    range.insertNode(newNode);
    // Now, set the cursor to the end of your text node
    sel.collapse(newNode, 1);
}

Relevant documentation for the web platform APIs used in the last example:

  • Selection
  • Selection.deleteFromDocument()
  • Selection.collapse
  • Range
  • Range.insertNode
like image 167
Rob W Avatar answered Oct 07 '22 12:10

Rob W