DjH
DjH

Reputation: 1498

Google (Docs) Apps Script - Can't check if cursor on named range

I am inserting text into a document and each text insertion is added to a named range so that I can look them all up with getNamedRanges(NAME) and getNamedRangesById(ID).

Now I need to check if the current cursor position is on a named range and I have yet to figure out how.

This post is similar: How to determine the named range from position in Google Docs through Google Apps Script

But when the cursor is on a namedrange cursor.getElement() returns Text object, not the named range.

How can I determine if the cursor is currently positioned on a named range?

Upvotes: 3

Views: 476

Answers (3)

Tanaike
Tanaike

Reputation: 201378

  • You want to confirm whether the current cursor position is inside in the namedRange on Google Document.
  • You want to achieve this using Google Apps Script.

Workaround:

In this workaround, I checked whether the cursor position is included in the namedRange by comparing the indexes of paragraph of both the namedRange and the cursor position.

Flow:

The flow of the script is as follows.

  1. Retrieve the indexes of paragraph of the namedRange.
    • I this sample script, from your question, the namedRange ID is used.
    • In this case, there might be multiple paragraphs including table, list and so on. So all indexes in the namedRange are retrieved.
  2. Retrieve the index of paragraph of the cursor position.
  3. Retrieve the index of paragraph of the selected range.
    • This sample script also checks whether the selected range is in the namedRange. Because when the text is selected, cursor becomes null.
  4. If the cursor or selected range are staying in the namedRange, myFunction() returns true.
    • If the cursor or selected range are not staying in the namedRange, myFunction() returns false.
    • Also you can confirm it at the log.

Sample script:

Before you use this script, please set the namedRange ID.

function myFunction() {
  var nameRangeId = "###"; // Please set namedRange ID here.
  
  var getIndex = function(doc, e) {
    while (e.getParent().getType() != DocumentApp.ElementType.BODY_SECTION) e = e.getParent();
    return doc.getBody().getChildIndex(e);
  };
  var doc = DocumentApp.getActiveDocument();
  
  // For namedRange
  var namedRange = doc.getNamedRangeById(nameRangeId);
  if (namedRange) {
    var indexOfNamedRange = namedRange.getRange().getRangeElements().map(function(e) {return getIndex(doc, e.getElement())});
  } else {
    throw new Error("No namedRange.");
  }
  
  var name = namedRange.getName();
  
  // For cursor
  var cursor = doc.getCursor();
  if (cursor) {
    var indexOfCursor = getIndex(doc, cursor.getElement());
    if (~indexOfNamedRange.indexOf(indexOfCursor)) {
      Logger.log("Inside of %s", name);
      return true;
    }
    Logger.log("Outside of %s", name);
    return false;
  }

  // For select
  var select = doc.getSelection();
  if (select) {
    var indexOfSelect = select.getRangeElements().map(function(e) {return getIndex(doc, e.getElement())});
    if (indexOfSelect.some(function(e) {return ~indexOfNamedRange.indexOf(e)})) {
      Logger.log("Inside of %s", name);
      return true;
    }
    Logger.log("Outside of %s", name);
    return false;
  }

  throw new Error("No cursor and select.");
}

Note:

  • In this script, when the text is selected on Document, the cursor position cannot be retrieved. So I added the function to check the selected range. If you don't want to check the selected range, please remove the script of // For select.
  • In this script, even only one index of selected range are included in the namedRange, true is returned. About this, please modify for your situation.
  • In the current stage, this script doesn't suppose about the header and footer sections.

References:

Added:

I had understood that from this situation, OP has set the named range to the paragraph. When I proposed a sample script for this, I thought that I correctly understood OP's goal. But, from gaspar's following comment,

this only shows whether the cursor is in the same element as the named range, but in case of named range partial text it gives a false positive finding if the cursor is in the same element but not in the same text part

If OP sets the part of the paragraph as the named range, and OP wants to check whether the cursor is included in the named range, the sample script is as follows.

Sample script:

function myFunction() {
  var nameRangeId = "###"; // Please set namedRange ID here.

  var getIndex = function (doc, e) {
    while (e.getParent().getType() != DocumentApp.ElementType.BODY_SECTION) e = e.getParent();
    return doc.getBody().getChildIndex(e);
  };
  var doc = DocumentApp.getActiveDocument();

  // For namedRange
  var namedRange = doc.getNamedRangeById(nameRangeId);

  if (namedRange) {
    var indexOfNamedRange = namedRange.getRange().getRangeElements().map(e => ({ idx: getIndex(doc, e.getElement()), start: e.getStartOffset(), end: e.getEndOffsetInclusive() }));
  } else {
    throw new Error("No namedRange.");
  }
  var name = namedRange.getName();

  // For cursor
  var cursor = doc.getCursor();
  if (cursor) {
    var indexOfCursor = getIndex(doc, cursor.getElement());
    var offset = cursor.getOffset();
    if (indexOfNamedRange.some(({ idx, start, end }) => idx == indexOfCursor && ((start == -1 && end == -1) || (offset > start && offset < end)))) {
      Logger.log("Inside of %s", name);
      return true;
    }
    Logger.log("Outside of %s", name);
    return false;
  }

  // For select
  var select = doc.getSelection();
  if (select) {
    var indexOfSelect = select.getRangeElements().map(e => ({ idx: getIndex(doc, e.getElement()), start: e.getStartOffset(), end: e.getEndOffsetInclusive() }));
    if (indexOfSelect.some(e => indexOfNamedRange.some(({ idx, start, end }) => idx == e.idx && ((start == -1 && end == -1) || ((e.start > start && e.start < end) || (e.end > start && e.end < end)))))) {
      Logger.log("Inside of %s", name);
      return true;
    }
    Logger.log("Outside of %s", name);
    return false;
  }

  throw new Error("No cursor and select.");
}
  • When I posted my answer, Google Apps Script cannot use V8 runtime. But, now, V8 runtime can be used. So I modified the script using V8 runtime. Please be careful about this.

Upvotes: 1

gaspar
gaspar

Reputation: 1068

Since I also needed this and none of the previous answers work correctly (I tested them all; see my comments), I wrote my own function that actually works, see below.

const cursorIndex = getIndex(cursor.getElement())
let found = false;
let rangeIndex = 0;
// note: to search for any named range, just omit the name
// (hence just give "doc.getNamedRanges()")
// then you can get the name of the found named range via "getName()"
for (const rangeEntry of doc.getNamedRanges('the_named_range_name')) {
    for (const element of rangeEntry.getRange().getRangeElements()) {
        rangeIndex = getIndex(element.getElement());
        if (cursorIndex === rangeIndex) {
            if (element.isPartial()) {
                let cursorOffset = cursor.getSurroundingTextOffset()
                if (cursorOffset >= element.getStartOffset() && cursorOffset <= element.getEndOffsetInclusive() + 1) {
                    found = true;
                    break;
                }
            } else {
                found = true;
                break;
            }
        }
    }
    if (found || rangeIndex > cursorIndex) {
        break;
    }
}
if (found) {
    DocumentApp.getUi().alert("There is a named range here.");
}

(With small modifications, this could also be an answer to this question.)

Upvotes: 0

ziganotschka
ziganotschka

Reputation: 26796

The solution proposed in the post to which you refer implies looping through your range of interest and checking if one of your range elements equals the element on which the cursor lies.

The code should look like this:

function myFunction() {

  var doc = DocumentApp.getActiveDocument(); 
  var cursor = doc.getCursor();
  var el=cursor.getElement().asText().getText();
  var range;
  //specify the name of the range of interest in getNamedRanges()
  doc.getNamedRanges('testy').forEach(function(rangeEntry){
    (rangeEntry.getRange().getRangeElements().forEach(function(element){
      var child=element.getElement().getText();
      if(child==el){
        Logger.log("Cursor is on named range "+rangeEntry.getName())
      }
    }))
  })
}

Upvotes: 1

Related Questions