Search code examples
google-apps-scriptgoogle-docsgoogle-apps-script-addon

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?


Solution

    • 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.