Как опросить Google Doc из дополнения

A документированное ограничение с надстройками документов и листов заключается в том, что Apps Script не может сказать, что пользователь делает вне дополнения, Этот дразнящий наконечник дается:

Можно опросить изменения в содержимом файла из боковая панель на стороне клиента, хотя у вас всегда будет небольшой задержка. Этот метод также может предупредить ваш Script об изменениях в выбранные пользователем ячейки (в таблицах) и курсор или выбор (в Документах).

К сожалению, это не показано ни в одном из демонстрационных кодов. Как я могу это сделать?

Ответ 1

Опрос выполняется из html-кода в вашем дополнительном пользовательском интерфейсе, перейдя к серверным приложениям Script с помощью google.script.run.

Использование jQuery упрощает это, и мы можем даже начать с ответов jQuery, простого примера опроса.

function doPoll(){
    $.post('ajax/test.html', function(data) {
        alert(data);  // process results here
        setTimeout(doPoll,5000);
    });
}

Основная идея может работать для Google Apps Script, если мы заменим вызовы ajax эквивалентами GAS.

Здесь скелет функции опроса, который вы будете использовать в своем html файле:

  /**
   * On document load, assign click handlers to button(s), add
   * elements that should start hidden (avoids "flashing"), and
   * start polling for document updates.
   */
  $(function() {
    // assign click handler(s)

    // Add elements that should start hidden

    // Start polling for updates
    poll();
  });

  /**
   * Poll a server-side function 'serverFunction' at the given interval 
   * and update DOM elements with results. 
   *
   * @param {Number} interval   (optional) Time in ms between polls.
   *                            Default is 2s (2000ms)
   */
  function poll(interval){
    interval = interval || 2000;
    setTimeout(function(){
      google.script.run
       .withSuccessHandler(
         function(results) {
           $('#some-element').updateWith(results);
           //Setup the next poll recursively
           poll(interval);
         })
       .withFailureHandler(
         function(msg, element) {
           showError(msg, $('#button-bar'));
           element.disabled = false;
         })
       .serverFunction();
    }, interval);
  };

Пример надстройки, Document Poller

Это демонстрация метода опроса jQuery, вызывающего функции Google Apps Script на стороне сервера для обнаружения поведения пользователя в документе Google. Он ничего не полезен, но демонстрирует несколько вещей, которые обычно требуют знания активности пользователя и состояния документа, например, с помощью контекстно-зависимого контроля над кнопкой.

Тот же принцип может применяться к электронной таблице или к отдельному веб-приложению GAS.

Как и пример приложения UI в этом вопросе, этот метод можно использовать для ограничения времени выполнения, для операций с пользовательским интерфейсом по крайней мере.

Screenshot

Код основывается на примерном надстройке от Google 5-минутный быстрый запуск. Следуйте инструкциям из этого руководства, используя код ниже, а не быстрый запуск.

Code.gs

/**
 * Creates a menu entry in the Google Docs UI when the document is opened.
 *
 * @param {object} e The event parameter for a simple onOpen trigger. To
 *     determine which authorization mode (ScriptApp.AuthMode) the trigger is
 *     running in, inspect e.authMode.
 */
function onOpen(e) {
  DocumentApp.getUi().createAddonMenu()
      .addItem('Start', 'showSidebar')
      .addToUi();
}

/**
 * Runs when the add-on is installed.
 *
 * @param {object} e The event parameter for a simple onInstall trigger. To
 *     determine which authorization mode (ScriptApp.AuthMode) the trigger is
 *     running in, inspect e.authMode. (In practice, onInstall triggers always
 *     run in AuthMode.FULL, but onOpen triggers may be AuthMode.LIMITED or
 *     AuthMode.NONE.)
 */
function onInstall(e) {
  onOpen(e);
}

/**
 * Opens a sidebar in the document containing the add-on user interface.
 */
function showSidebar() {
  var ui = HtmlService.createHtmlOutputFromFile('Sidebar')
      .setTitle('Document Poller');
  DocumentApp.getUi().showSidebar(ui);
}

/**
 * Check if there is a current text selection.
 *
 * @return {boolean}  'true' if any document text is selected
 */
function checkSelection() {
  return {isSelection : !!(DocumentApp.getActiveDocument().getSelection()),
          cursorWord : getCursorWord()};
}

/**
 * Gets the text the user has selected. If there is no selection,
 * this function displays an error message.
 *
 * @return {Array.<string>} The selected text.
 */
function getSelectedText() {
  var selection = DocumentApp.getActiveDocument().getSelection();
  if (selection) {
    var text = [];
    var elements = selection.getSelectedElements();
    for (var i = 0; i < elements.length; i++) {
      if (elements[i].isPartial()) {
        var element = elements[i].getElement().asText();
        var startIndex = elements[i].getStartOffset();
        var endIndex = elements[i].getEndOffsetInclusive();

        text.push(element.getText().substring(startIndex, endIndex + 1));
      } else {
        var element = elements[i].getElement();
        // Only translate elements that can be edited as text; skip images and
        // other non-text elements.
        if (element.editAsText) {
          var elementText = element.asText().getText();
          // This check is necessary to exclude images, which return a blank
          // text element.
          if (elementText != '') {
            text.push(elementText);
          }
        }
      }
    }
    if (text.length == 0) {
      throw 'Please select some text.';
    }
    return text;
  } else {
    throw 'Please select some text.';
  }
}

/**
 * Returns the word at the current cursor location in the document.
 *
 * @return {string}   The word at cursor location.
 */
function getCursorWord() {
  var cursor = DocumentApp.getActiveDocument().getCursor();
  var word = "<selection>";
  if (cursor) {
    var offset = cursor.getSurroundingTextOffset();
    var text = cursor.getSurroundingText().getText();
    word = getWordAt(text,offset);
    if (word == "") word = "<whitespace>";
  }
  return word;
}

/**
 * Returns the word at the index 'pos' in 'str'.
 * From https://stackoverflow.com/info/5173316/finding-the-word-at-a-position-in-javascript/5174867#5174867
 */
function getWordAt(str, pos) {

  // Perform type conversions.
  str = String(str);
  pos = Number(pos) >>> 0;

  // Search for the word beginning and end.
  var left = str.slice(0, pos + 1).search(/\S+$/),
      right = str.slice(pos).search(/\s/);

  // The last word in the string is a special case.
  if (right < 0) {
    return str.slice(left);
  }

  // Return the word, using the located bounds to extract it from the string.
  return str.slice(left, right + pos);
}

Sidebar.html

<link rel="stylesheet" href="#" onclick="location.href='https://ssl.gstatic.com/docs/script/css/add-ons.css'; return false;">
<!-- The CSS package above applies Google styling to buttons and other elements. -->


<div class="sidebar branding-below">
  <form>
    <div class="block" id="button-bar">
      <button class="blue" id="get-selection" disabled="disable">Get selection</button>
    </div>
  </form>
</div>

<div class="sidebar bottom">
  <img alt="Add-on logo" class="logo" height="27"
      id="logo"
      src="https://www.gravatar.com/avatar/adad1d8ad010a76a83574b1fff4caa46?s=128&d=identicon&r=PG">
  <span class="gray branding-text">by Mogsdad, D.Bingham</span>
</div>

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">
</script>
<script>
  /**
   * On document load, assign click handlers to button(s), add
   * elements that should start hidden (avoids "flashing"), and
   * start polling for document selections.
   */
  $(function() {
    // assign click handler(s)
    $('#get-selection').click(getSelection);


    // Add elements that should start hidden
    var newdiv1 = $( "<div class='block' id='cursor-word'/>" ).hide(),
        newdiv2 = $( "<div class='block' id='selected-text'/>" ).hide();
    $('#button-bar').after( newdiv1, newdiv2 );
    $('#cursor-word').html('<H2>Word at cursor:</H2><p id="cursor-word-content"></p>');
    $('#selected-text').html('<H2>Selected text:</H2><p id="selected-text-content"></p>');

    // Start polling for updates        
    poll();
  });

  /**
   * Poll the server-side 'checkSelection' function at the given
   * interval for document selection, and enable or disable the
   * '#get-selection' button.
   *
   * @param {Number} interval   (optional) Time in ms between polls.
   *                            Default is 2s (2000ms)
   */
  function poll(interval){
    interval = interval || 2000;
    setTimeout(function(){
      google.script.run
       .withSuccessHandler(
         function(cursor) {
           if (cursor.isSelection) {
             // Text has been selected: enable button, hide cursor word.
             $('#get-selection').attr('disabled', false);
             $('#cursor-word').hide();
             // $('#selected-text').show();  // Not so fast - wait until button is clicked.
           }
           else {
             $('#get-selection').attr('disabled', true);
             $('#cursor-word').show();
             $('#selected-text').hide();
           }
           $('#cursor-word-content').text(cursor.cursorWord);
           //Setup the next poll recursively
           poll(interval);
         })
       .withFailureHandler(
         function(msg, element) {
           showError(msg, $('#button-bar'));
           element.disabled = false;
         })
       .checkSelection();
    }, interval);
  };

  /**
   * Runs a server-side function to retrieve the currently
   * selected text.
   */
  function getSelection() {
    this.disabled = true;
    $('#error').remove();
    google.script.run
      .withSuccessHandler(
        function(selectedText, element) {
          // Show selected text
          $('#selected-text-content').text(selectedText);
          $('#selected-text').show();
          element.disabled = false;
        })
      .withFailureHandler(
        function(msg, element) {
          showError(msg, $('#button-bar'));
          element.disabled = false;
        })
      .withUserObject(this)
      .getSelectedText();
  }


  /**
   * Inserts a div that contains an error message after a given element.
   *
   * @param msg The error message to display.
   * @param element The element after which to display the error.
   */
  function showError(msg, element) {
    var div = $('<div id="error" class="error">' + msg + '</div>');
    $(element).after(div);
  }
</script>

Интервал опроса

Функция setTimeout() принимает интервал времени, выраженный в миллисекундах, но я обнаружил, что эксперимент показал, что наилучшим из двух ответов является то, что можно было ожидать. Поэтому скелет poll() имеет интервал 2000 мс по умолчанию. Если ваша ситуация может выдержать более длительную задержку между циклами опроса, тогда вы получите большее значение при вызове onLoad на poll(), например. poll(10000) для 10-секундного цикла опроса.

Простыни

Для примера с листом см. Как сделать отображаемые значения боковой панели из ячеек?