Статьи

Как создать расширение Trello Chrome — экспорт списков

В предыдущей части мы построили основы нашего расширения, внедрив аутентификацию через пользовательский экран настроек на основе Foundation и используя клиентскую библиотеку Trello JavaScript. В этой части мы закончим наше расширение, добавив логику экспорта и пользовательский интерфейс.

обмен сообщениями

Когда мы аутентифицируемся с Trello на экране настроек, токен Trello сохраняется в локальном хранилище. Однако страница настроек является собственной страницей и, по сути, собственной средой — следовательно, ни фоновая страница расширения, ни скрипты содержимого расширения не имеют к ней доступа. Это где нам нужно использовать передачу сообщений .

API chrome.extension.sendMessage используется для отправки сообщений на фоновые страницы и с них. В нашем случае мы будем использовать его для отправки токена со страницы настроек на нашу фоновую страницу. Поскольку наше приключение с настройками завершено, как только оно будет завершено, мы могли бы автоматически закрыть вкладку, чтобы повысить удобство для пользователя.

Обновите первую часть функции init в settings.js так:

 // Check if page load is a redirect back from the auth procedure if (HashSearch.keyExists('token')) { Trello.authorize( { name: "Trello Helper Extension", expiration: "never", interactive: false, scope: {read: true, write: false}, success: function () { chrome.extension.sendMessage({ command: 'saveToken', token: localStorage.getItem('trello_token') }, function(data) { chrome.tabs.getCurrent(function (tab) { chrome.tabs.remove(tab.id) }); }); }, error: function () { alert("Failed to authorize with Trello.") } }); } 

Используя эту логику, мы говорим библиотеке Trello отправлять сообщение на расширение после завершения аутентификации, и как только она получает ответное сообщение о том, что сообщение было получено (это часть function(data) ), мы закрываем текущую вкладку.

Теперь давайте разберемся с фоновой страницей. Сначала измените содержимое background.html на это:

 <!doctype html> <script type="text/javascript" src="scripts/key.js"></script> <script type="text/javascript" src="scripts/background.js"></script> <script type="text/javascript" src="lib/jquery-2.1.1.min.js"></script> <script type="text/javascript" src="lib/trello_client.js"></script> 

Мы загружаем ключ приложения, фоновый скрипт, который мы будем использовать для нашей логики, и клиент Trello, как и раньше. Очевидно, нам тоже нужен jQuery — это зависимость Trello.

Затем измените scripts/background.js на:

 chrome.extension.onMessage.addListener( function (request, sender, sendResponse) { chrome.pageAction.show(sender.tab.id); // Now we have a token saved locally, as fetched from the settings page after authorization. if (request.command == 'saveToken') { localStorage.setItem('trello_token', request.token); sendResponse(); return true; } }); 

Это та часть, которая получает сообщение со страницы настроек. Он извлекает токен из запроса и сохраняет его в localStorage для будущего использования. Мы используем формирование объекта с command в качестве основного ключа, потому что мы намереваемся отправить другие команды на фоновую страницу позже.

Авто-настройка

Над командой saveToken , давайте еще один блок:

 if (!request.command && !localStorage.getItem('trello_token')) { chrome.tabs.create({url: chrome.extension.getURL('settings/index.html')}); sendResponse(); return true; } 

Если мы не выполнили определенную команду, а наш пользователь еще не прошел аутентификацию в Trello, откройте страницу настроек в новой вкладке. Это гарантирует, что страница настроек открывается сразу после первоначальной установки расширения, как только в браузере появляется доска Trello.

Добавление опции меню

Пользовательский интерфейс Trello очень, очень недружелюбен к настройке. Списки не имеют идентификаторов в своих элементах, как атрибуты данных или ссылки любого вида, в то время как карты есть. Контекстные меню, которые появляются, когда вы нажимаете кнопку параметров списка в верхнем правом углу, перестраиваются с нуля каждый раз, когда их называют (слишком много?), И все из одного окна классифицируется как «всплывающее окно», которое также вызывается, если вы щелкаете практически все остальные меню в пользовательском интерфейсе. Что еще хуже, когда вы вызываете всплывающее меню списка, само меню не имеет идентификатора списка, к которому он был вызван, поэтому у вас нет представления о его контексте, что затрудняет получение простого идентификатора списка для правильного запроса Об этом API Trello и получить карты для экспорта. Вот почему то, что следует, может показаться ужасным занятием, но это потому, что так оно и есть.

Чтобы добавить пункт меню в контекстное меню, нам нужно отредактировать main.js содержимого main.js Преврати это в это:

 chrome.extension.sendMessage({}, function (response) { var readyStateCheckInterval = setInterval(function () { if (document.readyState === "complete") { clearInterval(readyStateCheckInterval); var popover = $(".pop-over"); $('.list-header-menu-icon').click(function(event) { var popover_summoned_interval = setInterval(function () { if ($(popover).is(':visible')) { clearInterval(popover_summoned_interval); $(".pop-over .content").append('<hr><ul class="pop-over-list"> <li><a class="js-export-list" href="#">Export This List</a></li> </ul>'); $(".js-export-list").click(function(e){ // EXPORT LIST }); } }, 50); }); } }, 10); }); 

Начиная с var popover = $(".pop-over"); , мы устанавливаем переменную для хранения объекта popover, просто чтобы нам не приходилось его снова извлекать. Затем, когда нажимается кнопка меню в списке ( .list-header-menu-icon ), мы .list-header-menu-icon интервал, который постоянно следит за тем, виден ли поповер или нет. Как только она становится видимой, проверка останавливается, и в конец всех опций добавляется пункт меню, специально созданный так, чтобы он выглядел как остальные, чтобы он подходил. Наконец, обработчик события щелчка привязан к этой опции, так что мы можем назовите «экспорт», когда опция нажата. Но .. как мы узнаем, что нам нужно экспортировать? И в какой формат мы экспортируем?

Нахождение списка ID

Как я уже говорил, пользовательский интерфейс Trello, как известно, недружелюбен по отношению к разработчикам. Он не предлагает идентификаторы списка с элементами DOM, поэтому найти их не так просто. Зачем нам нужны идентификаторы списка? Чтобы запросить API Trello и получить карты, чтобы мы могли их экспортировать — мы уже говорили, что не собираемся анализировать интерфейс из-за его нестабильности на больших платах, но будем полагаться на API.

К счастью, если мы проверим карточки в отдельных списках, мы увидим, что у них действительно есть атрибут href и что он содержит идентификатор карточки. Зная идентификатор карты, мы можем запросить у Trello информацию и получить идентификатор родительского списка. Но … если всплывающее меню не прикреплено к списку, как мы узнаем, по какому списку мы щелкнули? Мы не можем просто взять первую карту, с которой сталкиваемся, это было бы слишком случайно.

Мы можем использовать event , сгенерированное jQuery при нажатии кнопки меню. Это важно! Мы используем исходный щелчок по кнопке меню вместо щелчка по опции «Экспорт», поскольку, хотя исходная кнопка привязана к списку, который мы заинтересованы в экспорте, фактическое меню, которое вызывается, не является и, как таковое, делает для нас почти невозможным выяснить, с каким списком мы имеем дело. Вместо комментария // EXPORT LIST в приведенном выше коде добавьте:

 exportList(event); 

Затем создайте функцию:

 function exportList(event) { var first_card_id = findFirstCardId(event); if (!first_card_id) { alert('No cards found in the list.'); return false; } } 

Наконец, создайте функцию findFirstCardId :

 /** * Uses the menu button on a card to find the first card in that list and get its ID * Returns false if not found, or the ID if there is a card * @param event * @returns bool | string */ function findFirstCardId(event) { var titles = $(event.currentTarget).parent().parent().find('a.list-card-title:first'); if (titles[0] === undefined) { console.error('List has no cards!'); return false; } else { return $(titles[0]).attr('href').split('/')[2]; } } 

Мы выбираем прародителя цели события (список) и находим в ней первый заголовок карты. Название содержит href в этой форме:

04

Если заголовок не найден, мы предупреждаем пользователя о невозможности экспорта списка. В противном случае мы извлекаем и возвращаем ID карты.

Теперь, когда наша функция exportList имеет идентификатор карты, мы можем использовать его для определения идентификатора списка. Если мы посмотрим на документы API , мы можем использовать card/{{ID}} URL card/{{ID}} чтобы получить то, что нам нужно. Чтобы минимизировать объем данных, которые мы просим вернуть Trello, мы также можем ограничить запрос только свойством idList с fields param. Давайте добавим новую команду в background.js .

 if (request.command == 'getCardListId') { trelloInit(); Trello.rest('GET', 'cards/'+request.id, {fields: "idList"}, function(data){ sendResponse(data); }, function (data) { sendResponse(data); }); return true; } 

Нам также нужно определить функцию trelloInit . Это то, что мы можем вызывать каждый раз перед вызовом команды, взаимодействующей с Trello, поэтому токен и ключ установлены правильно, и мы на 100% уверены, что наши запросы аутентифицированы.

 function trelloInit() { Trello.setKey(APP_KEY); Trello.setToken(localStorage.getItem('trello_token')); } 

Теперь мы успешно получаем список ID.

Получение списка карт

С парой строк кода в main.js теперь у нас есть функция exportList которая выглядит следующим образом:

 function exportList(event) { var first_card_id = findFirstCardId(event); if (!first_card_id) { alert('No cards found in the list.'); return false; } chrome.extension.sendMessage({ command: 'getCardListId', id: first_card_id }, function(data){ if (data.idList !== undefined) { chrome.extension.sendMessage({ command: 'getListCards', id: data.idList }, function(data) { console.log(data); }); } }); } 

В «человечине» это:

  • получить идентификатор первой карты
  • если идентификатор не найден, список явно пуст
  • если идентификатор найден, позвоните на фоновую страницу и скажите, чтобы он передал нам идентификатор списка через вызов API Trello.
  • если идентификатор списка в порядке, сделайте еще один вызов фоновой странице, чтобы получить карточки списка, и когда закончите, выведите результат в консоль.

Вернувшись на фоновую страницу, теперь мы можем создать команду getListCards в соответствии с документами API :

 if (request.command == 'getListCards') { trelloInit(); Trello.rest('GET', 'lists/'+request.id+'/cards', {}, function(data){ sendResponse(data); }, function (data) { sendResponse(data); }); return true; } 

Если вы перезагрузите и протестируете расширение сейчас, вы сможете увидеть не только опцию экспорта в ваших списках, но также и данные, которые мы ищем, экспортируемые в консоли после выбора этой опции.

Экспортные форматы

Сейчас мы воспользуемся упрощенным подходом к экспорту, так как этот учебник работает немного дольше, чем сейчас. Мы предложим пользователю выбор из TXT или JSON, с заранее заданной нами формой и формой. Вывод текста пока будет выглядеть так:

 Topic: Test Card 1 Description: This is a description Test Card 2 Test Card 3 Topic: Test Card 4 Description: This is another description Test Card 5 

в то время как JSON будет содержимым, полученным от Trello, т.е.

05

Данные JSON, очевидно, будут давать гораздо больше информации, но ее также гораздо проще редактировать — просто вставьте ее в любую среду IDE или инструмент, такой как JSON Editor Online или JSON в CSV, и все готово.

Для экспорта нам понадобится модальное окно для вставки данных. Привлекательным вариантом здесь является платформа Foundation, так как мы уже используем ее для нашей страницы настроек, и у нее есть собственный модальный всплывающий компонент, но ни CSS Foundation, ни Trello не имеют должного пространства имен и включают CSS Foundation в конфликты причин Trello. У нас также есть предварительно включенный jQuery, но опять же, чтобы запустить и запустить Dialog, нам нужно было бы включить пользовательский интерфейс jQuery, и даже этого было бы недостаточно — расширения Chrome не поддерживают загрузку изображений в CSS через относительный url() синтаксис, который используется в jQuery UI — нам пришлось бы переписать CSS jQuery UI, чтобы использовать локальные URL-адреса расширения или использовать изображения в кодировке base64, оба непривлекательных подхода.

Вместо этого мы создадим наше собственное всплывающее окно и будем использовать некоторые из существующих стилей Trello, игнорируя при этом все конфликты. Я положу окончательный код здесь, а затем объясню это. Пожалуйста, создайте lib/TrelloHelper/js/exportPopup.js и предоставьте ему следующее содержимое:

 var exportPopup; var TrelloExportPopup = function() { } TrelloExportPopup.prototype.init = function() { // When run, this makes sure the popup isn't around. // If it finds the popup residue, it removes it, paving the way for a fresh one. var popoverScan = $('.trello_helper_export_popup'); if ($(popoverScan).length > 0) { $(popoverScan).remove(); } popoverScan = null; // Create our new popup, hidden by default exportPopup = $('<div class="trello_helper_export_popup" style="display: none"></div>'); // Create a header area for the popup, which will contain the buttons / tabs // Create a body area, which will contain the export data var header = $('<div class="trello_helper_export_popup_header"></div>'); var body = $('<div class="trello_helper_export_popup_body"></div>'); // Create areas for exporting the data - simple non-editable textareas var textarea = $('<textarea class="trello_helper_export_popup_textarea exportarea" readonly="true" style="display: none"></textarea>'); var jsonarea = $('<textarea class="trello_helper_export_popup_jsonarea exportarea" readonly="true" style="display: none"></textarea>'); // Create header buttons / tabs var textButton = $('<a href="#" class="exporttab button" data-area="text">Text Export</a>'); var jsonButton = $('<a href="#" class="exporttab button" data-area="json">JSON Export</a>'); var closeButton = $('<a href="#" class="button right">Close</a>'); // Have the close button close our tab, and do the same if the user clicks outside the popup $(closeButton).click(this.hide); // Put everything together $(header).append(jsonButton).append(textButton).append(closeButton); $(body).append(textarea).append(jsonarea); $(exportPopup).append(header).append(body); // Add out popup to the Trello page $("#content").append(exportPopup); // Bind listeners to the buttons / tabs in the header, so we can switch output modes $(".exporttab").click(function (e) { var area = e.currentTarget.dataset.area; $(".exportarea").hide(); $(".trello_helper_export_popup_" + area + "area").show(); }); }; TrelloExportPopup.prototype.hide = function() { // Execute hiding logic only if the popup is visible if ($(".trello_helper_export_popup").is(":visible")) { $(exportPopup).hide(); } }; TrelloExportPopup.prototype.show = function(data) { // Hide all textareas $(".exportarea").hide(); // Show the first one by simulating a click on the first tab // This makes sure our export popup always opens in JSON mode $(".exporttab")[0].click(); var text = ''; var cardCount = data.length; var i = 0; while (i < cardCount) { text += 'Topic: ' + data[i].name; if (data[i].desc) { text += '\nDescription:\n' + data[i].desc; } text += '\n\n\n'; i++; } $(exportPopup).find('.trello_helper_export_popup_textarea').text(text); $(exportPopup).find('.trello_helper_export_popup_jsonarea').text(JSON.stringify(data)); $(exportPopup).show(); }; 

Я решил использовать всплывающую логику вне основного скрипта, чтобы мы могли легко улучшить ее позже. Я также выбрал «объектно-ориентированный» подход, просто потому, что он мне нравится. Мы определяем новый «класс» TrelloExportPopup с тремя методами — init, show и hide. Init будет вызван, как только загрузятся скрипты контента. Это метод, отвечающий за создание всплывающего окна, подключение правильных слушателей событий и добавление всего этого в HTML-код платы Trello. Добавление класса .button к кнопкам в заголовке всплывающего окна гарантирует, что мы получим представление, соответствующее текущему интерфейсу Trello. Внешний вид, который я здесь использую, является своего рода интерфейсом с вкладками — нажмите «Текст», и экспорт текста отобразится, нажмите «JSON» и «JSON» отобразится.

Метод hide скрывает всплывающее окно, но только если оно существует где-то на странице в видимой форме. Метод show автоматически активирует первое (JSON) представление вкладки и заполняет области экспорта необходимыми данными. Область JSON представляет собой простой строковый дамп — вывод данных JSON в виде строки, в то время как текстовая область на данный момент просто выводит заголовок и описание карты в каждой отдельной строке, с двумя пустыми строками между картами — очень «Копировать-вставить дружественный».

Все, что нам нужно сделать сейчас, это немного украсить. Вот содержимое lib/TrelloHelper/css/exportpopup.css :

 .trello_helper_export_popup { background-color: white; z-index: 1000; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); /* Yep! */ width: 48%; min-height: 50%; border-radius: 3px; border: 1px solid #dbdbdb; border-bottom-color: #c2c2c2; box-shadow: 0 1px 6px rgba(0,0,0,.15); } .trello_helper_export_popup_body { position: absolute; right: 0; left: 0; bottom: 0; top: 55px; } .trello_helper_export_popup .button { margin: 10px; } .trello_helper_export_popup .button .right { float: right; } .trello_helper_export_popup textarea { height: 100%; } 

Это гарантирует, что всплывающее окно отцентрировано и выглядит как родное всплывающее окно Trello. Это также гарантирует, что текстовая область, которая будет показывать нам содержимое экспорта, заполняет остальное пространство всплывающего окна. Теперь давайте включим эти файлы в наши скрипты контента:

 "content_scripts": [ { "matches": ["https://trello.com/b/*"], "css": ["lib/TrelloHelper/css/exportpopup.css"], "js": [ "lib/jquery-2.1.1.min.js", "lib/TrelloHelper/js/exportpopup.js", "scripts/main.js" ], "run_at": "document_idle" } ], 

Наконец, давайте main.js с помощью новой всплывающей логики. Финальная версия main.js выглядит так:

 var tep = new TrelloExportPopup(); chrome.extension.sendMessage({}, function (response) { var readyStateCheckInterval = setInterval(function () { if (document.readyState === "complete") { clearInterval(readyStateCheckInterval); var popover = $(".pop-over"); tep.init(); $('.list-header-menu-icon').click(function (event) { var popover_summoned_interval = setInterval(function () { if ($(popover).is(':visible')) { clearInterval(popover_summoned_interval); $(".pop-over .content").append('<hr><ul class="pop-over-list"> <li><a class="js-export-list" href="#">Export This List</a></li> </ul>'); $(".js-export-list").click(function (e) { exportList(event); }); } }, 50); }); } }, 10); }); function exportList(event) { tep.hide(); var first_card_id = findFirstCardId(event); if (!first_card_id) { alert('No cards found in the list.'); return false; } chrome.extension.sendMessage({ command: 'getCardListId', id: first_card_id }, function (data) { if (data.idList !== undefined) { chrome.extension.sendMessage({ command: 'getListCards', id: data.idList }, function (data) { tep.show(data); }); } }); } 

Сначала мы «создаем» экземпляр TrelloExportPopup, чтобы мы могли использовать его методы в нашем коде. Затем, перед тем, как tep.init() прослушиватель события click с меню, мы инициализируем наше всплывающее окно с помощью tep.init() чтобы он был хорош и готов в нашей DOM, прежде чем он понадобится. После нажатия на ссылку «Экспорт» мы вызываем нашу функцию exportList как и раньше.

В функции exportList мы сначала скрываем всплывающее окно с помощью tep.hide() если оно открыто, когда мы просматриваем меню другого списка, а затем, как только мы получаем карточки с нашей фоновой страницы, мы показываем всплывающее окно экспорта. с tep.show(data) . Это оно!

Перезагрузите расширение сейчас, обновите страницу Trello, и у вас должна быть работающая опция экспорта!

01

Ошибки и улучшения

Я специально оставил несколько ошибок и предостережений. Если будет достаточно интереса, мы поговорим о них в следующем посте, настроив и оптимизировав расширение для обеспечения отказоустойчивости. Вот некоторые улучшения, которые все еще возможны:

Кэширование

Чтобы ускорить процесс использования в будущем, мы можем использовать localStorage, чтобы запомнить, к какой плате относится список. Обратите внимание, что это может сломаться, если вы переместите список с доски на доску, поэтому реализуйте эту функцию осторожно — убедитесь, что вы добавили еще один прослушиватель для перемещения списка, чтобы вы могли сделать недействительными кэшированные данные!

Появляется опция дублирования экспорта

Если вы вручную нажмете на значок меню, пока оно еще открыто, вы продолжите добавлять новые опции «Экспорт» в нижней части меню. Должен быть реализован отказоустойчивый, который проверяет, есть ли опция уже там.

Начальные проблемы

На огромных досках с сотнями досок и членов пользовательский интерфейс Trello становится ужасно медленным. Это приводит к пропуску события готовности документа, и часть init нашего скрипта выполняется до того, как появятся какие-либо элементы пользовательского интерфейса для привязки слушателей. Таким образом, меню иногда не имеет опции экспорта и не получает его, пока вы не обновите.

Пеленальные доски

Смена доски убивает текущий пользовательский интерфейс и перестраивает его для новой доски. Проблема, однако, в том, что элементы со слушателями событий тоже убиваются — поэтому наше меню больше не вызывает опцию Export. Как и в случае с вышеуказанной проблемой, при смене платы необходимо инициировать повторную инициализацию, чтобы все работало.

Inifinite loop

Есть астрономически небольшой шанс, что поповер не будет отображаться после щелчка по меню — возможно, Trello изменил что-то в своем пользовательском интерфейсе, возможно, они изменили класс, или, может быть, у них просто есть какая-то ошибка пользовательского интерфейса — в этом случае цикл проверяет его видимость стала бы бесконечной, занимая огромное количество ресурсов процессора, пока процесс вкладки не был убит. Защита от этого была бы хороша.

Вывод

В этой короткой серии мы создали простое расширение Chrome для Trello, которое позволяет экспортировать карточки из заданного списка в виде списка JSON или TXT. Используйте этот пример, чтобы построить его и создать свои собственные расширения Trello — вещи, которые вы можете выполнить, ограничены только вашим воображением (и возможностями, которые предоставляет API Trello :)). Аутентификация уже была решена для вас, и шаблоны логики на месте — начните кодировать!

Код, который мы написали в этой серии руководств, доступен на Github .

Хотели бы вы увидеть продолжение этого урока? Дополнительные функции реализованы? Дай мне знать! Обратная связь приветствуется!