На днях Google анонсировал выход новой платформы, позволяющей разработчикам создавать приложения, работающие внутри Google Docs и расширяющие базовый функционал Google Docs редактора.
Разберемся что это, как это работает и напишем небольшое приложение которое позволит нам переводить текст документа не выходя из Google Docs.
Немного теории.
Google Docs Аdd-on — приложение, написанное на javascript'е, существующее и работающее в Google Drive (сейчас пока только для Google Docs), имеющее свой UI ('sidebar' справа от документа или модальное окно поверх), расширяющее функционал Google Docs редактора. Google Docs Аdd-on является следующим шагом в развитии Google Apps Script. По сути это Google Apps Script проект с возможностью распространения и установки его другими пользователями Google Docs.
Существует несколько вида Google Apps Script проектов, основные из них — отдельный файл-проект в Google Drive (В Google Drive вы видите его как отдельный файл, 'standalone script') или проект с документом-контейнером ('container-bounds script'). Его вы можете увидеть открыв документ-контейнер, меню 'Tools' -> 'Script editor'. Google Docs Аdd-on — как раз второй вариант, для его создания необходим документ-контейнер, который вы будете использовать для тестирования и отладки вашего приложения.
Распространение
Распространяются Google Docs Аdd-on через Chrome Web store (хотя и не являются расширением браузера). Перед добавлением вашего add-on'a в Chrome Web Store необходимо пройти review со стороны Google. Для пользователей Google Docs все add-on'ы доступны для установки через новый пункт меню — 'Add-ons' -> 'Get Add-ons'.
Среда разработки
Для Google Apps Script проектов Google предоставляет среду разработки, интегрированную в Google Drive, с возможностью запуска, отладки и автодополнения кода. Есть ли возможность использовать другую среду разработки и хранить код не в Google Drive, а в CVS? Это позволяло бы работать над проектом нескольким разработчикам одновременно и поддерживать версионирование. Для 'standalone' проектов такая возможность есть, для 'сontainer-bounds' — к сожалению пока нет. Поэтому для разработки Google Docs Add-on'a приходится использовать интегрированную среду разработки. Вот пример python скрипта, который позволяет «выкачать» 'standalone' проект на файловую систему и обновить его в Google Drive после изменения.
Что внутри
Немного подробнее о том, как работает Google Apps Script. Файлы проекта содержат «серверную» (.gs) и «клиентскую» (.html) части приложения. «Серверная» часть — набор javascript функций, которые могут обращаться к сторонним сервисам, различным Google API (Drive, GMail, Calendar), в том числе к API документа, в котором установлен данный add-on.
Также '.gs' файлы содержат предопределенные функции, которые вызываются когда пользователь устанавливает/открывает add-on, что позволяет создавать UI вашего приложения внутри стандартного редактора Google Docs. В свою очередь «клиесткая часть» представляет собой стандартные html файлы, с возможностью вызова любой функции из любого «серверного» файла. А также, естественно, с возможностью подключения сторонних CSS/javascript файлов.
Пример
Напишем приложение, которое будет переводить выделенный текст в документе с английского на русский язык. Создаем новый Google Docs документ в Google Drive, открываем меню 'Tools' -> 'Script editor'. В появившемся диалоге выбираем 'Create script for -> Document'. После этого вы увидите ту самую интегрированную среду разработки. В проекте по умолчанию создается файл 'Code.gs', содержащий пример приложения от Google. Нам он пока не нужен, поэтому я сразу предлагаю очистить его содержимое, а сам файл переименовать в 'Server.gs'. Также лучше сразу переименовать проект из 'Untitled project', скажем в 'Translate example'.
Итак, в 'Server.gs' мы определим функции, которые будут создавать UI нашего add-on'a. Для этого достаточно объявить функцию 'onOpen'.
function onOpen() {
DocumentApp.getUi().createAddonMenu()
.addItem('Translate', 'openSidebar')
.addToUi();
}
function openSidebar( ) {}
Теперь, при открытии документа, в котором установлен данный add-on, в специальном меню 'Add-ons' появиться пункт 'Translate'. Нажатие на данный пункт меню будет вызывать функцию 'openSidebar', которая пока ничего не делает. Чтобы проверить это, в среде разработки выберите функцию «onOpen» и нажмите 'Run'
Теперь в вашем документе появился пункт меню 'Add-ons' -> 'Translate example' (название вашего проекта) -> 'Translate'
Займемся разработкой UI для нашего add-on'a. Так как UI add-on'a представляет собой html страницу, то есть смысл сразу разделить HTML разметку, CSS и javascript код на разные файлы. Для этого мы создадим файлы 'Sidebar.html', 'Styles.html', 'Scripts.html' и 'Content.html'. 'Sidebar.html' будет файлом-темплейтом, включающем в себя остальные файлы.
UI всех add-on'ов должен следовать определенному style guide, поэтому Google предоставляет специальный CSS файл с предопределенными стилями, ссылку на который вы видите в примере.
Открываем 'Content.html' и 'Styles.html' и создаем разметку
<div class="content">
<p>Select text and click 'Translate'</p>
<button class="action btn-block">Translate</button>
<p class="result"></p>
</div>
и стили
<style>
.content {
padding: 20px;
margin : 20px;
}
.btn-block {
display: block;
width: 100%;
padding-left: 0;
padding-right: 0;
}
</style>
а в 'Server.gs' дописываем тело функции 'openSidebar'
function onOpen() {
DocumentApp.getUi().createAddonMenu()
.addItem('Translate', 'openSidebar')
.addToUi();
}
function openSidebar( ) {
var html = HtmlService.createTemplateFromFile('Sidebar')
.evaluate()
.setSandboxMode(HtmlService.SandboxMode.NATIVE)
.setTitle('Translate example')
.setWidth(300);
DocumentApp.getUi().showSidebar(html);
}
Теперь по нажатию пункт меню «Translate» в документе, слева, откроется 'sidebar'.
Осталось добавить логику для перевода текста. Для этого мы будем использовать API различных Google сервисов, доступных нам в '.gs' файлах и позволяющих находить выделенный в документе текст и переводить его на выбранный язык.
'Server.gs'
function translate(pollingCounter) {
var text = getSelectedText(getSingleSeletetedElement());
if (text) {
return LanguageApp.translate(text, 'en', 'ru');
}
}
function getSelectedText(rangeElement) {
if (!rangeElement) { return; }
var element = rangeElement.getElement();
var from = rangeElement.getStartOffset();
var to = rangeElement.getEndOffsetInclusive() + 1;
if (element.getType() === DocumentApp.ElementType.TEXT || element.getType() === DocumentApp.ElementType.PARAGRAPH) {
var text = element.getText();
return text.substring(from, to);
}
}
function getSingleSeletetedElement() {
var selection = DocumentApp.getActiveDocument().getSelection();
if (selection) {
var elements = selection.getSelectedElements();
if (elements.length === 1) {
return elements[0];
}
}
}
Функция 'translate' ищет выделенный элемент в документе, и если это текст или текстовый блок — получает выделенную часть текста и переводит ее с английского на русский язык. Теперь нам нужно как-то вызвать эту функцию из нашего UI. Для этого javascript код, запущенный в 'sidebar' имеют доступ к глобальному объекту 'google.script.run', позволяющему вызвать любую функцию из любого '.gs' файла.
'Scripts.html'
$(function() {
$('.action').click(function() {
google.script.run.withSuccessHandler(function(text) {
if (text) {
$('.result').text('Result: ' + text);
} else {
$('.result').text('Please, select text to translate');
}
}).translate();
});
});
Готово, теперь наш add-on может переводить выделенный текст по нажатию на клавишу 'Translate'.
С ходу можно внести 2 улучшения. Во-первых, при открытии 'sidebar', пока загружаются все необходимые для него файлы, было бы неплохо добавить какой-нибудь индикатор загрузки. Создадим файл 'Loading.html', добавим ссылку на него в наш 'Sidebar.html', а основное содержимое спрячем до полной загрузки всех ресурсов. Как только все готово — мы прячем индикатор и показываем наш UI.
'Loading.html'
<div class="app-loading" style="text-align:center">
<br><br><br><br><br><br>
Loading...
</div>
Content.html
<div class="content" style="display:none">
<p>Select text and click 'Translate'</p>
<button class="action btn-block">Translate</button>
<p class="result"></p>
</div>
Sidebar.html
<!-- styles -->
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<?!= HtmlService.createHtmlOutputFromFile('Styles').getContent(); ?>
<!-- layout -->
<?!= HtmlService.createHtmlOutputFromFile('Loading').getContent(); ?>
<?!= HtmlService.createHtmlOutputFromFile('Content').getContent(); ?>
<!--3rd party scripts -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<!-- application scripts -->
<?!= HtmlService.createHtmlOutputFromFile('Scripts').getContent(); ?>
и наконец в Scripts.html
$(function() {
$('.app-loading').hide();
$('.content').show();
$('.action').click(function() {
google.script.run.withSuccessHandler(function(text) {
if (text) {
$('.result').text('Result: ' + text);
} else {
$('.result').text('Please, select text to translate');
}
}).translate();
});
});
А во-вторых, было бы неплохо автоматически получать перевод выделенного текста из документа, без нажатия кнопки 'Translate'. Для решения этой задачи нужно придумать как обойти одно из ограничений, существующее сейчас в Google Docs Аdd-on — нет event модели, которая позволяла бы Google Script Аdd-on'у реагировать на действия пользователя в документе. Нам прийдется имитировать интерактивность add-on'a используя long polling — постоянные запросы со стороны UI, которые будут вынуждать наш ' Server.gs' проверять документ на наличие выделения и переводить текст. Для этого напишем в 'Script.html' функцию, которая будет запускать 2 процесса с определенным интервалом. Первый процесс будет вызывать 'translate' функцию и просто сохранять результат перевода. Второй процесс будет проверять на наличие результат перевода и обновлять UI.
'Scripts.html'
var LongPollingManager = (function() {
var process = function(options) {
var pollingTimeout = 1500,
notificationTimeout = 1500,
pollingCounter = 0,
pollingStopped = false,
pollingResult,
pollingProcess,
notificationProcess;
var start = function() {
pollingStopped = false;
pollingProcess = setInterval(function() {
if (pollingStopped) { return; }
pollingCounter++;
google.script.run.withSuccessHandler(function(response) {
if (pollingStopped) { return; }
if (!pollingResult || response.pollingCounter > pollingResult.pollingCounter) {
pollingResult = response;
}
})[options.method](pollingCounter);
}, pollingTimeout);
setTimeout(function() {
notificationProcess = setInterval(function() {
if (pollingStopped) { return; }
options.notification(getLastResult());
}, notificationTimeout);
}, notificationTimeout/2);
};
var getLastResult = function() {
return pollingResult;
};
var stop = function() {
pollingResult = null;
pollingStopped = true;
clearInterval(pollingProcess);
clearInterval(notificationProcess);
};
return {
start: start,
stop : stop,
isActive: function() { return !pollingStopped; },
getLastResult : getLastResult
};;
};
return {
process: process
};
})();
var showResult = function(result) {
if (result && result.text) {
$('.result').text('Result: ' + result.text);
} else {
$('.result').text('');
}
};
$(function() {
$('.loading').hide();
$('.content').show();
$('.action').click(function() {
google.script.run.withSuccessHandler(function(text) {
if (text) {
$('.result').text('Result: ' + text);
} else {
$('.result').text('Please, select text to translate');
}
}).translate();
});
LongPollingManager.process({
method: 'translate',
notification: showResult
}).start();
});
'Server.gs'
function translate(pollingCounter) {
var result = { pollingCounter : pollingCounter };
var text = getSelectedText(getSingleSeletetedElement());
if (text) {
result.text = LanguageApp.translate(text, 'en', 'ru');
}
return result;
}
Готово, теперь наш add-on автоматически переводит выделенный текст.
Документ + код проекта на Google Drive
Код на github
Заключение
Итак, Google Docs add-on — новая платформа, позволяющая создавать приложения, расширяющие возможности базового редактора Google Docs.
Плюсы и минусы, на мой взгляд:
- + Огромная аудитория Google Docs пользователей.
- + Разработка приложения полностью на javascript.
- + Доступ к многочисленным Google API и любому стороннему сервису (HTTP запросы).
- — Разработка проекта внутри Google Docs, нет возможности вести разработку локально, трудно разделить работу между несколькими разработчиками.
- — Отсутствие интерактивного взаимодействия с документом (нет возможности получать оповещения о каких-либо действия пользователя в документе.
P. S.
Я думаю что в соперничестве MS Office — Google Drive, Google решил сделать ставку на простоту, удобство и сообщество разработчиков. Поясню на примере MS Word vs Goodle Docs. MS Word имеет огромное количество доступных пользователю функций (и как следствие — перегруженный и сложный UI) и не имеет открытого API. Google Docs — наоборот, обладает минимально необходимым набором инструментов и открытым API. Теперь любую недостающую вам функцию вы можете найти в галлерее Google Docs add-on'ов или написать сами. А пользователь может добавить в свой документ только нужные ему функции.
Автор: alex_baran