From bb8d0be177c3d98c7e3f26bcf90b6c09aa86cf58 Mon Sep 17 00:00:00 2001 From: Naoki Ishihara Date: Tue, 10 Jun 2014 01:39:59 +0000 Subject: [PATCH 001/374] Initial empty repository From 5d5708f4337f6b5150f9f5d510b42b17f1ab5a91 Mon Sep 17 00:00:00 2001 From: Eric Koleda Date: Wed, 10 Sep 2014 11:23:55 -0400 Subject: [PATCH 002/374] Initial commit --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 + 2 files changed, 203 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..5c304d1a4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 000000000..4bfa3a5b1 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +apps-script-dialog2sidebar +========================== From 206e0cb8db3a740a9a88d4f130a5cde8d99bb7dd Mon Sep 17 00:00:00 2001 From: Eric Koleda Date: Wed, 10 Sep 2014 12:25:40 -0400 Subject: [PATCH 003/374] Initial commit. --- Code.gs | 150 +++++++++++++++++++++++++++++++++++++++++++++++++++ Dialog.html | 52 ++++++++++++++++++ README.md | 27 +++++++++- Sidebar.html | 49 +++++++++++++++++ 4 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 Code.gs create mode 100644 Dialog.html create mode 100644 Sidebar.html diff --git a/Code.gs b/Code.gs new file mode 100644 index 000000000..08710ef03 --- /dev/null +++ b/Code.gs @@ -0,0 +1,150 @@ +/** + * How long to wait for the dialog to check-in before assuming it's been + * closed, in seconds. + */ +var DIALOG_TIMEOUT_SECONDS = 10; + +/** + * The various states the dialog can be in. + */ +var DialogState = { + /** + * The dialog is still open. + */ + OPEN: 'open', + + /** + * The dialog has been closed without being completed. + */ + ABORTED: 'aborted', + + /** + * The dialog hasn't checked-in in a while, so we assume it's been closed + * via the "X" at the top of the dialog. + */ + LOST: 'lost', + + /** + * The dialog has been completed and closed. + */ + DONE: 'done' +}; + +/** + * The various proprties of a dialog we store. + */ +var DialogProperty = { + /** + * The state of the dialog. See DialogState. + */ + STATE: 'state', + + /** + * The timestamp of the last time the dialog checked-in. + */ + LAST_CHECK_IN: 'lastCheckIn' +}; + +/** + * Runs when the document opens, populating the menu. + */ +function onOpen() { + DocumentApp.getUi().createMenu('Sidebar') + .addItem('Show', 'showSidebar') + .addToUi(); +} + +/** + * Shows the sidebar in the document. + */ +function showSidebar() { + var page = HtmlService.createHtmlOutputFromFile('Sidebar') + .setTitle('Sidebar') + .setSandboxMode(HtmlService.SandboxMode.NATIVE); + DocumentApp.getUi().showSidebar(page); +} + +/** + * Shows the dialog in the document. + */ +function showDialog() { + var dialogId = Utilities.base64Encode(Math.random()); + var template = HtmlService.createTemplateFromFile('Dialog'); + template.dialogId = dialogId; + var page = template.evaluate() + .setTitle('Dialog') + .setSandboxMode(HtmlService.SandboxMode.NATIVE); + DocumentApp.getUi().showDialog(page); + checkIn(dialogId); + return dialogId; +} + +/** + * Records the last time a dialog checked-in with the server. The dialog should + * call this function periodically (every few seconds). + * @param {string} dialogId The ID of the dialog. + */ +function checkIn(dialogId) { + var key = getCacheKey_(dialogId, DialogProperty.LAST_CHECK_IN); + var timestamp = new Date().getTime(); + CacheService.getPrivateCache().put(key, timestamp); +} + +/** + * Sets the state of a dialog. The dialog should call this function before + * it closes itself. + * @param {string} dialogId The ID of the dialog. + * @param {string} state The state of the dialog. + */ +function setDialogState(dialogId, state) { + validateDialogState_(state); + var key = getCacheKey_(dialogId, DialogProperty.STATE); + CacheService.getPrivateCache().put(key, state); +} + +/** + * Gets the state of a dialog. The sidebar should call this function periodically + * to determine when the dialog is closed. + * @param {string} dialogId The ID of the dialog. + * @return {string} The state of the dialog. + */ +function getDialogState(dialogId) { + var key = getCacheKey_(dialogId, DialogProperty.STATE); + var status = CacheService.getPrivateCache().get(key); + if (status != null) { + return status; + } else { + var lastCheckInKey = getCacheKey_(dialogId, DialogProperty.LAST_CHECK_IN); + var lastCheckIn = parseInt(CacheService.getPrivateCache().get(lastCheckInKey)); + var now = new Date().getTime(); + if (now - lastCheckIn > DIALOG_TIMEOUT_SECONDS * 1000) { + return DialogState.LOST; + } else { + return DialogState.OPEN; + } + } +} + +/** + * Validates that a given dialog state is valid, and throws an exception if it isn't. + * @param {string} state The dialog state to validate. + * @private + */ +function validateDialogState_(state) { + var validStates = Object.keys(DialogState).map(function(key) { + return DialogState[key]; + }); + if (validStates.indexOf(state) == -1) { + throw 'Invalid dialog state: ' + state; + } +} + +/** + * Gets the cache key for a given property of a dialog. + * @param {string} dialogId The ID of the dialog. + * @param {stirng} property The property of the dialog. + * @return {stirng} The cache key. + */ +function getCacheKey_(dialogId, property) { + return dialogId + '-' + property; +} diff --git a/Dialog.html b/Dialog.html new file mode 100644 index 000000000..a1f6cab98 --- /dev/null +++ b/Dialog.html @@ -0,0 +1,52 @@ + + +
+

Content goes here...

+
+ + +
+
+ + + diff --git a/README.md b/README.md index 4bfa3a5b1..22e8849f9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ -apps-script-dialog2sidebar -========================== +# Dialog to Sidebar Communication in Apps Script + +This script demonstrates a method of setting up a communication channel between +a dialog and a sidebar in Apps Script. This helps solve the common problem +of having your sidebar know when a dialog it spawned is complete, closed, etc. + +Unfortunately Apps Script doesn't support a direct communication channel +across client-side code, nor does it support an eventing framework that allows +the server to notify the client of the change. Therefore this solution relies on +both the sidebar and dialog reguarly polling the backend. + +An overview is the process is as follows: + +* The sidebar requests a new dialog to be spawned. +* The backend generates a new ID for the dialog, opens the dialog (passing in + that ID as a template parameter), and sends the ID back to the sidebar. +* The sidebar regularly polls the backend using the dialog's ID to check for a + state change. +* The dialog regulary "checks in" with the backend, so that the backend knows + it's still open. +* When the user completes the dialog (by clicking either the "OK" or "Cancel" + button) it sends this status change to the backend. +* If the backend notices that the dialog hasn't checked in for some time, it + changes it's status to "lost". +* When the dialog notices a change in the dialog's status it stops polling. diff --git a/Sidebar.html b/Sidebar.html new file mode 100644 index 000000000..f4465978e --- /dev/null +++ b/Sidebar.html @@ -0,0 +1,49 @@ + + + + + + From b222c77bb1016e45273e6197436024d2aee1aa6f Mon Sep 17 00:00:00 2001 From: Eric Koleda Date: Wed, 10 Sep 2014 12:28:55 -0400 Subject: [PATCH 004/374] Fixed typo in README. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 22e8849f9..1ee1dc67c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ of having your sidebar know when a dialog it spawned is complete, closed, etc. Unfortunately Apps Script doesn't support a direct communication channel across client-side code, nor does it support an eventing framework that allows the server to notify the client of the change. Therefore this solution relies on -both the sidebar and dialog reguarly polling the backend. +both the sidebar and dialog regularly polling the backend. An overview is the process is as follows: @@ -16,7 +16,7 @@ An overview is the process is as follows: that ID as a template parameter), and sends the ID back to the sidebar. * The sidebar regularly polls the backend using the dialog's ID to check for a state change. -* The dialog regulary "checks in" with the backend, so that the backend knows +* The dialog regularly "checks in" with the backend, so that the backend knows it's still open. * When the user completes the dialog (by clicking either the "OK" or "Cancel" button) it sends this status change to the backend. From 9c6c3639492c845db54848e415611fe0be7bcdb1 Mon Sep 17 00:00:00 2001 From: Eric Koleda Date: Wed, 10 Sep 2014 12:33:45 -0400 Subject: [PATCH 005/374] Updated Add-on CSS link. --- Dialog.html | 2 +- Sidebar.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dialog.html b/Dialog.html index a1f6cab98..3957d0ba1 100644 --- a/Dialog.html +++ b/Dialog.html @@ -1,4 +1,4 @@ - +

Content goes here...

diff --git a/Sidebar.html b/Sidebar.html index f4465978e..27f415f96 100644 --- a/Sidebar.html +++ b/Sidebar.html @@ -1,4 +1,4 @@ - +
@@ -120,6 +125,9 @@ $('#creator-email').val(settings.creatorEmail); $('#response-step').val(!settings.responseStep ? 10 : settings.responseStep); + $('#submit-subject').val(!settings.responseSubject ? + 'Thank you for filling out our form!' : + settings.responseSubject); $('#submit-notice').val(!settings.responseText ? 'Thank you for responding to our form!' : settings.responseText); @@ -215,6 +223,7 @@ // Only save respondent options if notify is turned on if (respondentNotify) { settings.respondentEmailItemId = $('#respondent-email').val(); + settings.responseSubject = $('#submit-subject').val(); settings.responseText = $('#submit-notice').val(); } From a86c846a4b53a76d52a63917edd1297883d5a5ef Mon Sep 17 00:00:00 2001 From: Ryan Roth Date: Mon, 15 Dec 2014 17:33:47 -0500 Subject: [PATCH 011/374] Revised Web app template. Change-Id: I9b67f9688b10fcf8b2e4597bd9e3e8a93a1a1acf --- web-app/Code.gs | 60 +++++++++++++++++++++++++++++++++++++++++ web-app/Index.html | 26 ++++++++++++++++++ web-app/JavaScript.html | 36 +++++++++++++++++++++++++ web-app/README.md | 12 +++++++++ web-app/Stylesheet.html | 18 +++++++++++++ 5 files changed, 152 insertions(+) create mode 100644 web-app/Code.gs create mode 100644 web-app/Index.html create mode 100644 web-app/JavaScript.html create mode 100644 web-app/README.md create mode 100644 web-app/Stylesheet.html diff --git a/web-app/Code.gs b/web-app/Code.gs new file mode 100644 index 000000000..6c7d6e847 --- /dev/null +++ b/web-app/Code.gs @@ -0,0 +1,60 @@ +/** + * Serves HTML of the application for HTTP GET requests. + * If folderId is provided as a URL parameter, the web app will list + * the contents of that folder (if permissions allow). Otherwise + * the web app will list the contents of the root folder. + * + * @param {Object} e event parameter that can contain information + * about any URL parameters provided. + */ +function doGet(e) { + var template = HtmlService.createTemplateFromFile('Index'); + + // Retrieve and process any URL parameters, as necessary. + if (e.parameter.folderId) { + template.folderId = e.parameter.folderId; + } else { + template.folderId = 'root'; + } + + // Build and return HTML in IFRAME sandbox mode. + return template.evaluate() + .setTitle('Web App Window Title') + .setSandboxMode(HtmlService.SandboxMode.IFRAME); +} + +/** + * Return an array of up to 20 filenames contained in the + * folder previously specified (or the root folder by default). + * + * @param {String} folderId String ID of folder whose contents + * are to be retrieved; if this is 'root', the + * root folder is used. + * @return {Object} list of content filenames, along with + * the root folder name. + */ +function getFolderContents(folderId) { + var topFolder; + var contents = { + children: [] + }; + + if (folderId == 'root') { + topFolder = DriveApp.getRootFolder(); + } else { + // May throw exception if the folderId is invalid or app + // doesn't have permission to access. + topFolder = DriveApp.getFolderById(folderId); + } + contents.rootName = topFolder.getName() + '/'; + + var files = topFolder.getFiles(); + var numFiles = 0; + while (files.hasNext() && numFiles < 20) { + var file = files.next(); + contents.children.push(file.getName()); + numFiles++; + } + + return contents; +} diff --git a/web-app/Index.html b/web-app/Index.html new file mode 100644 index 000000000..6b3621393 --- /dev/null +++ b/web-app/Index.html @@ -0,0 +1,26 @@ + + + + +

+ To read a specific folder's contents, include that folder's ID as + a URL parameter: ?folderId=<folder id>. Otherwise the root folder + will be read. Up to 20 files may be displayed here, in no particular order. +

+

Loading...

+
+ +
+ + + + + + + + diff --git a/web-app/JavaScript.html b/web-app/JavaScript.html new file mode 100644 index 000000000..d6951e251 --- /dev/null +++ b/web-app/JavaScript.html @@ -0,0 +1,36 @@ + + diff --git a/web-app/README.md b/web-app/README.md new file mode 100644 index 000000000..70378b4f8 --- /dev/null +++ b/web-app/README.md @@ -0,0 +1,12 @@ +Template: Script as Web App +=========================== + +This template provides a framework for creating a [web app](https://developers.google.com/apps-script/guides/web). + +It shows the basic structure needed to define a UI and how to coordinate +communication between the client and server. This template also includes some +useful aspects of Apps Script, including: + +* Using [Templated HTML](https://developers.google.com/apps-script/guides/html/templates) +* Responding to HTTP GET requests with doGet(e) +* Using IFRAME sandbox mode diff --git a/web-app/Stylesheet.html b/web-app/Stylesheet.html new file mode 100644 index 000000000..db0b1e0cb --- /dev/null +++ b/web-app/Stylesheet.html @@ -0,0 +1,18 @@ + + + + From a0ef0f025bc11b09d510546992722eb04f01478e Mon Sep 17 00:00:00 2001 From: Ryan Roth Date: Wed, 19 Nov 2014 22:15:50 -0500 Subject: [PATCH 012/374] Initial Maestro templates. See the development files here: go/maestro-templates-development Change-Id: Ic10384485b5e9409825e6f6e98a17906fbc6b999 --- CONTRIB.md | 35 +++++ LICENSE | 201 ++++++++++++++++++++++++++++ README.md | 96 +++++++++++++ custom-functions/Code.gs | 126 +++++++++++++++++ custom-functions/README.md | 23 ++++ docs-addon/Code.gs | 93 +++++++++++++ docs-addon/Dialog.html | 20 +++ docs-addon/DialogJavaScript.html | 70 ++++++++++ docs-addon/README.md | 23 ++++ docs-addon/Sidebar.html | 27 ++++ docs-addon/SidebarJavaScript.html | 69 ++++++++++ docs-addon/Stylesheet.html | 54 ++++++++ forms-addon/Code.gs | 148 ++++++++++++++++++++ forms-addon/Dialog.html | 24 ++++ forms-addon/DialogJavaScript.html | 79 +++++++++++ forms-addon/README.md | 19 +++ forms-addon/Sidebar.html | 33 +++++ forms-addon/SidebarJavaScript.html | 59 ++++++++ forms-addon/Stylesheet.html | 41 ++++++ packaging.yaml | 15 +++ sheets-addon/Code.gs | 93 +++++++++++++ sheets-addon/Dialog.html | 25 ++++ sheets-addon/DialogJavaScript.html | 59 ++++++++ sheets-addon/README.md | 27 ++++ sheets-addon/Sidebar.html | 29 ++++ sheets-addon/SidebarJavaScript.html | 87 ++++++++++++ sheets-addon/Stylesheet.html | 43 ++++++ 27 files changed, 1618 insertions(+) create mode 100644 CONTRIB.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 custom-functions/Code.gs create mode 100644 custom-functions/README.md create mode 100644 docs-addon/Code.gs create mode 100644 docs-addon/Dialog.html create mode 100644 docs-addon/DialogJavaScript.html create mode 100644 docs-addon/README.md create mode 100644 docs-addon/Sidebar.html create mode 100644 docs-addon/SidebarJavaScript.html create mode 100644 docs-addon/Stylesheet.html create mode 100644 forms-addon/Code.gs create mode 100644 forms-addon/Dialog.html create mode 100644 forms-addon/DialogJavaScript.html create mode 100644 forms-addon/README.md create mode 100644 forms-addon/Sidebar.html create mode 100644 forms-addon/SidebarJavaScript.html create mode 100644 forms-addon/Stylesheet.html create mode 100644 packaging.yaml create mode 100644 sheets-addon/Code.gs create mode 100644 sheets-addon/Dialog.html create mode 100644 sheets-addon/DialogJavaScript.html create mode 100644 sheets-addon/README.md create mode 100644 sheets-addon/Sidebar.html create mode 100644 sheets-addon/SidebarJavaScript.html create mode 100644 sheets-addon/Stylesheet.html diff --git a/CONTRIB.md b/CONTRIB.md new file mode 100644 index 000000000..6736efd94 --- /dev/null +++ b/CONTRIB.md @@ -0,0 +1,35 @@ +# How to become a contributor and submit your own code + +## Contributor License Agreements + +We'd love to accept your sample apps and patches! Before we can take them, we +have to jump a couple of legal hurdles. + +Please fill out either the individual or corporate Contributor License Agreement +(CLA). + + * If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an [individual CLA] + (https://developers.google.com/open-source/cla/individual). + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate CLA] + (https://developers.google.com/open-source/cla/corporate). + +Follow either of the two links above to access the appropriate CLA and +instructions for how to sign and return it. Once we receive it, we'll be able to +accept your pull requests. + +## Contributing A Patch + +1. Submit an issue describing your proposed change to the repo in question. +1. The repo owner will respond to your issue promptly. +1. If your proposed change is accepted, and you haven't already done so, sign a + Contributor License Agreement (see details above). +1. Fork the desired repo, develop and test your code changes. +1. Ensure that your code adheres to the existing style in the sample to which + you are contributing. Refer to the + [Google Cloud Platform Samples Style Guide] + (https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the + recommended coding standards for this organization. +1. Ensure that your code has an appropriate set of unit tests which all pass. +1. Submit a pull request. diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..c02ca2f7a --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2014 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 000000000..defe19c8f --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +Templates for Google Apps Script +================================ + +Templates that provide an initial, working framework for Apps Script +projects. + +Introduction +------------ + +Google Apps Script allows developers to extend and maniplate Google +Docs, Sheets and Forms. For those just starting with Apps Script, it +can be useful to have a template to work from -- a framework that +developers can learn from and modify to suit their needs. + +This collection hosts the following templates: + +* Custom Functions for Sheets +* Google Docs Add-on +* Google Sheets Add-on +* Google Forms Add-on +* Script as Web App + + +Within these templates the following Google Apps Script concepts are +illustrated: + +* [Dialogs and Sidebars](https://developers.google.com/apps-script/guides/dialogs) +* Using [Templated HTML](https://developers.google.com/apps-script/guides/html/templates) +* Responding to HTTP GET requests with doGet(e) +* Using IFRAME sandbox mode + + +Getting Started +--------------- + +Templates can be accessed from the Apps Script editor Welcome Screen +(which is shown when the editor is first opened or by clicking the +"Help > Welcome screen" menu item. Selecting a template from the +Welcome Screen will create a new project pre-populated with the code +you need to get started. + +Alternatively, the code provided in this repository can be manually copied +into the Apps Script editor. Note that certain templates need to be used +in a container-bound script (that is, the template is meant to be in a +script attached to a Doc, Sheet or Form, rather than a standalone script). + + + +Learn more +---------- + +To continue learning about how to extend Google Docs, Sheets and Forms +with Apps Script, take a look at the following resources: + +* [Overview of Apps Script](https://developers.google.com/apps-script/overview) +* [Guide to Add-ons](https://developers.google.com/apps-script/add-ons/) + + +Support +------- +If you've found an error in this sample, please file an issue: +https://github.com/googlesamples/apps-script-templates/issues + +Patches are encouraged, and may be submitted by forking this project and +submitting a pull request through GitHub. + + +General questions about Apps Script can be asked on +StackOverflow: [google-apps-script on StackOverflow](http://stackoverflow.com/questions/tagged/google-apps-script) + + +General Apps Script bug reports or feature requests should be directed to the +Apps Script Issue Tracker: [google-apps-script-issues](https://code.google.com/p/google-apps-script-issues/issues/list) + + +License +------- + +Copyright 2015 Google, Inc. + +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. diff --git a/custom-functions/Code.gs b/custom-functions/Code.gs new file mode 100644 index 000000000..4f262953e --- /dev/null +++ b/custom-functions/Code.gs @@ -0,0 +1,126 @@ +/** + * @OnlyCurrentDoc Limits the script to only accessing the current spreadsheet. + */ + +/** + * A function that takes a single input value and returns a single value. + * Returns a simple concatenation of Strings. + * + * @param {String} name A name to greet. + * @return {String} A greeting. + * @customfunction + */ +function SAY_HELLO(name) { + return 'Hello ' + name; +} + +/** + * A function that takes an input cell or range of cells and returns a cell or + * range of cells. + * Returns a range with all the input values incremented by one. + * + * @param {Array} input The range of numbers to increment. + * @return {Array} The incremented values. + * @customfunction + */ +function INCREMENT(input) { + if (input instanceof Array) { + // Recurse to process an array. + return input.map(INCREMENT); + } else if (!(typeof input == 'number')) { + throw 'Input contains a cell value that is not a number'; + } + // Otherwise process as a single value. + return input + 1; +} + +/** + * A function that takes an range of values and returns a single value. + * Returns the sum the corner values in the range; for a single cell, + * this is equal to (4 * the cell value). + * + * @param {Array} input The Range of numbers to sum the corners of. + * @return {Number} The calculated sum. + * @customfunction + */ +function CORNER_SUM(input) { + if (!(input instanceof Array)) { + // Handle non-range inputs by putting them in an array. + return CORNER_SUM([[input]]); + } + // Range processing here. + var maxRowIndex = input.length - 1; + var maxColIndex = input[0].length - 1; + return input[0][0] + input[0][maxColIndex] + + input[maxRowIndex][0] + input[maxRowIndex][maxColIndex]; +} + +/** + * A function that takes a single value and returns a range of values. + * Returns a range consisting of the first 10 powers and roots of that + * number (with column headers). + * + * @param {Number} input The number to calculate from. + * @return {Array} The first ten powers and roots of that number, + * with associated labels. + * @customfunction + */ +function POWERS_AND_ROOTS(input) { + if (input instanceof Array) { + throw 'Invalid: Range input not permitted'; + } + // Value processing and range generation here. + var headers = ['x', input + '^x', input + '^(1/x)']; + var result = [headers]; + for (var i = 1; i <= 10; i++) { + result.push([i, Math.pow(input, i), Math.pow(input, 1/i)]); + } + return result; +} + +/** + * A function that takes a single input cell that is Date- or Date time-formatted. + * Returns the day of the year represented by the provided date. + * + * @param {Date} date A Date to examine. + * @return {Number} The day of year for that date. + * @customfunction + */ +function GET_DAY_OF_YEAR(date) { + if (!(date instanceof Date)) { + throw 'Invalid: Date input required'; + } + // Date processing here. + var firstOfYear = new Date(date.getFullYear(), 0, 0); + var diff = date - firstOfYear; + var oneDay = 1000 * 60 * 60 * 24; + return Math.floor(diff / oneDay); +} + +/** + * A function that takes a single input cell that is Duration-formatted. + * Returns the number of seconds measured by that duration. + * + * @param {Date} duration A duration to convert. + * @return {Number} Number of seconds in that duration. + * @customfunction + */ +function CONVERT_DURATION_TO_SECONDS(duration) { + if (!(duration instanceof Date)) { + throw 'Invalid: Duration input required'; + } + + // Getting elapsed times from duration-formatted cells in Sheets requires + // subtracting the reference date from the cell value (while correcting for + // timezones). + var spreadsheetTimezone = + SpreadsheetApp.getActiveSpreadsheet().getSpreadsheetTimeZone(); + var dateString = Utilities.formatDate(duration, spreadsheetTimezone, + 'EEE, d MMM yyyy HH:mm:ss'); + var date = new Date(dateString); + var epoch = new Date('Dec 30, 1899 00:00:00'); + var durationInMilliseconds = date.getTime() - epoch.getTime(); + + // Duration processing here. + return Math.round(durationInMilliseconds / 1000); +} diff --git a/custom-functions/README.md b/custom-functions/README.md new file mode 100644 index 000000000..77c54aac9 --- /dev/null +++ b/custom-functions/README.md @@ -0,0 +1,23 @@ +Template: Custom Functions for Sheets +===================================== + +This template provides a framework for creating custom functions +in Google Sheets. It shows the structure needed to define a +custom function and its autocomplete documentation, and provides +a few examples. + +The examples provided here demonstrate: + +* How to create custom functions that accept arguments of different types +* How to document a custom function to generate correct autocomplete + information in Sheets + +Note that this template must be added to a container-bound script +attached to a Google Sheet in order to function. + + +For more information, see [Custom Functions in Google Sheets](https://developers.google.com/apps-script/guides/sheets/functions). + +In addition, developers of Sheets custom functions should be aware of +the [known issues specific to Google Sheets](https://developers.google.com/apps-script/migration/sheets). + diff --git a/docs-addon/Code.gs b/docs-addon/Code.gs new file mode 100644 index 000000000..b732890fa --- /dev/null +++ b/docs-addon/Code.gs @@ -0,0 +1,93 @@ +/** + * @OnlyCurrentDoc Limits the script to only accessing the current document. + */ + +var DIALOG_TITLE = 'Example Dialog'; +var SIDEBAR_TITLE = 'Example Sidebar'; + +/** + * Adds a custom menu with items to show the sidebar and dialog. + * + * @param {Object} e The event parameter for a simple onOpen trigger. + */ +function onOpen(e) { + DocumentApp.getUi() + .createAddonMenu() + .addItem('Show sidebar', 'showSidebar') + .addItem('Show dialog', 'showDialog') + .addToUi(); +} + +/** + * Runs when the add-on is installed; calls onOpen() to ensure menu creation and + * any other initializion work is done immediately. + * + * @param {Object} e The event parameter for a simple onInstall trigger. + */ +function onInstall(e) { + onOpen(e); +} + +/** + * Opens a sidebar. The sidebar structure is described in the Sidebar.html + * project file. + */ +function showSidebar() { + var ui = HtmlService.createTemplateFromFile('Sidebar') + .evaluate() + .setTitle(SIDEBAR_TITLE); + DocumentApp.getUi().showSidebar(ui); +} + +/** + * Opens a dialog. The dialog structure is described in the Dialog.html + * project file. + */ +function showDialog() { + var ui = HtmlService.createTemplateFromFile('Dialog') + .evaluate() + .setWidth(400) + .setHeight(130); + DocumentApp.getUi().showModalDialog(ui, DIALOG_TITLE); +} + +/** + * Returns the existing footer text (if any). + * + * @return {String} existing document footer text (as a plain string). + */ +function getFooterText() { + // Retrieve and return the information requested by the sidebar. + return DocumentApp.getActiveDocument().getFooter().getText(); +} + +/** + * Replaces the current document footer with the given text. + * + * @param {String} footerText text collected from the client-side + * sidebar. + */ +function setFooterText(footerText) { + // Use data collected from sidebar to manipulate the document. + DocumentApp.getActiveDocument().getFooter().setText(footerText); +} + +/** + * Returns the document title. + * + * @return {String} the current document title. + */ +function getDocTitle() { + // Retrieve and return the information requested by the dialog. + return DocumentApp.getActiveDocument().getName(); +} + +/** + * Changes the document title. + * + * @param {String} title the new title to use for the document. + */ +function setDocTitle(title) { + // Use data collected from dialog to manipulate the document. + DocumentApp.getActiveDocument().setName(title); +} diff --git a/docs-addon/Dialog.html b/docs-addon/Dialog.html new file mode 100644 index 000000000..dc030b6df --- /dev/null +++ b/docs-addon/Dialog.html @@ -0,0 +1,20 @@ + + + + +
+
+ + +
+
+ + +
+
+
+ + + diff --git a/docs-addon/DialogJavaScript.html b/docs-addon/DialogJavaScript.html new file mode 100644 index 000000000..bd98d07b6 --- /dev/null +++ b/docs-addon/DialogJavaScript.html @@ -0,0 +1,70 @@ + + diff --git a/docs-addon/README.md b/docs-addon/README.md new file mode 100644 index 000000000..c918cb16e --- /dev/null +++ b/docs-addon/README.md @@ -0,0 +1,23 @@ +Template: Google Docs Add-on +============================ + +This template provides a framework for creating a +[Google Docs add-on](https://developers.google.com/apps-script/add-ons/). +It shows the structure needed to define a UI (including +menus, a sidebar and dialog) and how to coordinate communication +between the UI client and the server where the Doc resides. This +template also covers some basic uses of Apps Script with Google +Docs, including: + +* Reading and writing text to and from a Google Doc +* Getting and modifying basic file information, such as the file title + +Note that add-ons that work with Google Docs will usually need to read +and manipulate the (sometimes complex) +[Doc structure](https://developers.google.com/apps-script/guides/docs#structure_of_a_document). + +Finally, note that this template must be added to a container-bound +script attached to a Google Doc in order to function. Developed +add-ons must go through a +[publishing process](https://developers.google.com/apps-script/add-ons/publish) +before they can be made available publicly. diff --git a/docs-addon/Sidebar.html b/docs-addon/Sidebar.html new file mode 100644 index 000000000..9ff73261c --- /dev/null +++ b/docs-addon/Sidebar.html @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/docs-addon/SidebarJavaScript.html b/docs-addon/SidebarJavaScript.html new file mode 100644 index 000000000..8be9ba697 --- /dev/null +++ b/docs-addon/SidebarJavaScript.html @@ -0,0 +1,69 @@ + + diff --git a/docs-addon/Stylesheet.html b/docs-addon/Stylesheet.html new file mode 100644 index 000000000..fd416a337 --- /dev/null +++ b/docs-addon/Stylesheet.html @@ -0,0 +1,54 @@ + + + + diff --git a/forms-addon/Code.gs b/forms-addon/Code.gs new file mode 100644 index 000000000..956ca332d --- /dev/null +++ b/forms-addon/Code.gs @@ -0,0 +1,148 @@ +/** + * @OnlyCurrentDoc Limits the script to only accessing the current form. + */ + +var DIALOG_TITLE = 'Example Dialog'; +var SIDEBAR_TITLE = 'Example Sidebar'; + +/** + * Adds a custom menu with items to show the sidebar and dialog. + * + * @param {Object} e The event parameter for a simple onOpen trigger. + */ +function onOpen(e) { + FormApp.getUi() + .createAddonMenu() + .addItem('Show sidebar', 'showSidebar') + .addItem('Show dialog', 'showDialog') + .addToUi(); +} + +/** + * Runs when the add-on is installed; calls onOpen() to ensure menu creation and + * any other initializion work is done immediately. + * + * @param {Object} e The event parameter for a simple onInstall trigger. + */ +function onInstall(e) { + onOpen(e); +} + +/** + * Opens a sidebar. The sidebar structure is described in the Sidebar.html + * project file. + */ +function showSidebar() { + var ui = HtmlService.createTemplateFromFile('Sidebar') + .evaluate() + .setTitle(SIDEBAR_TITLE); + FormApp.getUi().showSidebar(ui); +} + +/** + * Opens a dialog. The dialog structure is described in the Dialog.html + * project file. + */ +function showDialog() { + var ui = HtmlService.createTemplateFromFile('Dialog') + .evaluate() + .setWidth(350) + .setHeight(170); + FormApp.getUi().showModalDialog(ui, DIALOG_TITLE); +} + +/** + * Appends a new form item to the current form. + * + * @param {Object} itemData a collection of String data used to + * determine the exact form item created. + */ +function addFormItem(itemData) { + // Use data collected from sidebar to manipulate the form. + var form = FormApp.getActiveForm(); + switch (itemData.type) { + case 'Date': + form.addDateItem().setTitle(itemData.name); + break; + case 'Scale': + form.addScaleItem().setTitle(itemData.name); + break; + case 'Text': + form.addTextItem().setTitle(itemData.name); + break; + } +} + +/** + * Queries the form DocumentProperties to determine whether the formResponse + * trigger is enabled or not. + * + * @return {Boolean} True if the form submit trigger is enabled; false + * otherwise. + */ +function getTriggerState() { + // Retrieve and return the information requested by the dialog. + var properties = PropertiesService.getDocumentProperties(); + return properties.getProperty('triggerId') != null; +} + +/** + * Turns the form submit trigger on or off based on the given argument. + * + * @param {Boolean} enableTrigger whether to turn on the form submit + * trigger or not + */ +function adjustFormSubmitTrigger(enableTrigger) { + // Use data collected from dialog to manipulate form. + + // Determine existing state of trigger on the server. + var form = FormApp.getActiveForm(); + var properties = PropertiesService.getDocumentProperties(); + var triggerId = properties.getProperty('triggerId'); + + if (!enableTrigger && triggerId != null) { + // Delete the existing trigger. + var triggers = ScriptApp.getUserTriggers(form); + for (var i = 0; i < triggers.length; i++) { + if (triggers[i].getUniqueId() == triggerId) { + ScriptApp.deleteTrigger(triggers[i]); + break; + } + } + properties.deleteProperty('triggerId'); + } else if (enableTrigger && triggerId == null) { + // Create a new trigger. + var trigger = ScriptApp.newTrigger('respondToFormSubmit') + .forForm(form) + .onFormSubmit() + .create(); + properties.setProperty('triggerId', trigger.getUniqueId()); + } +} + +/** + * Responds to form submit events if a form summit trigger is enabled. + * Collects some form information and sends it as an email to the form creator. + * + * @param {Object} e The event parameter created by a form + * submission; see + * https://developers.google.com/apps-script/understanding_events + */ +function respondToFormSubmit(e) { + if (MailApp.getRemainingDailyQuota() > 0) { + var form = FormApp.getActiveForm(); + var message = 'There have been ' + form.getResponses().length + + ' response(s) so far. Latest Response:\n'; + var itemResponses = e.response.getItemResponses(); + for (var i = 0; i < itemResponses.length; i++) { + var itemTitle = itemResponses[i].getItem().getTitle(); + var itemResponse = JSON.stringify(itemResponses[i].getResponse()); + message += itemTitle + ': ' + itemResponse + '\n'; + } + MailApp.sendEmail( + Session.getEffectiveUser().getEmail(), + 'Form response received for form ' + form.getTitle(), + message, + {name: 'Forms Add-on Template'}); + } +} diff --git a/forms-addon/Dialog.html b/forms-addon/Dialog.html new file mode 100644 index 000000000..fa520cb8c --- /dev/null +++ b/forms-addon/Dialog.html @@ -0,0 +1,24 @@ + + + + +
+
+

+ This dialog is used to turn on or off a form submit trigger that will + send an email to the form owner whenever a form is responded to. +

+
+ + +
+
+ + +
+
+
+
+ + + diff --git a/forms-addon/DialogJavaScript.html b/forms-addon/DialogJavaScript.html new file mode 100644 index 000000000..621b74914 --- /dev/null +++ b/forms-addon/DialogJavaScript.html @@ -0,0 +1,79 @@ + + diff --git a/forms-addon/README.md b/forms-addon/README.md new file mode 100644 index 000000000..54a795445 --- /dev/null +++ b/forms-addon/README.md @@ -0,0 +1,19 @@ +Template: Google Forms Add-on +============================= + +This template provides a framework for creating a +[Google Forms add-on](https://developers.google.com/apps-script/add-ons/). +It shows the structure needed to define a UI (including +menus, a sidebar and dialog) and how to coordinate communication +between the UI client and the server where the Form resides. This +template also covers some basic uses of Apps Script with Google +Forms, including: + +* Creating new form items programmatically +* Setting, removing and responding to form submit triggers + +Note that this template must be added to a container-bound script +attached to a Google Form in order to function. Also note that +developed add-ons must go through a +[publishing process](https://developers.google.com/apps-script/add-ons/publish) +before they can be made available publicly. diff --git a/forms-addon/Sidebar.html b/forms-addon/Sidebar.html new file mode 100644 index 000000000..b01ec682a --- /dev/null +++ b/forms-addon/Sidebar.html @@ -0,0 +1,33 @@ + + + + + + + + + + diff --git a/forms-addon/SidebarJavaScript.html b/forms-addon/SidebarJavaScript.html new file mode 100644 index 000000000..dfb38f9eb --- /dev/null +++ b/forms-addon/SidebarJavaScript.html @@ -0,0 +1,59 @@ + + diff --git a/forms-addon/Stylesheet.html b/forms-addon/Stylesheet.html new file mode 100644 index 000000000..e444d1cf4 --- /dev/null +++ b/forms-addon/Stylesheet.html @@ -0,0 +1,41 @@ + + + + diff --git a/packaging.yaml b/packaging.yaml new file mode 100644 index 000000000..ecf833de1 --- /dev/null +++ b/packaging.yaml @@ -0,0 +1,15 @@ +# GOOGLE SAMPLE PACKAGING DATA +# +# This file is used by Google as part of our samples packaging process. +# End users may safely ignore this file. It has no relevance to other systems. +--- +status: DRAFT +technologies: [Google Apps] +categories: [Web Services] +languages: [Google Apps Script] +solutions: [Web Developer] +github: googlesamples/apps-script-templates +difficulty: NOVICE +api_refs: [] +dev_console_apis: [] +license: apache2 diff --git a/sheets-addon/Code.gs b/sheets-addon/Code.gs new file mode 100644 index 000000000..29de19992 --- /dev/null +++ b/sheets-addon/Code.gs @@ -0,0 +1,93 @@ +/** + * @OnlyCurrentDoc Limits the script to only accessing the current spreadsheet. + */ + +var DIALOG_TITLE = 'Example Dialog'; +var SIDEBAR_TITLE = 'Example Sidebar'; + +/** + * Adds a custom menu with items to show the sidebar and dialog. + * + * @param {Object} e The event parameter for a simple onOpen trigger. + */ +function onOpen(e) { + SpreadsheetApp.getUi() + .createAddonMenu() + .addItem('Show sidebar', 'showSidebar') + .addItem('Show dialog', 'showDialog') + .addToUi(); +} + +/** + * Runs when the add-on is installed; calls onOpen() to ensure menu creation and + * any other initializion work is done immediately. + * + * @param {Object} e The event parameter for a simple onInstall trigger. + */ +function onInstall(e) { + onOpen(e); +} + +/** + * Opens a sidebar. The sidebar structure is described in the Sidebar.html + * project file. + */ +function showSidebar() { + var ui = HtmlService.createTemplateFromFile('Sidebar') + .evaluate() + .setTitle(SIDEBAR_TITLE); + SpreadsheetApp.getUi().showSidebar(ui); +} + +/** + * Opens a dialog. The dialog structure is described in the Dialog.html + * project file. + */ +function showDialog() { + var ui = HtmlService.createTemplateFromFile('Dialog') + .evaluate() + .setWidth(400) + .setHeight(190); + SpreadsheetApp.getUi().showModalDialog(ui, DIALOG_TITLE); +} + +/** + * Returns the value in the active cell. + * + * @return {String} The value of the active cell. + */ +function getActiveValue() { + // Retrieve and return the information requested by the sidebar. + var cell = SpreadsheetApp.getActiveSheet().getActiveCell(); + return cell.getValue(); +} + +/** + * Replaces the active cell value with the given value. + * + * @param {Number} value A reference number to replace with. + */ +function setActiveValue(value) { + // Use data collected from sidebar to manipulate the sheet. + var cell = SpreadsheetApp.getActiveSheet().getActiveCell(); + cell.setValue(value); +} + +/** + * Executes the specified action (create a new sheet, copy the active sheet, or + * clear the current sheet). + * + * @param {String} action An identifier for the action to take. + */ +function modifySheets(action) { + // Use data collected from dialog to manipulate the spreadsheet. + var ss = SpreadsheetApp.getActiveSpreadsheet(); + var currentSheet = ss.getActiveSheet(); + if (action == "create") { + ss.insertSheet(); + } else if (action == "copy") { + currentSheet.copyTo(ss); + } else if (action == "clear") { + currentSheet.clear(); + } +} diff --git a/sheets-addon/Dialog.html b/sheets-addon/Dialog.html new file mode 100644 index 000000000..0da04ae66 --- /dev/null +++ b/sheets-addon/Dialog.html @@ -0,0 +1,25 @@ + + + + +
+

This allows you to create, copy and clear sheets.

+
+ + +
+
+ + +
+
+
+ + + diff --git a/sheets-addon/DialogJavaScript.html b/sheets-addon/DialogJavaScript.html new file mode 100644 index 000000000..b6c42e9b3 --- /dev/null +++ b/sheets-addon/DialogJavaScript.html @@ -0,0 +1,59 @@ + + diff --git a/sheets-addon/README.md b/sheets-addon/README.md new file mode 100644 index 000000000..f2231870d --- /dev/null +++ b/sheets-addon/README.md @@ -0,0 +1,27 @@ +Template: Google Sheets Add-on +============================== + +This template provides a framework for creating a +[Google Sheets add-on](https://developers.google.com/apps-script/add-ons/). +It shows the structure needed to define a UI (including menus, a sidebar and +dialog) and how to coordinate communication between the UI client and the server +where the Sheet resides. This template also covers some basic uses of Apps +Script with Google Sheets, including: + +* Reading and writing data to a Sheet +* Creating, copying and clearing a sheet + +Note that add-ons that work with Google Sheets will usually need to +read and manipulate the Sheet data, formatting, validation, etc. For +more information, see +[Extending Google Sheets](https://developers.google.com/apps-script/guides/sheets). + +In addition, developers of Sheets add-ons should be aware of the +[Known Issues specific to Google Sheets](https://developers.google.com/apps-script/migration/sheets). + +Finally, note that this template must be added to a container-bound +script attached to a Google Sheet in order to function. Developed +add-ons must go through a +[publishing process](https://developers.google.com/apps-script/add-ons/publish) +before they can be made available publicly. + diff --git a/sheets-addon/Sidebar.html b/sheets-addon/Sidebar.html new file mode 100644 index 000000000..c27b1d087 --- /dev/null +++ b/sheets-addon/Sidebar.html @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/sheets-addon/SidebarJavaScript.html b/sheets-addon/SidebarJavaScript.html new file mode 100644 index 000000000..bb71444b7 --- /dev/null +++ b/sheets-addon/SidebarJavaScript.html @@ -0,0 +1,87 @@ + + diff --git a/sheets-addon/Stylesheet.html b/sheets-addon/Stylesheet.html new file mode 100644 index 000000000..218d37015 --- /dev/null +++ b/sheets-addon/Stylesheet.html @@ -0,0 +1,43 @@ + + + + From 4fe3826045bc371af6936aeb8f0a12e5bf7aff6a Mon Sep 17 00:00:00 2001 From: Ryan Roth Date: Tue, 10 Feb 2015 11:48:32 -0500 Subject: [PATCH 013/374] Fixing font used in dialog. Change-Id: I9c3d7c08f537945a436ce8112c286d220f1347c5 --- About.html | 1 + 1 file changed, 1 insertion(+) diff --git a/About.html b/About.html index 6d2d5eecd..4dd076ad9 100644 --- a/About.html +++ b/About.html @@ -1,3 +1,4 @@ +

Form Notifications was created as an sample add-on, and is meant From 46ba6d2c3fccd62b96c43a2d6af507cf82eca073 Mon Sep 17 00:00:00 2001 From: Ryan Roth Date: Wed, 11 Mar 2015 11:10:07 -0400 Subject: [PATCH 014/374] Updating add-on to use IFRAME sandbox mode Change-Id: I27689cb8a6c658cfa802335a1052ac2531c76ba5 --- Code.gs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Code.gs b/Code.gs index fe48dbb39..8a4f32af1 100644 --- a/Code.gs +++ b/Code.gs @@ -61,6 +61,7 @@ function onInstall(e) { */ function showSidebar() { var ui = HtmlService.createHtmlOutputFromFile('Sidebar') + .setSandboxMode(HtmlService.SandboxMode.IFRAME) .setTitle('Form Notifications'); FormApp.getUi().showSidebar(ui); } @@ -71,6 +72,7 @@ function showSidebar() { */ function showAbout() { var ui = HtmlService.createHtmlOutputFromFile('About') + .setSandboxMode(HtmlService.SandboxMode.IFRAME) .setWidth(420) .setHeight(270); FormApp.getUi().showModalDialog(ui, 'About Form Notifications'); From 586a9ddcad0f8eb4263733fb7fee3d6cf631b1c2 Mon Sep 17 00:00:00 2001 From: Ryan Roth Date: Mon, 23 Mar 2015 11:38:35 -0400 Subject: [PATCH 015/374] Just a small edit to ensure the CONTRIB.md file matches the template CONTRIB file exactly Change-Id: I8529d12fb4e999ce3d36ae89149369609e467db6 --- CONTRIB.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CONTRIB.md b/CONTRIB.md index 6736efd94..cd116067a 100644 --- a/CONTRIB.md +++ b/CONTRIB.md @@ -7,7 +7,6 @@ have to jump a couple of legal hurdles. Please fill out either the individual or corporate Contributor License Agreement (CLA). - * If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA] (https://developers.google.com/open-source/cla/individual). From 98564f1021b8ae651ee58db2581a49154df20dea Mon Sep 17 00:00:00 2001 From: Ryan Roth Date: Tue, 24 Mar 2015 14:31:55 -0400 Subject: [PATCH 016/374] added Apache headers to source files at OSS request Change-Id: Iea7c853648cc604a6c547968d2bfc95b45eccfee --- custom-functions/Code.gs | 16 ++++++++++++++++ docs-addon/Code.gs | 16 ++++++++++++++++ docs-addon/Dialog.html | 16 ++++++++++++++++ docs-addon/DialogJavaScript.html | 16 ++++++++++++++++ docs-addon/Sidebar.html | 16 ++++++++++++++++ docs-addon/SidebarJavaScript.html | 16 ++++++++++++++++ docs-addon/Stylesheet.html | 16 ++++++++++++++++ forms-addon/Code.gs | 16 ++++++++++++++++ forms-addon/Dialog.html | 16 ++++++++++++++++ forms-addon/DialogJavaScript.html | 16 ++++++++++++++++ forms-addon/Sidebar.html | 16 ++++++++++++++++ forms-addon/SidebarJavaScript.html | 16 ++++++++++++++++ forms-addon/Stylesheet.html | 16 ++++++++++++++++ sheets-addon/Code.gs | 16 ++++++++++++++++ sheets-addon/Dialog.html | 16 ++++++++++++++++ sheets-addon/DialogJavaScript.html | 16 ++++++++++++++++ sheets-addon/Sidebar.html | 16 ++++++++++++++++ sheets-addon/SidebarJavaScript.html | 16 ++++++++++++++++ sheets-addon/Stylesheet.html | 16 ++++++++++++++++ web-app/Code.gs | 16 ++++++++++++++++ web-app/Index.html | 16 ++++++++++++++++ web-app/JavaScript.html | 16 ++++++++++++++++ web-app/Stylesheet.html | 16 ++++++++++++++++ 23 files changed, 368 insertions(+) diff --git a/custom-functions/Code.gs b/custom-functions/Code.gs index 4f262953e..d5a936b24 100644 --- a/custom-functions/Code.gs +++ b/custom-functions/Code.gs @@ -1,3 +1,19 @@ +/** + * Copyright 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * @OnlyCurrentDoc Limits the script to only accessing the current spreadsheet. */ diff --git a/docs-addon/Code.gs b/docs-addon/Code.gs index b732890fa..4671a7b07 100644 --- a/docs-addon/Code.gs +++ b/docs-addon/Code.gs @@ -1,3 +1,19 @@ +/** + * Copyright 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * @OnlyCurrentDoc Limits the script to only accessing the current document. */ diff --git a/docs-addon/Dialog.html b/docs-addon/Dialog.html index dc030b6df..c876d7390 100644 --- a/docs-addon/Dialog.html +++ b/docs-addon/Dialog.html @@ -1,3 +1,19 @@ + + diff --git a/docs-addon/DialogJavaScript.html b/docs-addon/DialogJavaScript.html index bd98d07b6..3eeba456f 100644 --- a/docs-addon/DialogJavaScript.html +++ b/docs-addon/DialogJavaScript.html @@ -1,3 +1,19 @@ + + + + diff --git a/sheets-import/AuthorizationEmail.html b/sheets-import/AuthorizationEmail.html new file mode 100644 index 000000000..ba9de64a5 --- /dev/null +++ b/sheets-import/AuthorizationEmail.html @@ -0,0 +1,34 @@ + + +

The Google Sheets add-on was recently updated +and it needs you to re-authorize.

+ +

The add-on's automatic report updates are temporarily disabled until you +re-authorize. You can accomplish this by opening one of the sheets +using the add-on and running the add-on through the menu. Alternatively, you can +click this link to approve authorization directly:

+ +

Click here to re-authorize the add-on.

+ +

This notification email will be sent to you at most once per day until the +add-on is re-authorized.

+ +
+ +

This automatic message was sent to you via +the add-on for Google Sheets. +

diff --git a/sheets-import/Configurations.gs b/sheets-import/Configurations.gs new file mode 100644 index 000000000..38e926e66 --- /dev/null +++ b/sheets-import/Configurations.gs @@ -0,0 +1,218 @@ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var REPORT_SET_KEY = 'Import.ReportSet'; +var SCHEDULE_TRIGGER_ID = 'Import.scheduled.triggerId'; + +/** + * Update type enum used when adding or deleting a report. + */ +var UPDATE_TYPE = { + ADD: 1, + REMOVE: 2 +}; + +/** + * Return the report configuration for the report with the given + * ID; returns an empty Object if no such report name exists. + * @param {String} reportId a report ID. + * @return {Object} a report configuration corresponding to that ID, + * or null if no such report exists. + */ +function getReportConfig(reportId) { + var config = getObjectFromProperties(reportId); + if (!config) { + return null; + } + // Sheet name may have been changed manually, so + // get the current one. + var ss = SpreadsheetApp.getActiveSpreadsheet(); + var sheet = getSheetById(ss, parseInt(config.sheetId)); + config.sheetName = !sheet ? null : sheet.getName(); + return config; +} + +/** + * Given a report configuration, save it. + * @param {Object} config the report configuration. + * @param {Object} the updated report configuration. + */ +function saveReportConfig(config) { + var previous = getReportConfig(config.reportId); + if (config.reportId === 'new-report') { + config.reportId = newReportId(); + config.lastRun = null; + config.owner = Session.getEffectiveUser().getEmail(); + } + saveObjectToProperties(config.reportId, config); + updateReportSet(UPDATE_TYPE.ADD, config.reportId, config.name); + if (previous == null) { + return config; + } + return _.extend(previous, config); +} + +/** + * Delete the report specified by the given ID. + * @param {String} reportId indicates the report to delete. + */ +function deleteReportConfig(reportId) { + deleteObjectFromProperties(reportId); + updateReportSet(UPDATE_TYPE.REMOVE, reportId); +} + +/** + * Returns true if the current user is allowed to edit the + * report associated with the given config; returns + * false otherwise. + * @param {Object} config a report configuration. + */ +function canEditReport(config) { + if (!config) { + return false; + } + return config.scheduled == false || + Session.getEffectiveUser().getEmail() == config.owner; +} + +/** + * Given a new report configuration, return true if it saving + * this report would mean the limit on scheduled reports would + * be exceeded; return false otherwise. + * @param {Object} config a report configuration to be saved. + */ +function isOverScheduleLimit(config) { + var previous = getReportConfig(config.reportId); + var currentUser = Session.getEffectiveUser().getEmail(); + var isScheduled = config == null ? false : config.scheduled; + var wasScheduled = previous == null ? false : previous.scheduled; + return (isScheduled && wasScheduled != true && + getScheduledReports(currentUser).length >= MAX_SCHEDULED_REPORTS); +} + +/** + * Return a set of all saved reports (reportIds as keys, report + * names as values). + * @return {Object} + */ +function getAllReports() { + var properties = PropertiesService.getDocumentProperties(); + return JSON.parse(properties.getProperty(REPORT_SET_KEY)); +} + +/** + * Get a set of report configurations that all have been marked + * for scheduled imports. + * @param {String} opt_user optional user email; if provided, returned + * results will only include reports that user is the owner of. + * @return {Object} collection of configuration object for scheduled + * reports. + */ +function getScheduledReports(opt_user) { + var scheduledReports = []; + _.keys(getAllReports()).forEach(function(reportId) { + var config = getReportConfig(reportId); + if (config && config.scheduled && + (!opt_user || opt_user == config.owner)) { + scheduledReports.push(config); + } + }); + return scheduledReports; +} + +/** + * Updates the current report list (adding or removing a given + * report name and id). + * @param {Number} updateType Enum: either UPDATE_TYPE.ADD or + * UPDATE_TYPE.REMOVE. + * @param {String} reportId report to add or remove. + * @param {String} reportName report name (only needed for ADD). + */ +function updateReportSet(updateType, reportId, reportName) { + var properties = PropertiesService.getDocumentProperties(); + var lock = LockService.getDocumentLock(); + lock.waitLock(2000); + var reportSet = JSON.parse(properties.getProperty(REPORT_SET_KEY)); + if (reportSet == null) { + reportSet = {}; + } + if (updateType == UPDATE_TYPE.ADD) { + reportSet[reportId] = reportName; + } else if (updateType == UPDATE_TYPE.REMOVE) { + delete reportSet[reportId]; + } + properties.setProperty(REPORT_SET_KEY, JSON.stringify(reportSet)); + lock.releaseLock(); +} + +/** + * Update a report configuration with a sheetId and last runtime + * information, save and return it. Include but do not save the + * sheet name. + * @param {Object} config the report configuration. + * @param {Sheet} sheet the report's sheet. + * @param {String} lastRun the datetime string indicating the last + * time the report was run. + * @return {Object} the updated report configuration. + */ +function updateOnImport(config, sheet, lastRun) { + var update = { + sheetId: sheet.getSheetId().toString(), + lastRun: lastRun + }; + saveObjectToProperties(config.reportId, update); + update.sheetName = sheet.getName(); + return _.extend(config, update); +} + +/** + * Return the array of column IDs used by the given report + * configuration. + * @param {Object} config the report configuration. + * @return {Array} column ID strings. + */ +function getColumnIds(config) { + return _.map(config.columns, function(col) { + return col.column; + }); +} + +/** + * Return the saved trigger ID of the scheduling trigger for this + * user. Returns null if the trigger is not set. + @ @return {String} the trigger ID. + */ +function getTriggerId() { + var properties = PropertiesService.getUserProperties(); + return properties.getProperty(SCHEDULE_TRIGGER_ID); +} + +/** + * Save the trigger ID of the scheduling trigger for this user. + * @param {Trigger} trigger the trigger whose ID should be saved. + */ +function saveTriggerId(trigger) { + var properties = PropertiesService.getUserProperties(); + properties.setProperty(SCHEDULE_TRIGGER_ID, trigger.getUniqueId()); +} + +/** + * Remove the saved trigger ID. + */ +function removeTriggerId() { + var properties = PropertiesService.getUserProperties(); + properties.deleteProperty(SCHEDULE_TRIGGER_ID); +} diff --git a/sheets-import/JavaScript.html b/sheets-import/JavaScript.html new file mode 100644 index 000000000..390ee6229 --- /dev/null +++ b/sheets-import/JavaScript.html @@ -0,0 +1,418 @@ + + + diff --git a/sheets-import/README.md b/sheets-import/README.md new file mode 100644 index 000000000..7795340fc --- /dev/null +++ b/sheets-import/README.md @@ -0,0 +1,102 @@ +Template: Importing Data to Sheets +================================== + +This template provides a framework for creating a Sheets [add-on](https://developers.google.com/apps-script/add-ons/) +that imports data from a 3rd-party source (such as an API). + +It shows the basic structure needed to define a UI and how to coordinate +communication between the client, server, and 3rd-party source. +This template also demonstrates some useful aspects of Apps Script, including: + +* Writing data to a Google Sheet +* Using time-based triggers to establish automated sheet updates +* Using [Templated HTML](https://developers.google.com/apps-script/guides/html/templates) +* Using IFRAME sandbox mode + +**Note**: The purpose of this template is to show a general add-on structure. +It will not run as an add-on in it's current state. To make use of this +template, you will need to fill in the sections marked **TODO** to customize +the template to a specific 3rd-party data source. + +## Project manifest +The following project files are included in this template: + +* **APICode.gs** - This file contains all the API-specific code for handling + authorization, callbacks, and API calls. It will need to be modified to handle + a specific API. +* **Auth.gs** - This file contains code that assists with constructing a + OAuth2 service object using the [Apps Script OAuth2 library](https://github.com/googlesamples/apps-script-oauth2). +* **AuthCallbackView.html** - This file is the page that is presented to the + user after an authorization attempt, and shows whether the authorization was + successful. +* **AuthorizationEmail.html** - This file contains the html template of an email + that would be sent to the user in the event that a trigger attempts to fire + without all the required authorizations. +* **Configurations.gs** - This file contains code that controls the creation, + updating and deletion of report configurations that describe what to import + to Sheets from the 3rd party source. By default report configurations are + saved to Apps Script's PropertyService, but it would be possible to adapt the + code here to store that data elsewhere (for example, in an external database). +* **JavaScript.html** - This file contains the bulk of the control code for the + sidebar UI. +* **Server.gs** - This file contains server-side code that responds to user + interactions in the sidebar UI. It also sets up the add-on menu. +* **Sidebar.html** - This file contains the HTML structure for that defines + the sidebar UI. +* **Stylesheet.html** - This file contains all the CSS properties defined for + the template. +* **Utilities.gs** - This file contains some generic functionalities to support + the rest of the code. The functions here are not specific to this template and + could be taken for use in other projects without modification. +* **intercom.js.html** - This file contains a copy of + [intercom.js](https://github.com/diy/intercom.js), + a cross-window message broadcast interface (intercom.js is released under an + Apache V2.0 license). + +## Setup: Libraries +This template makes use of the following libraries, which much the added to the +Apps Script project before the template can be used: + +* [Apps Script OAuth2 library](https://github.com/googlesamples/apps-script-oauth2) +* [Underscore](http://underscorejs.org/) + +These libraries are already published as an Apps Script, making it easy to +include in your project. To add it to your script, do the following in the +Apps Script code editor: + +1. Click on the menu item "Resources > Libraries..." +1. In the "Find a Library" text box, enter the project key +"MswhXl8fVhTFUH_Q3UOJbXvxhMjh3Sh48" and click the "Select" button. +1. Choose the latest version in the dropdown box. +1. Click the "Save" button. + +This will add the [OAuth2 library](https://github.com/googlesamples/apps-script-oauth2) +to your project. Repeat the above steps with the project key +"MGwgKN2Th03tJ5OdmlzB8KPxhMjh3Sh48" to add Underscore to the project as well. + +## Setup: API configuration +This template requires app-specific configuration before it can used. +Specifically, the template will need to be informed of the authorization +details, and certain adjustments made to ensure the correct data can be +extracted from the responses. + +In the Server.gs, APICode.gs and Auth.gs project files, there are several +comments marked as **TODO**. To configure the template, visit each of these +**TODO** sections and follow the directions found there. + +Note that the template design assumes that the template will connect to an API +service using OAuth 2.0; however, it would be possible adapt it to connect to +a different kind of service, such as a SQL database. + +## Additional information + +For more information, see: + +* [Extending Google Sheets](https://developers.google.com/apps-script/guides/sheets) +* [Known Issues specific to Google Sheets](https://developers.google.com/apps-script/migration/sheets) + +Note that this template must be added to a container-bound +script attached to a Google Sheet in order to function. Developed +add-ons must go through a +[publishing process](https://developers.google.com/apps-script/add-ons/publish) +before they can be made available publicly. diff --git a/sheets-import/Server.gs b/sheets-import/Server.gs new file mode 100644 index 000000000..76711ff07 --- /dev/null +++ b/sheets-import/Server.gs @@ -0,0 +1,319 @@ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @OnlyCurrentDoc Limits the script to only accessing the current spreadsheet. + */ + +var _ = Underscore.load(); + +/** + * TODO: Replace the following with the name of the service you are importing + * from and the name of the add-on you are building, respectively. + */ +var DATA_ALIAS = 'MyDataSource'; +var ADDON_NAME = "YOUR_ADDON_NAME_HERE"; +var SIDEBAR_TITLE = 'Import Control Center'; +var MAX_SCHEDULED_REPORTS = 24; +var IMPORT_PAGE_SIZE = 30; + +/** + * Error code enum; this gets passed to the sidebar for use there as well. + */ +var ERROR_CODES = { + AUTO_UPDATE_LIMIT: 1, + ILLEGAL_EDIT: 2, + ILLEGAL_DELETE: 3, + IMPORT_FAILED: 4 +} + +/** + * Adds a custom menu with items to show the sidebar. + * @param {Object} e The event parameter for a simple onOpen trigger. + */ +function onOpen(e) { + SpreadsheetApp.getUi() + .createAddonMenu() + .addItem('Import control center', 'showSidebar') + .addToUi(); +} + +/** + * Runs when the add-on is installed; calls onOpen() to ensure menu creation and + * any other initializion work is done immediately. + * @param {Object} e The event parameter for a simple onInstall trigger. + */ +function onInstall(e) { + onOpen(e); +} + +/** + * Opens a sidebar. The sidebar structure is described in the Sidebar.html + * project file. + */ +function showSidebar() { + var service = getService(); + var template = HtmlService.createTemplateFromFile('Sidebar'); + template.user = Session.getEffectiveUser().getEmail(); + template.dataSource = DATA_ALIAS; + template.isAuthorized = service.hasAccess(); + template.authorizationUrl = null; + if (!template.isAuthorized) { + template.authorizationUrl = service.getAuthorizationUrl(); + } + var page = template.evaluate() + .setTitle(SIDEBAR_TITLE) + .setSandboxMode(HtmlService.SandboxMode.IFRAME); + SpreadsheetApp.getUi().showSidebar(page); +} + +/** + * Return data needed to build the sidebar UI: a list of the names of the + * currently saved report configurations and the list of potential + * column choices. + * @return {Object} a collection of saved report data and column options. + */ +function getInitialDataForSidebar() { + var reportSet = getAllReports(); + var reportList = []; + _.each(reportSet, function(val, key) { + reportList.push({'name': val, 'reportId': key}); + }); + reportList.sort(function (a, b) { + if (a.name > b.name) { return 1; } + if (a.name < b.name) { return -1; } + return 0; + }); + return {reports: reportList, columns: getColumnOptions()}; +} + +/** + * Get the report configuration for the given report and, if a sheet + * exists for it, activate that sheet. + * @param {String} reportId a report ID. + */ +function switchToReport(reportId) { + var config = getReportConfig(reportId); + activateById(config.sheetId); + return config; +} + +/** + * Import data to the spreadsheet according to the given report + * configuration. + * @param {String} reportId the report identifier. + * @return {Object} the (possibly updated) report configuration. + */ +function runImport(reportId) { + var ss = SpreadsheetApp.getActiveSpreadsheet(); + var config = getReportConfig(reportId); + + // Acquire the sheet to place the import results in, + // then clear and format it. + // Update the saved config with sheet/time information. + var sheet = activateReportSheet(config); + var columnIds = getColumnIds(config); + var lastRun = new Date().toString(); + config = updateOnImport(config, sheet, lastRun); + + // Call for pages of API information to place in the sheet, one + // page at a time. + var pageNumber = 0; + var firstRow = 2; + try { + var page; + do { + page = getDataPage(columnIds, pageNumber, IMPORT_PAGE_SIZE, config); + if (page) { + sheet.getRange(firstRow, 1, page.length, page[0].length).setValues(page); + firstRow += page.length; + pageNumber++; + SpreadsheetApp.flush(); + } + } while (page != null); + } catch (e) { + // Ensure a new sheet Id, if created, is preserved. + throw ERROR_CODES.IMPORT_FAILED; + } + + for (var i = 1; i <= sheet.getLastColumn(); i++) { + sheet.autoResizeColumn(i); + } + ss.toast('Report ' + config.name + ' updated.'); + return config; +} + +/** + * Save the given report configuration. + * @param {Object} config a report configuration to save. + * @return {Object} the updated report configuration. + */ +function saveReport(config) { + var existingConfig = getReportConfig(config.reportId); + if (existingConfig != null) { + activateById(existingConfig.sheetId); + // Check: users are not allowed to save edits to reports + // created by other users if those reports have been marked + // for auto-update. + if (!canEditReport(existingConfig)) { + throw ERROR_CODES.ILLEGAL_EDIT; + } + } + // Check against max number of scheduled reports. + if (isOverScheduleLimit(config)) { + throw ERROR_CODES.AUTO_UPDATE_LIMIT; + } + + var result = saveReportConfig(config); + adjustScheduleTrigger(); + return result; +} + +/** + * Delete the given report configuration. + * @param {String} reportId indicates the report to delete. + * @return {String} the report ID deleted. + */ +function removeReport(reportId) { + // Check: users are not allowed to delete reports created by + // other users if those reports have been marked for auto-update. + if (!canEditReport(getReportConfig(reportId))) { + throw ERROR_CODES.ILLEGAL_DELETE; + } + deleteReportConfig(reportId); + adjustScheduleTrigger(); + return reportId; +} + +/** + * Activate, clear, format and return the sheet associated with the + * specified report configuration. If the sheet does not exist, create, + * format and activate it. + * @param {Object} config the report configuration. + * @return {Sheet} + */ +function activateReportSheet(config) { + var ss = SpreadsheetApp.getActiveSpreadsheet(); + var sheet = getSheetById(ss, parseInt(config.sheetId)); + if (sheet == null) { + sheet = ss.insertSheet(); + sheet.setName(getUniqueSheetName(ss, config.name)); + } + sheet.activate(); + + var headers = _.map(config.columns, function(col) { + return col.label; + }); + sheet.clear(); + sheet.clearNotes(); + sheet.setFrozenRows(1); + sheet.getRange('1:1') + .setFontWeight('bold') + .setBackground('#000000') + .setFontColor('#ffffff'); + sheet.getRange(1, 1, 1, headers.length).setValues([headers]); + return sheet; +} + +/** + * On an hourly trigger, search through scheduled reports, find one + * that hasn't been run in 24 hours or more (or never), and run + * an import for that one. With <= 24 scheduled reports, this pattern + * ensures that every scheduled report will be updated once a day. + */ +function respondToHourlyTrigger() { + var authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL); + // Check if the actions of the trigger require authorizations that have not + // been supplied yet -- if so, warn the active user via email (if possible). + // This check is required when using triggers with add-ons to maintain + // functional triggers. + if (authInfo.getAuthorizationStatus() == + ScriptApp.AuthorizationStatus.REQUIRED) { + // Re-authorization is required. In this case, the user needs to be alerted + // that they need to reauthorize; the normal trigger action is not + // conducted, since it authorization needs to be provided first. Send at + // most one 'Authorization Required' email a day, to avoid spamming users + // of the add-on. + sendReauthorizationRequest(); + } else { + var potentials = getScheduledReports(Session.getEffectiveUser().getEmail()); + for (var i = 0; i < potentials.length; i++) { + var lastRun = potentials[i].lastRun; + if (!lastRun || isOlderThanADay(lastRun) ) { + runImport(potentials[i].reportId); + return; + } + } + } +} + +/** + * Called when the user needs to reauthorize. Sends the user of the + * add-on an email explaining the need to reauthorize and provides + * a link for the user to do so. Capped to send at most one email + * a day to prevent spamming the users of the add-on. + */ +function sendReauthorizationRequest() { + var authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL); + var properties = PropertiesService.getUserProperties(); + var LAST_AUTH_EMAIL_KEY = 'Import.reauth.lastAuthEmailDate'; + var lastAuthEmailDate = properties.getProperty(LAST_AUTH_EMAIL_KEY); + var today = new Date().toDateString(); + if (lastAuthEmailDate != today) { + if (MailApp.getRemainingDailyQuota() > 0) { + var template = + HtmlService.createTemplateFromFile('AuthorizationEmail'); + template.url = authInfo.getAuthorizationUrl(); + template.addonName = ADDON_NAME; + var message = template.evaluate(); + MailApp.sendEmail(Session.getEffectiveUser().getEmail(), + 'Add-on Authorization Required', + message.getContent(), { + name: ADDON_NAME, + htmlBody: message.getContent() + }); + } + properties.setProperty(LAST_AUTH_EMAIL_KEY, today); + } +} + +/** + * Turn on the scheduling trigger if scheduled reports owned + * by the current user are present; turn it off otherwise. + */ +function adjustScheduleTrigger() { + var existingTriggerId = getTriggerId(); + var user = Session.getEffectiveUser().getEmail(); + var triggerNeeded = getScheduledReports(user).length > 0; + + // Create a new trigger if required; delete existing trigger + // if it is not needed. + if (triggerNeeded && existingTriggerId == null) { + var trigger = ScriptApp.newTrigger('respondToHourlyTrigger') + .timeBased() + .everyHours(1) + .create(); + saveTriggerId(trigger); + } else if (!triggerNeeded && existingTriggerId != null) { + var existingTrigger = getUserTriggerById( + SpreadsheetApp.getActiveSpreadsheet(), + existingTriggerId); + if (existingTrigger != null) { + ScriptApp.deleteTrigger(existingTrigger); + } + removeTriggerId(); + } +} diff --git a/sheets-import/Sidebar.html b/sheets-import/Sidebar.html new file mode 100644 index 000000000..8e10baee4 --- /dev/null +++ b/sheets-import/Sidebar.html @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + diff --git a/sheets-import/Stylesheet.html b/sheets-import/Stylesheet.html new file mode 100644 index 000000000..3a7da1065 --- /dev/null +++ b/sheets-import/Stylesheet.html @@ -0,0 +1,65 @@ + + + + + + diff --git a/sheets-import/Utilities.gs b/sheets-import/Utilities.gs new file mode 100644 index 000000000..34de0380f --- /dev/null +++ b/sheets-import/Utilities.gs @@ -0,0 +1,168 @@ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Includes the given project HTML file in the current HTML project file. + * Also used to include JavaScript. + * @param {String} filename Project file name. + */ +function include(filename) { + return HtmlService.createHtmlOutputFromFile(filename).getContent(); +} + +/** + * Returns true if the given date string represents a date that is + * more than 24 hours in the past; returns false otherwise. + * @param {String} dateStr a date string. + * @return {Boolean} + */ +function isOlderThanADay(dateStr) { + var now = (new Date()).getTime(); + var then = Date.parse(dateStr); + return (then + 24 * 60 * 60 * 1000) < now; +} + +/** + * Given an object and a string prefix, save every value in that object + * to the Document properties service as a JSONified string. The property + * key for each object key will be: prefix. + * @param {String} prefix a common string to label each added property. + * @param {Object} obj a collection of key-values to save as + * user properties. + */ +function saveObjectToProperties(prefix, obj) { + var properties = PropertiesService.getDocumentProperties(); + _.each(obj, function(val, key) { + var propKey = prefix + '.' + key; + properties.setProperty(propKey, JSON.stringify(val)); + }); +} + +/** + * Given a string prefix, fetch from the Document properties service all + * properties whose keys start with that prefix, and return the (JSON-parsed) + * values in an object. The keys of the returned object will be the + * same as the property keys with the leading "prefix." removed. + * @param {String} prefix label of requested properties. + * @return {Object} collection of key-value pairs taken from the + * properties service. Will return null if the prefix is unrecognized. + */ +function getObjectFromProperties(prefix) { + var properties = PropertiesService.getDocumentProperties(); + var obj = {}; + _.each(properties.getProperties(), function(val, key) { + if (key.indexOf(prefix) > -1) { + obj[key.substr(prefix.length + 1)] = JSON.parse(val); + } + }); + if (_.keys(obj).length == 0) { + return null; + } + return obj; +} + +/** + * Given a string prefix, remove from the Document properties service all + * properties whose keys start with that prefix. + * @param {String} prefix label of properties to remove. + */ +function deleteObjectFromProperties(prefix) { + var properties = PropertiesService.getDocumentProperties(); + _.each(properties.getProperties(), function(val, key) { + if (key.indexOf(prefix) > -1) { + properties.deleteProperty(key); + } + }); +} + +/** + * Generate a random alphanumeric string. + * @return {String} report ID string. + */ +function newReportId() { + return Math.random().toString(36).substring(2); +} + +/** + * Sheets-specific utility. Find a sheet within a spreadsheet with + * the given id. If not present, return null. + * @param {Object} ss a Spreadsheet object. + * @param {Number} sheetId a Sheet id. + * @return {Object} a Sheet object, or null if not found. + */ +function getSheetById(ss, sheetId) { + if (sheetId === null) { + return null; + } + var sheets = ss.getSheets(); + for (var i = 0; i < sheets.length; i++) { + if (sheets[i].getSheetId() === sheetId) { + return sheets[i]; + } + } + return null; +} + +/** + * Sheets-specific utility. Given a base title for a sheet, check + * for that it is unique in the spreadsheet. If not, find an integer + * suffix to append to it to make it unique and return. This function + * is used to avoid name collisions while adding or renaming sheets + * automatically. + * @param {Object} spreadsheet a Spreadsheet. + * @param {String} baseName the initial suggested title for a sheet. + * @return {String} a unique title for the sheet, based on the + * given base title. + */ +function getUniqueSheetName(spreadsheet, baseName) { + var sheetName = baseName; + var i = 2; + while (spreadsheet.getSheetByName(sheetName) != null) { + sheetName = baseName + ' ' + i++; + } + return sheetName; +} + +/** + * Sheets-specific utility. Given a spreadsheet and a triggerId string, + * return the user trigger that corresponds to that ID. Returns null + * if no such trigger exists. + * @param {Spreadsheet} spreadsheet container of the user triggers. + * @param {String} triggerId trigger ID string. + * @return {Trigger} corresponding user trigger, or null if not found. + */ +function getUserTriggerById(spreadsheet, triggerId) { + var triggers = ScriptApp.getUserTriggers(spreadsheet); + for (var i = 0; i < triggers.length; i++) { + if (triggers[i].getUniqueId() == triggerId) { + return triggers[i]; + } + } + return null; +} + +/** + * Sheets-specific utility. Given a String sheet id, activate that + * sheet if it exists. + * @param {String} sheetId the sheet ID. + */ +function activateById(sheetId) { + var ss = SpreadsheetApp.getActiveSpreadsheet(); + var sheet = getSheetById(ss, parseInt(sheetId)); + if (sheet != null) { + sheet.activate(); + } +} diff --git a/sheets-import/intercom.js.html b/sheets-import/intercom.js.html new file mode 100644 index 000000000..1154c4048 --- /dev/null +++ b/sheets-import/intercom.js.html @@ -0,0 +1,14 @@ + From a43baa08fb0d9580325d70e3734fefd69fd919cb Mon Sep 17 00:00:00 2001 From: Ryan Roth Date: Wed, 22 Jul 2015 11:16:27 -0400 Subject: [PATCH 019/374] Added link to response sheet in creator emails. Change-Id: Ie167c88e58bd51f54abeae4aff6c4175eaa7bd83 --- Code.gs | 2 ++ CreatorNotification.html | 1 + 2 files changed, 3 insertions(+) diff --git a/Code.gs b/Code.gs index 8a4f32af1..9e46a86f4 100644 --- a/Code.gs +++ b/Code.gs @@ -243,6 +243,8 @@ function sendCreatorNotification() { if (MailApp.getRemainingDailyQuota() > addresses.length) { var template = HtmlService.createTemplateFromFile('CreatorNotification'); + template.sheet = + DriveApp.getFileById(form.getDestinationId()).getUrl(); template.summary = form.getSummaryUrl(); template.responses = form.getResponses().length; template.title = form.getTitle(); diff --git a/CreatorNotification.html b/CreatorNotification.html index 6beb904c9..8f41b4af7 100644 --- a/CreatorNotification.html +++ b/CreatorNotification.html @@ -2,6 +2,7 @@ titled has received responses so far.

+

Response sheet

Summary of form responses

You are receiving this email because an editor of this form configured From af25ecfdf4549e99795f3e867c9b890024bba66c Mon Sep 17 00:00:00 2001 From: Eric Koleda Date: Tue, 11 Aug 2015 09:49:06 -0400 Subject: [PATCH 020/374] Update the sample to use IFRAME sandboxing and intercom.js for message passing. --- Code.gs | 139 +++++++---------------------------------- Dialog.html | 133 +++++++++++++++++++++++---------------- Intercom.js.html | 29 +++++++++ README.md | 30 ++++----- Sidebar.html | 159 +++++++++++++++++++++++++++++++++-------------- 5 files changed, 260 insertions(+), 230 deletions(-) create mode 100644 Intercom.js.html diff --git a/Code.gs b/Code.gs index 08710ef03..3da00613a 100644 --- a/Code.gs +++ b/Code.gs @@ -1,49 +1,21 @@ -/** - * How long to wait for the dialog to check-in before assuming it's been - * closed, in seconds. - */ -var DIALOG_TIMEOUT_SECONDS = 10; /** - * The various states the dialog can be in. + * + * Copyright 2015 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + * */ -var DialogState = { - /** - * The dialog is still open. - */ - OPEN: 'open', - - /** - * The dialog has been closed without being completed. - */ - ABORTED: 'aborted', - - /** - * The dialog hasn't checked-in in a while, so we assume it's been closed - * via the "X" at the top of the dialog. - */ - LOST: 'lost', - - /** - * The dialog has been completed and closed. - */ - DONE: 'done' -}; - -/** - * The various proprties of a dialog we store. - */ -var DialogProperty = { - /** - * The state of the dialog. See DialogState. - */ - STATE: 'state', - - /** - * The timestamp of the last time the dialog checked-in. - */ - LAST_CHECK_IN: 'lastCheckIn' -}; /** * Runs when the document opens, populating the menu. @@ -58,93 +30,30 @@ function onOpen() { * Shows the sidebar in the document. */ function showSidebar() { - var page = HtmlService.createHtmlOutputFromFile('Sidebar') + var page = HtmlService.createTemplateFromFile('Sidebar') + .evaluate() .setTitle('Sidebar') - .setSandboxMode(HtmlService.SandboxMode.NATIVE); + .setSandboxMode(HtmlService.SandboxMode.IFRAME); DocumentApp.getUi().showSidebar(page); } /** - * Shows the dialog in the document. + * Open a dialog in the document. */ -function showDialog() { +function openDialog() { var dialogId = Utilities.base64Encode(Math.random()); var template = HtmlService.createTemplateFromFile('Dialog'); template.dialogId = dialogId; var page = template.evaluate() .setTitle('Dialog') - .setSandboxMode(HtmlService.SandboxMode.NATIVE); + .setSandboxMode(HtmlService.SandboxMode.IFRAME); DocumentApp.getUi().showDialog(page); - checkIn(dialogId); return dialogId; } /** - * Records the last time a dialog checked-in with the server. The dialog should - * call this function periodically (every few seconds). - * @param {string} dialogId The ID of the dialog. - */ -function checkIn(dialogId) { - var key = getCacheKey_(dialogId, DialogProperty.LAST_CHECK_IN); - var timestamp = new Date().getTime(); - CacheService.getPrivateCache().put(key, timestamp); -} - -/** - * Sets the state of a dialog. The dialog should call this function before - * it closes itself. - * @param {string} dialogId The ID of the dialog. - * @param {string} state The state of the dialog. - */ -function setDialogState(dialogId, state) { - validateDialogState_(state); - var key = getCacheKey_(dialogId, DialogProperty.STATE); - CacheService.getPrivateCache().put(key, state); -} - -/** - * Gets the state of a dialog. The sidebar should call this function periodically - * to determine when the dialog is closed. - * @param {string} dialogId The ID of the dialog. - * @return {string} The state of the dialog. - */ -function getDialogState(dialogId) { - var key = getCacheKey_(dialogId, DialogProperty.STATE); - var status = CacheService.getPrivateCache().get(key); - if (status != null) { - return status; - } else { - var lastCheckInKey = getCacheKey_(dialogId, DialogProperty.LAST_CHECK_IN); - var lastCheckIn = parseInt(CacheService.getPrivateCache().get(lastCheckInKey)); - var now = new Date().getTime(); - if (now - lastCheckIn > DIALOG_TIMEOUT_SECONDS * 1000) { - return DialogState.LOST; - } else { - return DialogState.OPEN; - } - } -} - -/** - * Validates that a given dialog state is valid, and throws an exception if it isn't. - * @param {string} state The dialog state to validate. - * @private - */ -function validateDialogState_(state) { - var validStates = Object.keys(DialogState).map(function(key) { - return DialogState[key]; - }); - if (validStates.indexOf(state) == -1) { - throw 'Invalid dialog state: ' + state; - } -} - -/** - * Gets the cache key for a given property of a dialog. - * @param {string} dialogId The ID of the dialog. - * @param {stirng} property The property of the dialog. - * @return {stirng} The cache key. + * Include the contents of the given file into the HTML content. */ -function getCacheKey_(dialogId, property) { - return dialogId + '-' + property; +function include(filename) { + return HtmlService.createHtmlOutputFromFile(filename).getContent(); } diff --git a/Dialog.html b/Dialog.html index 3957d0ba1..c3aa4f14a 100644 --- a/Dialog.html +++ b/Dialog.html @@ -1,52 +1,81 @@ - - -

-

Content goes here...

-
- - -
-
- - - + + + + + + + +
+

Content goes here...

+
+ + +
+
+ + + + + + diff --git a/Intercom.js.html b/Intercom.js.html new file mode 100644 index 000000000..7ee7686c8 --- /dev/null +++ b/Intercom.js.html @@ -0,0 +1,29 @@ + + diff --git a/README.md b/README.md index 1ee1dc67c..06f610395 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,24 @@ This script demonstrates a method of setting up a communication channel between a dialog and a sidebar in Apps Script. This helps solve the common problem -of having your sidebar know when a dialog it spawned is complete, closed, etc. +of having your sidebar know when a dialog is opened, is submitted, closed, etc. -Unfortunately Apps Script doesn't support a direct communication channel -across client-side code, nor does it support an eventing framework that allows -the server to notify the client of the change. Therefore this solution relies on -both the sidebar and dialog regularly polling the backend. +With the introduction of the IFRAME sandbox mode, HtmlService UIs can take +advantage of the +[HTML5 localStorage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). +The open source library [intercom.js](https://github.com/diy/intercom.js/) +builds a messaging system on top of this API, allowing for dialogs and sidebars +in the same browser to communicate with each other. -An overview is the process is as follows: +An overview of the process is as follows: -* The sidebar requests a new dialog to be spawned. +* The sidebar requests a new dialog to be opened. * The backend generates a new ID for the dialog, opens the dialog (passing in that ID as a template parameter), and sends the ID back to the sidebar. -* The sidebar regularly polls the backend using the dialog's ID to check for a - state change. -* The dialog regularly "checks in" with the backend, so that the backend knows - it's still open. +* The sidebar listens for events on the dialog's intercom.js channel. +* The dialog regularly "checks in" with the sidebar, resetting a + [timer](https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Timers). * When the user completes the dialog (by clicking either the "OK" or "Cancel" - button) it sends this status change to the backend. -* If the backend notices that the dialog hasn't checked in for some time, it - changes it's status to "lost". -* When the dialog notices a change in the dialog's status it stops polling. + button) it sends this status change to the sidebar. +* If the sidebar's timer actually fires, that means the dialog hasn't checked in + recently, and it is considered "lost". diff --git a/Sidebar.html b/Sidebar.html index 27f415f96..e36800c32 100644 --- a/Sidebar.html +++ b/Sidebar.html @@ -1,49 +1,112 @@ - - - - - - + + + + + From 6e9df3e4371740505e52ca5662dd7d791a4efe58 Mon Sep 17 00:00:00 2001 From: Ryan Roth Date: Thu, 10 Sep 2015 18:46:55 -0400 Subject: [PATCH 021/374] Updating templates to use IFRAME sandbox mode. Change-Id: Iaab0682a4cf173f3d460b7a5250ba99a8afa0439 --- docs-addon/Code.gs | 6 ++-- docs-addon/Dialog.html | 45 ++++++++++++++----------- docs-addon/Sidebar.html | 55 +++++++++++++++++------------- forms-addon/Code.gs | 6 ++-- forms-addon/Dialog.html | 51 ++++++++++++++++------------ forms-addon/Sidebar.html | 69 +++++++++++++++++++++----------------- sheets-addon/Code.gs | 6 ++-- sheets-addon/Dialog.html | 55 +++++++++++++++++------------- sheets-addon/Sidebar.html | 61 ++++++++++++++++++--------------- sheets-import/Sidebar.html | 3 +- web-app/Index.html | 27 ++++++++------- 11 files changed, 218 insertions(+), 166 deletions(-) diff --git a/docs-addon/Code.gs b/docs-addon/Code.gs index 4671a7b07..54033b618 100644 --- a/docs-addon/Code.gs +++ b/docs-addon/Code.gs @@ -51,7 +51,8 @@ function onInstall(e) { function showSidebar() { var ui = HtmlService.createTemplateFromFile('Sidebar') .evaluate() - .setTitle(SIDEBAR_TITLE); + .setTitle(SIDEBAR_TITLE) + .setSandboxMode(HtmlService.SandboxMode.IFRAME); DocumentApp.getUi().showSidebar(ui); } @@ -63,7 +64,8 @@ function showDialog() { var ui = HtmlService.createTemplateFromFile('Dialog') .evaluate() .setWidth(400) - .setHeight(130); + .setHeight(150) + .setSandboxMode(HtmlService.SandboxMode.IFRAME); DocumentApp.getUi().showModalDialog(ui, DIALOG_TITLE); } diff --git a/docs-addon/Dialog.html b/docs-addon/Dialog.html index c876d7390..4f3959e2b 100644 --- a/docs-addon/Dialog.html +++ b/docs-addon/Dialog.html @@ -14,23 +14,30 @@ * limitations under the License. --> - - + + + + + + + + + +
+
+ + +
+
+ + +
+
+
- -
-
- - -
-
- - -
-
-
- - - + + + + diff --git a/docs-addon/Sidebar.html b/docs-addon/Sidebar.html index 4d81059aa..7123be627 100644 --- a/docs-addon/Sidebar.html +++ b/docs-addon/Sidebar.html @@ -14,30 +14,37 @@ * limitations under the License. --> - - - - - - - - - - + - - + + + + From 5972d06f514fa6ec7ac6d5b34bfdb3af33bad179 Mon Sep 17 00:00:00 2001 From: Ryan Roth Date: Thu, 10 Sep 2015 20:11:03 -0400 Subject: [PATCH 022/374] Added top-level html tags, licence info Change-Id: Ia977d4b4a9bf70f13ad7a72e9e3a0b939827c6f6 --- About.html | 55 ++-- AuthorizationEmail.html | 16 ++ Code.gs | 16 ++ CreatorNotification.html | 16 ++ RespondentNotification.html | 16 ++ Sidebar.html | 521 +++++++++++++++++++----------------- 6 files changed, 376 insertions(+), 264 deletions(-) diff --git a/About.html b/About.html index 8f75cc73e..e0afac8ba 100644 --- a/About.html +++ b/About.html @@ -1,15 +1,40 @@ - -
-

- Form Notifications was created as an sample add-on, and is meant - for demonstration purposes only. It should not be used for complex or - important workflows. -

-

- The number of notifications this add-on produces are limited by the owner's - available email quota; it will not send email notifications if the owner's - daily email quota has been exceeded. Collaborators using this add-on on the - same form will be able to adjust the notification settings, but will not be - able to disable the notification triggers set by other collaborators. -

-
+ + + + + + + + + + +
+

+ Form Notifications was created as an sample add-on, and is meant + for demonstration purposes only. It should not be used for complex or + important workflows. +

+

+ The number of notifications this add-on produces are limited by the owner's + available email quota; it will not send email notifications if the owner's + daily email quota has been exceeded. Collaborators using this add-on on the + same form will be able to adjust the notification settings, but will not be + able to disable the notification triggers set by other collaborators. +

+
+ + diff --git a/AuthorizationEmail.html b/AuthorizationEmail.html index a1344b637..c1c7b3996 100644 --- a/AuthorizationEmail.html +++ b/AuthorizationEmail.html @@ -1,3 +1,19 @@ + +

The Google Forms add-on Form Notifications is set to run automatically whenever a form is submitted. The add-on was recently updated and it needs you to re-authorize it to run on your behalf.

diff --git a/Code.gs b/Code.gs index 9e46a86f4..8a11352e5 100644 --- a/Code.gs +++ b/Code.gs @@ -1,3 +1,19 @@ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * @OnlyCurrentDoc * diff --git a/CreatorNotification.html b/CreatorNotification.html index 8f41b4af7..4fe3e1bdd 100644 --- a/CreatorNotification.html +++ b/CreatorNotification.html @@ -1,3 +1,19 @@ + +

Form Notifications (a Google Forms add-on) has detected that the form titled has received responses so far.

diff --git a/RespondentNotification.html b/RespondentNotification.html index a20234d3d..5f4225c87 100644 --- a/RespondentNotification.html +++ b/RespondentNotification.html @@ -1,3 +1,19 @@ + +

diff --git a/Sidebar.html b/Sidebar.html index eb77e6d23..19110d3c4 100644 --- a/Sidebar.html +++ b/Sidebar.html @@ -1,259 +1,282 @@ - - + - - - - - - - - + + + + + + + + + + + + From e5035c6d61dfc3b055e7be20f609fc167bb1bff3 Mon Sep 17 00:00:00 2001 From: Jan-Felix Schmakeit Date: Mon, 16 Nov 2015 03:15:31 +0000 Subject: [PATCH 023/374] Initial empty repository From b188e9e2b796f85a83fbcb94e64f0dfe96671ab0 Mon Sep 17 00:00:00 2001 From: Ryan Roth Date: Thu, 14 Jul 2016 13:58:14 -0400 Subject: [PATCH 024/374] Initial commit --- CONTRIB.md | 35 + LICENSE | 647 +++++++++++++ README.md | 61 ++ mobile-translate/Code.gs | 245 +++++ mobile-translate/README.md | 118 +++ mobile-translate/app/build.gradle | 48 + .../app/src/main/AndroidManifest.xml | 46 + .../DefaultLaunchActivity.java | 51 ++ .../mobiledoctranslate/MainActivity.java | 849 ++++++++++++++++++ .../main/res/layout-land/activity_main.xml | 72 ++ .../app/src/main/res/layout/activity_main.xml | 63 ++ .../app/src/main/res/layout/buttons.xml | 28 + .../src/main/res/layout/input_text_pane.xml | 19 + .../src/main/res/layout/launch_default.xml | 28 + .../src/main/res/layout/output_text_pane.xml | 22 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 4266 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2436 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 5736 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 9451 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 13259 bytes .../app/src/main/res/values/colors.xml | 4 + .../app/src/main/res/values/dimens.xml | 22 + .../app/src/main/res/values/strings.xml | 61 ++ .../app/src/main/res/values/styles.xml | 26 + packaging.yaml | 15 + 25 files changed, 2460 insertions(+) create mode 100644 CONTRIB.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 mobile-translate/Code.gs create mode 100644 mobile-translate/README.md create mode 100644 mobile-translate/app/build.gradle create mode 100644 mobile-translate/app/src/main/AndroidManifest.xml create mode 100644 mobile-translate/app/src/main/java/com/google/samples/mobiledoctranslate/DefaultLaunchActivity.java create mode 100644 mobile-translate/app/src/main/java/com/google/samples/mobiledoctranslate/MainActivity.java create mode 100644 mobile-translate/app/src/main/res/layout-land/activity_main.xml create mode 100644 mobile-translate/app/src/main/res/layout/activity_main.xml create mode 100644 mobile-translate/app/src/main/res/layout/buttons.xml create mode 100644 mobile-translate/app/src/main/res/layout/input_text_pane.xml create mode 100644 mobile-translate/app/src/main/res/layout/launch_default.xml create mode 100644 mobile-translate/app/src/main/res/layout/output_text_pane.xml create mode 100644 mobile-translate/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 mobile-translate/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 mobile-translate/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile-translate/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile-translate/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile-translate/app/src/main/res/values/colors.xml create mode 100644 mobile-translate/app/src/main/res/values/dimens.xml create mode 100644 mobile-translate/app/src/main/res/values/strings.xml create mode 100644 mobile-translate/app/src/main/res/values/styles.xml create mode 100644 packaging.yaml diff --git a/CONTRIB.md b/CONTRIB.md new file mode 100644 index 000000000..30a003810 --- /dev/null +++ b/CONTRIB.md @@ -0,0 +1,35 @@ +# How to become a contributor and submit your own code + +## Contributor License Agreements + +We'd love to accept your sample apps and patches! Before we can take them, we +have to jump a couple of legal hurdles. + +Please fill out either the individual or corporate Contributor License Agreement +(CLA). + + * If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an [individual CLA] + (https://developers.google.com/open-source/cla/individual). + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate CLA] + (https://developers.google.com/open-source/cla/corporate). + +Follow either of the two links above to access the appropriate CLA and +instructions for how to sign and return it. Once we receive it, we'll be able to +accept your pull requests. + +## Contributing A Patch + +1. Submit an issue describing your proposed change to the repo in question. +1. The repo owner will respond to your issue. +1. If your proposed change is accepted, and you haven't already done so, sign a + Contributor License Agreement (see details above). +1. Fork the desired repo, develop and test your code changes. +1. Ensure that your code adheres to the existing style in the sample to which + you are contributing. Refer to the + [Google Cloud Platform Samples Style Guide] + (https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the + recommended coding standards for this organization. +1. Ensure that your code has an appropriate set of unit tests which all pass. +1. Submit a pull request. diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..4f229463d --- /dev/null +++ b/LICENSE @@ -0,0 +1,647 @@ +Apache License +-------------- + + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +All image and audio files (including *.png, *.jpg, *.svg, *.mp3, *.wav +and *.ogg) are licensed under the CC-BY-NC license. All other files are +licensed under the Apache 2 license. + +CC-BY-NC License +---------------- + +Attribution-NonCommercial-ShareAlike 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More_considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International +Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial-ShareAlike 4.0 International Public License +("Public License"). To the extent this Public License may be +interpreted as a contract, You are granted the Licensed Rights in +consideration of Your acceptance of these terms and conditions, and the +Licensor grants You such rights in consideration of benefits the +Licensor receives from making the Licensed Material available under +these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. BY-NC-SA Compatible License means a license listed at + creativecommons.org/compatiblelicenses, approved by Creative + Commons as essentially the equivalent of this Public License. + + d. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + e. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + f. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + g. License Elements means the license attributes listed in the name + of a Creative Commons Public License. The License Elements of this + Public License are Attribution, NonCommercial, and ShareAlike. + + h. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + i. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + j. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + k. NonCommercial means not primarily intended for or directed towards + commercial advantage or monetary compensation. For purposes of + this Public License, the exchange of the Licensed Material for + other material subject to Copyright and Similar Rights by digital + file-sharing or similar means is NonCommercial provided there is + no payment of monetary compensation in connection with the + exchange. + + l. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + m. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + n. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part, for NonCommercial purposes only; and + + b. produce, reproduce, and Share Adapted Material for + NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. Additional offer from the Licensor -- Adapted Material. + Every recipient of Adapted Material from You + automatically receives an offer from the Licensor to + exercise the Licensed Rights in the Adapted Material + under the conditions of the Adapter's License You apply. + + c. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties, including when + the Licensed Material is used other than for NonCommercial + purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + b. ShareAlike. + + In addition to the conditions in Section 3(a), if You Share + Adapted Material You produce, the following conditions also apply. + + 1. The Adapter's License You apply must be a Creative Commons + license with the same License Elements, this version or + later, or a BY-NC-SA Compatible License. + + 2. You must include the text of, or the URI or hyperlink to, the + Adapter's License You apply. You may satisfy this condition + in any reasonable manner based on the medium, means, and + context in which You Share Adapted Material. + + 3. You may not offer or impose any additional or different terms + or conditions on, or apply any Effective Technological + Measures to, Adapted Material that restrict exercise of the + rights granted under the Adapter's License You apply. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database for NonCommercial purposes + only; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material, + including for purposes of Section 3(b); and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + +======================================================================= + +Creative Commons is not a party to its public licenses. +Notwithstanding, Creative Commons may elect to apply one of its public +licenses to material it publishes and in those instances will be +considered the "Licensor." Except for the limited purpose of indicating +that material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the public +licenses. + +Creative Commons may be contacted at creativecommons.org. + diff --git a/README.md b/README.md new file mode 100644 index 000000000..bff38a85c --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +Apps Script Mobile Add-ons +========================== + +Android applications that interact with the Google Docs and Sheets +editor apps, and use the +[Apps Script Execution API](https://developers.google.com/apps-script/guides/) +to call Apps Script functions to interact with Google services. + +Introduction +------------ + +Google Apps Script now allows developers to construct mobile add-ons -- +Android applications which extend and support Google Docs and Sheets. + +This repository contains sample mobile add-ons that are meant to +provide an example of a mobile add-on's project structure. + +Learn more +---------- + +To continue learning about mobile add-ons for Google Docs and Sheets, +take a look at the following resources: + +* [Mobile Add-ons](https://developers.google.com/apps-script/add-ons/mobile) +* [Apps Script Execution API](https://developers.google.com/apps-script/guides/) + +Support +------- + +For general Apps Script support, check the following: + +- Stack Overflow Tag: [google-apps-script](http://stackoverflow.com/questions/tagged/google-apps-script) +- Issue Tracker: [google-apps-script-issues](https://code.google.com/p/google-apps-script-issues/issues/list) + +If you've found an error in this sample, please file an issue: +https://github.com/googlesamples/apps-script-mobile-addons + +Patches are encouraged, and may be submitted by forking this project and +submitting a pull request through GitHub. + +License +------- + +Copyright 2016 Google, Inc. + +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. \ No newline at end of file diff --git a/mobile-translate/Code.gs b/mobile-translate/Code.gs new file mode 100644 index 000000000..914e32810 --- /dev/null +++ b/mobile-translate/Code.gs @@ -0,0 +1,245 @@ +/** + * @OnlyCurrentDoc + * + * The above comment directs Apps Script to limit the scope of file + * access for this add-on. It specifies that this add-on will only + * attempt to read or modify the files in which the add-on is used, + * and not all of the user's files. The authorization request message + * presented to users will reflect this limited scope. + */ + +/** + * Creates a menu entry in the Google Docs UI when the document is opened. + * This method is only used by the regular add-on, and is never called by + * the mobile add-on version. + * + * @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. + * This method is only used by the regular add-on, and is never called by + * the mobile add-on version. + * + * @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's user interface. + * This method is only used by the regular add-on, and is never called by + * the mobile add-on version. + */ +function showSidebar() { + var ui = HtmlService.createHtmlOutputFromFile('Sidebar') + .setTitle('Translate'); + DocumentApp.getUi().showSidebar(ui); +} + +/** + * Gets the text the user has selected. If there is no selection, + * this function displays an error message. + * + * @return {Array.} 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.'; + } +} + +/** + * Gets the stored user preferences for the origin and destination languages, + * if they exist. + * This method is only used by the regular add-on, and is never called by + * the mobile add-on version. + * + * @return {Object} The user's origin and destination language preferences, if + * they exist. + */ +function getPreferences() { + var userProperties = PropertiesService.getUserProperties(); + var languagePrefs = { + originLang: userProperties.getProperty('originLang'), + destLang: userProperties.getProperty('destLang') + }; + return languagePrefs; +} + +/** + * Gets the user-selected text and translates it from the origin language to the + * destination language. The languages are notated by their two-letter short + * form. For example, English is 'en', and Spanish is 'es'. The origin language + * may be specified as an empty string to indicate that Google Translate should + * auto-detect the language. + * + * @param {string} origin The two-letter short form for the origin language. + * @param {string} dest The two-letter short form for the destination language. + * @param {boolean} savePrefs Whether to save the origin and destination + * language preferences. + * @return {Object} Object containing the original text and the result of the + * translation. + */ +function getTextAndTranslation(origin, dest, savePrefs) { + var result = {}; + var text = getSelectedText(); + result['text'] = text.join('\n'); + + if (savePrefs == true) { + var userProperties = PropertiesService.getUserProperties(); + userProperties.setProperty('originLang', origin); + userProperties.setProperty('destLang', dest); + } + + result['translation'] = translateText(result['text'], origin, dest); + + return result; +} + +/** + * Replaces the text of the current selection with the provided text, or + * inserts text at the current cursor location. (There will always be either + * a selection or a cursor.) If multiple elements are selected, only inserts the + * translated text in the first element that can contain text and removes the + * other elements. + * + * @param {string} newText The text with which to replace the current selection. + */ +function insertText(newText) { + var selection = DocumentApp.getActiveDocument().getSelection(); + if (selection) { + var replaced = false; + var elements = selection.getSelectedElements(); + if (elements.length == 1 && + elements[0].getElement().getType() == + DocumentApp.ElementType.INLINE_IMAGE) { + throw "Can't insert text into an image."; + } + 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(); + + var remainingText = element.getText().substring(endIndex + 1); + element.deleteText(startIndex, endIndex); + if (!replaced) { + element.insertText(startIndex, newText); + replaced = true; + } else { + // This block handles a selection that ends with a partial element. We + // want to copy this partial text to the previous element so we don't + // have a line-break before the last partial. + var parent = element.getParent(); + parent.getPreviousSibling().asText().appendText(remainingText); + // We cannot remove the last paragraph of a doc. If this is the case, + // just remove the text within the last paragraph instead. + if (parent.getNextSibling()) { + parent.removeFromParent(); + } else { + element.removeFromParent(); + } + } + } else { + var element = elements[i].getElement(); + if (!replaced && element.editAsText) { + // Only translate elements that can be edited as text, removing other + // elements. + element.clear(); + element.asText().setText(newText); + replaced = true; + } else { + // We cannot remove the last paragraph of a doc. If this is the case, + // just clear the element. + if (element.getNextSibling()) { + element.removeFromParent(); + } else { + element.clear(); + } + } + } + } + } else { + var cursor = DocumentApp.getActiveDocument().getCursor(); + var surroundingText = cursor.getSurroundingText().getText(); + var surroundingTextOffset = cursor.getSurroundingTextOffset(); + + // If the cursor follows or preceds a non-space character, insert a space + // between the character and the translation. Otherwise, just insert the + // translation. + if (surroundingTextOffset > 0) { + if (surroundingText.charAt(surroundingTextOffset - 1) != ' ') { + newText = ' ' + newText; + } + } + if (surroundingTextOffset < surroundingText.length) { + if (surroundingText.charAt(surroundingTextOffset) != ' ') { + newText += ' '; + } + } + cursor.insertText(newText); + } +} + + +/** + * Given text, translate it from the origin language to the destination + * language. The languages are notated by their two-letter short form. For + * example, English is 'en', and Spanish is 'es'. The origin language may be + * specified as an empty string to indicate that Google Translate should + * auto-detect the language. + * + * @param {string} text text to translate. + * @param {string} origin The two-letter short form for the origin language. + * @param {string} dest The two-letter short form for the destination language. + * @return {string} The result of the translation, or the original text if + * origin and dest languages are the same. + */ +function translateText(text, origin, dest) { + if (origin === dest) { + return text; + } + return LanguageApp.translate(text, origin, dest); +} diff --git a/mobile-translate/README.md b/mobile-translate/README.md new file mode 100644 index 000000000..83c245640 --- /dev/null +++ b/mobile-translate/README.md @@ -0,0 +1,118 @@ +Mobile Doc Translate Add-on +=========================== + +A sample Google Apps Script mobile add-on for Google Docs. This add-on is +essentially a mobile version of the Docs +[Translate Add-on Quickstart](https://developers.google.com/apps-script/quickstart/docs). + +Introduction +------------ + +Google Apps Script now allows developers to construct Mobile Add-ons -- Android +applications which extend and support Google Docs and Sheets. + +This sample shows how to construct a mobile add-on called +**Mobile Doc Translate**. This add-on allows users to select text in a +Google Doc on their mobile device and see a translation of that text in one +of several languages. The user can then edit the translation as needed and +replace the original selected text in the Doc with the translation. + + +Getting Started +--------------- + +The add-on will need to call an Apps Script project to get Doc text, make +translations, and insert text into the Doc. Users can access this add-on from +the Google Docs Android app by highlighting text and selecting the add-on in the +text context menu. + +The Apps Script code file for this project is `Code.gs`. This is the same code +used in the [Translate Add-on Quickstart](https://developers.google.com/apps-script/quickstart/docs), +but does not include the HTML code that defines the quickstart's sidebar. + +The mobile add-on will make use of the +[Apps Script Execution API](https://developers.google.com/apps-script/guides/rest/) +to call the `Code.gs` functions. The +[Execution API quickstart for Android](https://developers.google.com/apps-script/guides/rest/quickstart/android) +describes how to call Apps Script functions from Android applications. + +To build this sample: + +1. The `app/` folder in this repository contains all the required Android files + for this add-on. These can be manually copied or imported into a new Android + Studio project. +1. Create a new Apps Script project. +1. Replace the code in the new project's `Code.gs` file with the code from this + repo. +1. Save the project. +1. In the code editor, select **Publish > Deploy as API** executable. +1. In the dialog that opens, leave the **Version** as "New" and enter + "Target-v1" into the text box. Click **Deploy**. +1. Follow the + [Keytool SHA1 Fingerprint](https://developers.google.com/apps-script/guides/rest/quickstart/android#step_1_acquire_a_sha1_fingerprint) + instructions to acquire a SHA1 fingerprint for your project. +1. Using that SHA code, follow the + [Turn on the Execution API](https://developers.google.com/apps-script/guides/rest/quickstart/android#step_2_turn_on_the_api_name) + instructions to enable the API for your script project and create OAuth + credentials. Be sure to match the same package name used in your Android + code. +1. Edit the `MainActivity.java` file so that the `SCRIPT_ID` constant is set to + your Apps Script project ID (in the script editor, select + **File > Project properties**, and use the **Project key**). + + These steps should allow you to build the Android app and have it successfully + call the Apps Script code. You can test it by: + + 1. Install the app on a test Android device. + 1. Set the app as the debug app on the device by running this + [ADB](https://developer.android.com/studio/command-line/adb.html) + command: + `$ adb shell am set-debug-app --persistent ` + 1. Open a docucment using the Google Docs app on the device. + 1. Highlight some text in the doc and select the three-dot icon to open the + context menu, and then select **Mobile Doc Translate**. + +Learn more +---------- + +To continue learning about mobile add-ons for Google Docs and Sheets, +take a look at the following resources: + +* [Mobile Add-ons](https://developers.google.com/apps-script/add-ons/mobile) +* [Apps Script Execution API](https://developers.google.com/apps-script/guides/) + +Support +------- + +For general Apps Script support, check the following: + +- Stack Overflow Tag: [google-apps-script](http://stackoverflow.com/questions/tagged/google-apps-script) +- Issue Tracker: [google-apps-script-issues](https://code.google.com/p/google-apps-script-issues/issues/list) + +If you've found an error in this sample, please file an issue: +https://github.com/googlesamples/apps-script-mobile-addons + +Patches are encouraged, and may be submitted by forking this project and +submitting a pull request through GitHub. + +License +------- + +Copyright 2016 Google, Inc. + +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. \ No newline at end of file diff --git a/mobile-translate/app/build.gradle b/mobile-translate/app/build.gradle new file mode 100644 index 000000000..5a3e94242 --- /dev/null +++ b/mobile-translate/app/build.gradle @@ -0,0 +1,48 @@ +apply plugin: 'com.android.application' + +def keystorePropertiesFile = rootProject.file("keystore.properties") +def keystoreProperties = new Properties() +keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + +android { + signingConfigs { + mainRelease { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile file(keystoreProperties['storeFile']) + storePassword keystoreProperties['storePassword'] + } + } + compileSdkVersion 23 + buildToolsVersion "24.0.0 rc4" + defaultConfig { + applicationId "com.google.samples.mobiledoctranslate" + minSdkVersion 17 + targetSdkVersion 23 + versionCode 2 + versionName "1.0.1" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), + 'proguard-rules.pro' + signingConfig signingConfigs.mainRelease + } + } +} + +dependencies { + compile fileTree(include: ['*.jar'], dir: 'libs') + testCompile 'junit:junit:4.12' + compile 'com.android.support:appcompat-v7:23.4.0' + compile 'com.google.android.gms:play-services-auth:9.0.2' + compile 'com.android.support:cardview-v7:23.4.0' + compile 'pub.devrel:easypermissions:0.1.5' + compile('com.google.api-client:google-api-client-android:1.20.0') { + exclude group: 'org.apache.httpcomponents' + } + compile('com.google.apis:google-api-services-script:v1-rev1-1.20.0') { + exclude group: 'org.apache.httpcomponents' + } +} diff --git a/mobile-translate/app/src/main/AndroidManifest.xml b/mobile-translate/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..6bffeaec7 --- /dev/null +++ b/mobile-translate/app/src/main/AndroidManifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile-translate/app/src/main/java/com/google/samples/mobiledoctranslate/DefaultLaunchActivity.java b/mobile-translate/app/src/main/java/com/google/samples/mobiledoctranslate/DefaultLaunchActivity.java new file mode 100644 index 000000000..cd78e5cc0 --- /dev/null +++ b/mobile-translate/app/src/main/java/com/google/samples/mobiledoctranslate/DefaultLaunchActivity.java @@ -0,0 +1,51 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.mobiledoctranslate; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; + +/** + * Since this add-on needs context from the Docs editor app, it should only be + * launched from that app (via context menus). + * + * This activity handles the edge case where the app is (erroneously) launched + * from the home screen or a notification. This activity simply presents a + * message to the user and provides an Exit button. + */ +public class DefaultLaunchActivity extends Activity { + + /** + * Create the default launch activity. + * @param savedInstanceState previously saved instance data + */ + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.launch_default); + } + + /** + * Cancel the add-on and return without action. + * @param v The button's View context + */ + public void cancel(View v) { + setResult(Activity.RESULT_CANCELED); + finish(); + } +} diff --git a/mobile-translate/app/src/main/java/com/google/samples/mobiledoctranslate/MainActivity.java b/mobile-translate/app/src/main/java/com/google/samples/mobiledoctranslate/MainActivity.java new file mode 100644 index 000000000..528d733dd --- /dev/null +++ b/mobile-translate/app/src/main/java/com/google/samples/mobiledoctranslate/MainActivity.java @@ -0,0 +1,849 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.mobiledoctranslate; + +import com.google.android.gms.auth.GoogleAuthException; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GoogleApiAvailability; +import com.google.api.client.extensions.android.http.AndroidHttp; +import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; +import com.google.api.client.googleapis.extensions.android.gms.auth.GooglePlayServicesAvailabilityIOException; +import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestInitializer; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.ExponentialBackOff; +import com.google.api.services.script.model.*; +import com.google.api.services.script.Script; + +import android.Manifest; +import android.accounts.Account; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.text.method.ScrollingMovementMethod; +import android.view.View; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import java.io.IOException; +import java.lang.StringBuilder; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import pub.devrel.easypermissions.AfterPermissionGranted; +import pub.devrel.easypermissions.EasyPermissions; + +/** + * This is the main (and only) activity of the add-on. It shows the user what + * text or cells were selected, the results of translation and provides some + * UI controls. + */ +public class MainActivity extends Activity + implements EasyPermissions.PermissionCallbacks { + + /** + * The script ID for the Apps Script the add-on will call + */ + private static final String SCRIPT_ID = "ENTER_YOUR_SCRIPT_ID_HERE"; + + // Constants + private static final String FUNCTION_GET_TEXT = "getTextAndTranslation"; + private static final String FUNCTION_TRANSLATE_TEXT = "translateText"; + private static final String FUNCTION_INSERT_TEXT = "insertText"; + static final int REQUEST_AUTHORIZATION = 1001; + static final int REQUEST_GOOGLE_PLAY_SERVICES = 1002; + static final int REQUEST_PERMISSION_GET_ACCOUNTS = 1003; + private static final String[] SCOPES = { + "https://www.googleapis.com/auth/documents.currentonly", + "https://www.googleapis.com/auth/script.scriptapp", + "https://www.googleapis.com/auth/script.storage" + }; + static final String SAVED_ORIG_LANG = "origLangPosition"; + static final String SAVED_DEST_LANG = "destLangPosition"; + private static final int CALL_GET_TEXT = 0; + private static final int CALL_TRANSLATE_TEXT = 1; + private static final int CALL_REPLACE_TEXT = 2; + + /** + * An Apps Script API service object used to access the API, and related + * objects + */ + Script mService = null; + GoogleAccountCredential mCredential = null; + final HttpTransport mTransport = AndroidHttp.newCompatibleTransport(); + final JsonFactory mJsonFactory = JacksonFactory.getDefaultInstance(); + + // Layout components + private TextView mSelectedText; + private EditText mTranslationText; + private Button mReplaceButton; + private ProgressDialog mProgress; + + // Translation language controls + private String mOrigLang; + private String mDestLang; + private int mPrevOrigSpinnerPos; + private int mPrevDestSpinnerPos; + + // Other variables + private NetworkReceiver mReceiver; + private boolean mConnectionAvailable; + private int mLastFunctionCalled; + private String mState; + private Account mAccount; + + /** + * Create the main activity. + * @param savedInstanceState previously saved instance data + */ + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + // Verify the add-on was called from the Docs editor. + if (! "com.google.android.apps.docs.editors.docs".equals( + getCallingPackage())) { + showErrorDialog(getString(R.string.unexpected_app) + + getCallingPackage()); + } + + // Acquire the doc/sheet state from the incoming intent. + // It's also possible to acquire the docId from the intent; + // that is not used in this example, however. + mState = getIntent().getStringExtra( + "com.google.android.apps.docs.addons.SessionState"); + mAccount = getIntent().getParcelableExtra( + "com.google.android.apps.docs.addons.Account"); + + // Load previously chosen language selections, if any + SharedPreferences settings = getPreferences(Context.MODE_PRIVATE); + mPrevOrigSpinnerPos = settings.getInt(SAVED_ORIG_LANG, 0); + mOrigLang = getLangIdFromSpinnerPosition(mPrevOrigSpinnerPos, false); + mPrevDestSpinnerPos = settings.getInt(SAVED_DEST_LANG, 0); + mDestLang = getLangIdFromSpinnerPosition(mPrevDestSpinnerPos, true); + + // Initialize layout objects + mProgress = new ProgressDialog(MainActivity.this); + + mSelectedText = (TextView) findViewById(R.id.selected_text); + mSelectedText.setVerticalScrollBarEnabled(true); + mSelectedText.setMovementMethod(new ScrollingMovementMethod()); + + mTranslationText = (EditText) findViewById(R.id.translated_text); + mTranslationText.setVerticalScrollBarEnabled(true); + mTranslationText.setMovementMethod(new ScrollingMovementMethod()); + + mReplaceButton = (Button) findViewById(R.id.replace_button); + + Spinner origLangSpinner = (Spinner) findViewById(R.id.origin_lang); + Spinner destLangSpinner = (Spinner) findViewById(R.id.dest_lang); + origLangSpinner.setSelection(mPrevOrigSpinnerPos); + destLangSpinner.setSelection(mPrevDestSpinnerPos); + origLangSpinner.setOnItemSelectedListener( + new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected( + AdapterView parent, View view, int pos, long id) { + if (pos != mPrevOrigSpinnerPos) { + mPrevOrigSpinnerPos = pos; + mOrigLang = getLangIdFromSpinnerPosition(pos, false); + SharedPreferences settings = + getPreferences(Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + editor.putInt(SAVED_ORIG_LANG, pos); + editor.apply(); + translate(); + } + } + + @Override + public void onNothingSelected(AdapterView parent) {} + }); + destLangSpinner.setOnItemSelectedListener( + new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected( + AdapterView parent, View view, int pos, long id) { + if (pos != mPrevDestSpinnerPos) { + mPrevDestSpinnerPos = pos; + mDestLang = getLangIdFromSpinnerPosition(pos, true); + SharedPreferences settings = + getPreferences(Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + editor.putInt(SAVED_DEST_LANG, pos); + editor.apply(); + translate(); + } + } + + @Override + public void onNothingSelected(AdapterView parent) {} + }); + + // Register BroadcastReceiver to track connection changes, and + // determine if a connection is initially available + IntentFilter filter = + new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); + mReceiver = new NetworkReceiver(); + MainActivity.this.registerReceiver(mReceiver, filter); + updateButtonEnableStatus(); + + // Start the add-on by attempting to retrieve the selected text from + // the Doc that fired the add-on + callAppsScriptTask(CALL_GET_TEXT); + } + + /** + * Extend the given HttpRequestInitializer (usually a Credentials object) + * with additional initialize() instructions. + * + * @param requestInitializer the initializer to copy and adjust; typically + * a Credential object + * @return an initializer with an extended read timeout + */ + private static HttpRequestInitializer setHttpTimeout( + final HttpRequestInitializer requestInitializer) { + return new HttpRequestInitializer() { + @Override + public void initialize(HttpRequest httpRequest) + throws java.io.IOException { + requestInitializer.initialize(httpRequest); + // This allows the API to call (and avoid timing out on) + // functions that take up to 30 seconds to complete. Note that + // the maximum allowed script run time is 6 minutes. + httpRequest.setReadTimeout(30000); + } + }; + } + + /** + * Clean up and destroy the main activity. + */ + @Override + public void onDestroy() { + super.onDestroy(); + // Unregister the connectivity broadcast receiver. + if (mReceiver != null) { + MainActivity.this.unregisterReceiver(mReceiver); + } + } + + /** + * Called when an activity launched here (specifically, AccountPicker + * and authorization) exits, giving you the requestCode you started it with, + * the resultCode it returned, and any additional data from it. + * @param requestCode code indicating which activity result is incoming + * @param resultCode code indicating the result of the incoming + * activity result + * @param data Intent (containing result data) returned by incoming + * activity result + */ + @Override + protected void onActivityResult( + int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + switch(requestCode) { + case REQUEST_GOOGLE_PLAY_SERVICES: + if (resultCode == RESULT_OK) { + callAppsScriptTask(mLastFunctionCalled); + } else { + showErrorDialog(getString(R.string.gps_required)); + } + break; + case REQUEST_AUTHORIZATION: + if (resultCode == RESULT_OK) { + callAppsScriptTask(mLastFunctionCalled); + } else { + showErrorDialog(getString(R.string.no_auth_provided)); + } + break; + } + } + + /** + * Call the API to execute an Apps Script function, after verifying + * all the preconditions are satisfied. The preconditions are: Google + * Play Services is installed, the device has a network connection, and + * the Execution API service and credentials have been created. + * @param functionToCall code indicating which function to call using + * the API + */ + private void callAppsScriptTask(int functionToCall) { + mLastFunctionCalled = functionToCall; + if (! isGooglePlayServicesAvailable()) { + toast(getString(R.string.gps_required)); + acquireGooglePlayServices(); + } else if (! mConnectionAvailable) { + toast(getString(R.string.no_network)); + } else if (! hasValidCredentials()) { + createCredentialsAndService(); + } else { + switch (functionToCall) { + case CALL_GET_TEXT: + new GetTextTask().execute(mOrigLang, mDestLang, false); + break; + case CALL_TRANSLATE_TEXT: + String originalText = mSelectedText.getText().toString(); + new TranslateTextTask().execute( + originalText, mOrigLang, mDestLang); + break; + case CALL_REPLACE_TEXT: + String translation = mTranslationText.getText().toString(); + new ReplaceTextTask().execute(translation); + break; + } + } + } + + /** + * Attempts to initialize credentials and service object (prior to a call + * to the API); uses the account provided by the calling app. This + * requires the GET_ACCOUNTS permission to be explicitly granted by the + * user; this will be requested here if it is not already granted. The + * AfterPermissionGranted annotation indicates that this function will be + * rerun automatically whenever the GET_ACCOUNTS permission is granted. + */ + @AfterPermissionGranted(REQUEST_PERMISSION_GET_ACCOUNTS) + private void createCredentialsAndService() { + if (EasyPermissions.hasPermissions( + MainActivity.this, Manifest.permission.GET_ACCOUNTS)) { + mCredential = GoogleAccountCredential.usingOAuth2( + getApplicationContext(), Arrays.asList(SCOPES)) + .setBackOff(new ExponentialBackOff()) + .setSelectedAccountName(mAccount.name); + mService = new com.google.api.services.script.Script.Builder( + mTransport, mJsonFactory, setHttpTimeout(mCredential)) + .setApplicationName(getString(R.string.app_name)) + .build(); + updateButtonEnableStatus(); + + // Callback to retry the API call with valid service/credentials + callAppsScriptTask(mLastFunctionCalled); + } else { + // Request the GET_ACCOUNTS permission via a user dialog + EasyPermissions.requestPermissions( + MainActivity.this, + getString(R.string.get_accounts_rationale), + REQUEST_PERMISSION_GET_ACCOUNTS, + Manifest.permission.GET_ACCOUNTS); + } + } + + /** + * Returns true if a valid service object has been created and instantiated + * with valid OAuth credentials; returns false otherwise. + * @return true if the service and credentials are valid; false otherwise. + */ + private boolean hasValidCredentials() { + return mService != null + && mCredential != null + && mCredential.getSelectedAccountName() != null; + } + + /** + * Respond to requests for permissions at runtime for SDK 23 and above. + * @param requestCode The request code passed in + * requestPermissions(android.app.Activity, String, int, String[]) + * @param permissions The requested permissions. Never null. + * @param grantResults The grant results for the corresponding permissions + * which is either PERMISSION_GRANTED or PERMISSION_DENIED. Never null. + */ + @Override + public void onRequestPermissionsResult(int requestCode, + @NonNull String[] permissions, + @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + EasyPermissions.onRequestPermissionsResult( + requestCode, permissions, grantResults, MainActivity.this); + } + + /** + * Callback for when a permission is granted using the EasyPermissions + * library. + * @param requestCode The request code associated with the requested + * permission + * @param list The requested permission list. Never null. + */ + @Override + public void onPermissionsGranted(int requestCode, List list) { + // Do nothing. + } + + /** + * Callback for when a permission is denied using the EasyPermissions + * library. Displays status message and disables functionality that + * would require that permission. + * @param requestCode The request code associated with the requested + * permission + * @param list The requested permission list. Never null. + */ + @Override + public void onPermissionsDenied(int requestCode, List list) { + toast(getString(R.string.get_accounts_denied_message)); + updateButtonEnableStatus(); + } + + /** + * Given the position of one of the language spinners, return the language + * id corresponding to that position. + * @param pos spinner position + * @param omitAutoDetect true if the spinner does not include 'Auto-detect' + * as the first option + * @return String two-letter language id + */ + private String getLangIdFromSpinnerPosition(int pos, boolean omitAutoDetect) { + String id; + if (omitAutoDetect) { + pos++; + } + switch (pos) { + case 0: id = ""; break; // Auto-detect (input language only) + case 1: id = "ar"; break; // Arabic + case 2: id = "zh-CN"; break; // Chinese (Simplified) + case 3: id = "en"; break; // English + case 4: id = "fr"; break; // French + case 5: id = "de"; break; // German + case 6: id = "hi"; break; // Hindi + case 7: id = "ja"; break; // Japanese + case 8: id = "pt"; break; // Portuguese + case 9: id = "es"; break; // Spanish + default: id = "en"; break; + } + return id; + } + + /** + * Call the API to translate the selected text. + */ + private void translate() { + String originalText = mSelectedText.getText().toString(); + if (originalText.length() != 0) { + callAppsScriptTask(CALL_TRANSLATE_TEXT); + } + } + + /** + * Call the API to replace the translated text back to the original + * document. + * @param v The button's View context + */ + public void replace(View v) { + String translation = mTranslationText.getText().toString(); + if (translation.length() != 0) { + callAppsScriptTask(CALL_REPLACE_TEXT); + } + } + + /** + * Cancel the add-on and return without action to the calling app. + * @param v The button's View context + */ + public void cancel(View v) { + finishWithState(Activity.RESULT_CANCELED); + } + + /** + * End the add-on and return to the calling application. + * @param state result code for add-on: one of Activity.RESULT_CANCELED or + * Activity.RESULT_OK + */ + private void finishWithState(int state) { + dismissProgressDialog(); + setResult(state); + finish(); + } + + /** + * Display a short toast message. + * @param message text to display + */ + private void toast(String message) { + Toast.makeText(MainActivity.this, message, Toast.LENGTH_SHORT).show(); + } + + /** + * Checks whether the device currently has a network connection. + * @return true if the device has a network connection, false otherwise + */ + private boolean isDeviceOnline() { + ConnectivityManager connMgr = + (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); + return (networkInfo != null && networkInfo.isConnected()); + } + + /** + * Check that Google Play services APK is installed and up to date. + * @return true if Google Play Services is available and up to + * date on this device; false otherwise. + */ + private boolean isGooglePlayServicesAvailable() { + GoogleApiAvailability apiAvailability = + GoogleApiAvailability.getInstance(); + final int connectionStatusCode = + apiAvailability.isGooglePlayServicesAvailable(MainActivity.this); + return connectionStatusCode == ConnectionResult.SUCCESS; + } + + /** + * Attempt to resolve a missing, out-of-date, invalid or disabled Google + * Play Services installation via a user dialog, if possible. + */ + private void acquireGooglePlayServices() { + GoogleApiAvailability apiAvailability = + GoogleApiAvailability.getInstance(); + final int connectionStatusCode = + apiAvailability.isGooglePlayServicesAvailable(MainActivity.this); + if (apiAvailability.isUserResolvableError(connectionStatusCode)) { + showGooglePlayServicesAvailabilityErrorDialog(connectionStatusCode); + } + } + /** + * Display an error dialog showing that Google Play Services is missing + * or out of date. + * @param connectionStatusCode code describing the presence (or lack of) + * Google Play Services on this device + */ + private void showGooglePlayServicesAvailabilityErrorDialog( + final int connectionStatusCode) { + Dialog dialog = + GoogleApiAvailability.getInstance().getErrorDialog( + MainActivity.this, + connectionStatusCode, + REQUEST_GOOGLE_PLAY_SERVICES); + dialog.show(); + } + + /** + * Check the current connectivity status of the device and enable/disable + * the highlight buttons if the device is online/offline, respectively. + */ + private void updateButtonEnableStatus() { + mConnectionAvailable = isDeviceOnline(); + boolean enable = mConnectionAvailable && hasValidCredentials(); + mReplaceButton.setEnabled(enable); + } + + /** + * Show a dialog with an error message, with a button to cancel out of + * the add-on. + * @param errorMessage Error message to display + */ + protected void showErrorDialog(String errorMessage) { + AlertDialog.Builder alertDialogBuilder = + new AlertDialog.Builder(MainActivity.this); + alertDialogBuilder.setTitle(getString(R.string.error_occurred)); + alertDialogBuilder + .setMessage(errorMessage) + .setCancelable(false) + .setNegativeButton( + getString(R.string.exit_button), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + finishWithState(Activity.RESULT_CANCELED); + } + }); + dismissProgressDialog(); + AlertDialog alertDialog = alertDialogBuilder.create(); + alertDialog.show(); + } + + /** + * Dismiss the ProgressDialog, if it is visible. + */ + public void dismissProgressDialog() { + if (mProgress != null && mProgress.isShowing()) { + Context context = + ((ContextWrapper) mProgress.getContext()).getBaseContext(); + // Dismiss only if launching activity hasn't been finished or + // destroyed + if(! (context instanceof Activity && + ((Activity)context).isFinishing() || + ((Activity)context).isDestroyed())) { + mProgress.dismiss(); + } + } + } + + /** + * This BroadcastReceiver intercepts the + * android.net.ConnectivityManager.CONNECTIVITY_ACTION, which indicates a + * connection change. This is used to determine if the API can be called. + */ + public class NetworkReceiver extends BroadcastReceiver { + /** + * Responds to a connection change, recording whether a connection is + * available. + * @param context The Context in which the receiver is running + * @param intent The Intent being received + */ + @Override + public void onReceive(Context context, Intent intent) { + // Checks the network connection. Based on the + // result, enables/disables flag to allow API calls and + // enables/disables buttons. + updateButtonEnableStatus(); + if (!mConnectionAvailable) { + toast(getString(R.string.no_network)); + } + } + } + + /** + * Abstract class for handling Execution API calls. Typically a subclass of + * this is created for each Apps Script function that will be called. + * Placing the API calls in their own task ensures the UI stays responsive. + */ + public abstract class CallApiTask extends AsyncTask { + + private Exception mLastError = null; + protected String mFunctionName; + + /** + * Background task to call Apps Script API. + * @param params Object parameters; used as parameters for that + * function, in the given order + * @return an object returned by the API; may be null + */ + @Override + protected Object doInBackground(Object... params) { + try { + return executeCall(mFunctionName, Arrays.asList(params)); + } catch (Exception e) { + mLastError = e; + cancel(true); + return null; + } + } + + /** + * Handle cancel requests -- specifically, those caused by exceptions + * raised when attempting to call the API. + */ + @Override + protected void onCancelled() { + mProgress.hide(); + if (mLastError != null) { + if (mLastError instanceof GooglePlayServicesAvailabilityIOException) { + showGooglePlayServicesAvailabilityErrorDialog( + ((GooglePlayServicesAvailabilityIOException) mLastError).getConnectionStatusCode()); + } else if (mLastError instanceof UserRecoverableAuthIOException) { + startActivityForResult( + ((UserRecoverableAuthIOException) mLastError).getIntent(), + MainActivity.REQUEST_AUTHORIZATION); + } else { + showErrorDialog(mLastError.toString()); + } + } + } + + /** + * Interpret an error response returned by the API and return a String + * summary. The summary will include the general error message and + * (in most cases) a stack trace. + * @param op the Operation returning an error response + * @return summary of error response, or null if Operation returned no + * error + */ + protected String getScriptError(Operation op) { + if (op.getError() == null) { + return null; + } + + // Extract the first (and only) set of error details and cast as a + // Map. The values of this map are the script's 'errorMessage' and + // 'errorType', and an array of stack trace elements (which also + // need to be cast as Maps). + Map detail = op.getError().getDetails().get(0); + List> stacktrace = + (List>)detail.get("scriptStackTraceElements"); + + StringBuilder sb = + new StringBuilder(getString(R.string.script_error)); + sb.append(detail.get("errorMessage")); + + if (stacktrace != null) { + // There may not be a stacktrace if the script didn't start + // executing. + sb.append(getString(R.string.script_error_trace)); + for (Map elem : stacktrace) { + sb.append("\n "); + sb.append(elem.get("function")); + sb.append(":"); + sb.append(elem.get("lineNumber")); + } + } + sb.append("\n"); + return sb.toString(); + } + + /** + * Given a script function and a list of parameter objects, create a + * request and run it with the API. + * @param functionName script function name to call + * @param params parameters needed by that function; may be null + * @return Object returned from a successful execution; may be null + * @throws IOException + * @throws GoogleAuthException + */ + protected Object executeCall(String functionName, List params) + throws IOException, GoogleAuthException { + // Create execution request. + ExecutionRequest request = new ExecutionRequest() + .setFunction(functionName) + .setSessionState(mState); + if (params != null) { + request.setParameters(params); + } + + // Call the API and return the results (as an Operation object). + Operation op = mService.scripts().run(SCRIPT_ID, request).execute(); + + // If the response from the API contains an error, throw an + // exception to display it. + if (op.getError() != null) { + throw new IOException(getScriptError(op)); + } + + // Return null if the API didn't yield a result. + if (op.getResponse() == null || + op.getResponse().get("result") == null) { + return null; + } + + return op.getResponse().get("result"); + } + } + + /** + * An asynchronous task that handles the Apps Script API call to get + * selected text data, and the translation of it to the previously chosen + * destination language (or the default destination language). + */ + public class GetTextTask extends CallApiTask { + /** + * Clear the display and show the progress bar prior to calling the + * API. + */ + @Override + protected void onPreExecute() { + mFunctionName = FUNCTION_GET_TEXT; + mProgress.setMessage(getString(R.string.retrieve_status)); + mProgress.show(); + } + + /** + * Take the text output and display it. If no data is returned, update + * the status bar accordingly. + * @param scriptResult object returned by the script function; may be + * null + */ + @Override + protected void onPostExecute(Object scriptResult) { + mProgress.hide(); + if (scriptResult != null) { + // Place the original text and default translation into the UI. + Map data = (Map) scriptResult; + mSelectedText.setText(data.get("text")); + mTranslationText.setText(data.get("translation")); + } + } + } + + /** + * An asynchronous task that handles the Apps Script API call to + * translate text data. + */ + public class TranslateTextTask extends CallApiTask { + /** + * Clear the display and show the progress bar prior to calling the + * API. + */ + @Override + protected void onPreExecute() { + mFunctionName = FUNCTION_TRANSLATE_TEXT; + mProgress.setMessage(getString(R.string.translate_status)); + mProgress.show(); + } + + /** + * Take the text output and display it. If no data is returned, update + * the status bar accordingly. + * @param scriptResult object returned by the script function; may be + * null + */ + @Override + protected void onPostExecute(Object scriptResult) { + mProgress.hide(); + if (scriptResult != null) { + String data = (String) scriptResult; + mTranslationText.setText(data); + } + } + } + + /** + * An asynchronous task that handles the Apps Script API call to send + * (translated) text back into the document, replacing the previous + * selection. + */ + public class ReplaceTextTask extends CallApiTask { + /** + * Show the progress bar prior to calling the API. + */ + @Override + protected void onPreExecute() { + mFunctionName = FUNCTION_INSERT_TEXT; + mProgress.setMessage(getString(R.string.replace_status)); + mProgress.show(); + } + + /** + * After finishing the API call, clear the progress bar and close + * the add-on. + * @param scriptResult object returned by the script function; should + * always be null for this script function + */ + @Override + protected void onPostExecute(Object scriptResult) { + mProgress.hide(); + finishWithState(Activity.RESULT_OK); + } + } +} diff --git a/mobile-translate/app/src/main/res/layout-land/activity_main.xml b/mobile-translate/app/src/main/res/layout-land/activity_main.xml new file mode 100644 index 000000000..80647ff0d --- /dev/null +++ b/mobile-translate/app/src/main/res/layout-land/activity_main.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile-translate/app/src/main/res/layout/activity_main.xml b/mobile-translate/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..caf9e4548 --- /dev/null +++ b/mobile-translate/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile-translate/app/src/main/res/layout/buttons.xml b/mobile-translate/app/src/main/res/layout/buttons.xml new file mode 100644 index 000000000..6b336e14f --- /dev/null +++ b/mobile-translate/app/src/main/res/layout/buttons.xml @@ -0,0 +1,28 @@ + + - - - - - diff --git a/bibstro/citationdialog_js.html b/bibstro/citationdialog_js.html deleted file mode 100644 index 11d2fc708..000000000 --- a/bibstro/citationdialog_js.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - - diff --git a/bibstro/common.gs b/bibstro/common.gs deleted file mode 100644 index 7795b1aa2..000000000 --- a/bibstro/common.gs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2013 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * @fileOverview Google Apps Script and Google Docs demo: Bibstro. Utility - * functions. - * @author Jonathan Rascher - * @author Saurabh Gupta - */ - - -/** - * This is a slightly simplified version of Closure's {@code goog.inherits} - * function. It sets up the prototype of the first constructor so that the first - * class acts as a subclass of the second class. - * @param {!Function} childCtor The subclass's constructor function. - * @param {!Function} parentCtor The superclass's constructor function. - */ -function inherits(childCtor, parentCtor) { - /** @constructor */ function childPrototypeCtor() {} - childPrototypeCtor.prototype = parentCtor.prototype; - childCtor.prototype = new childPrototypeCtor(); - childCtor.prototype.constructor = parentCtor; // Make instanceof work. -} - - -/** - * Takes an object like {@code {'foo[0]': 'a', 'foo[1]': 'b', 'foo[2]': 'c'}} - * and turns it into an array like {@code ['a', 'b', 'c']}. - * @param {!Object} obj The object containing the array key/value pairs (and - * possibly other properties as well. - * @param {string} prefix The portion of the array key names before the square - * brackets (e.g., {@code foo} in the example above). - * @return {!Array} The array extracted from the original object. - */ -function extractArrayFields(obj, prefix) { - var arrayIndicies = Object.keys(obj).map(function(key) { - if (key.indexOf(prefix) != 0) { - return null; - } - var arrayIndexStr = key.substring(prefix.length); - var matchResults = /^\[([0-9]+)\]$/.exec(arrayIndexStr); - return matchResults ? Number(matchResults[1]) : null; - }).filter(function(index) { - return index != null; - }).sort(); - - var ret = []; - for (var i = 0; i < arrayIndicies.length; ++i) { - ret[i] = obj[prefix + '[' + i + ']']; - } - return ret; -} - - -/** - * Escapes HTML special characters. - * @param {string} unescapedStr A string that might contain unescaped HTML tags - * or entities. - * @return {string} The same string with characters having special meaning in - * HTML properly escaped. - */ -function escapeHtml(unescapedStr) { - var template = HtmlService.createTemplate(''); - template.unescaped = unescapedStr; - return template.evaluate().getContent(); -} - - -/** - * Escapes RegExp special characters. - * @param {string} unescapedStr A string that might contain unescaped regular - * expression metacharacters. - * @return {string} The same string with characters having special meaning in - * JavaScript regular expressions properly escaped. - * @see StackOverflow - */ -function escapeRegExp(unescapedStr) { - return unescapedStr.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); -} diff --git a/bibstro/configdialog.html b/bibstro/configdialog.html deleted file mode 100644 index 66cb93493..000000000 --- a/bibstro/configdialog.html +++ /dev/null @@ -1,86 +0,0 @@ - -Google Apps Script and Google Docs demo: Bibstro - - - - -
-
- Configure bibliography settings - -

- - Before you begin entering sources and inserting citations, please take a minute to configure - the application to your liking. Thank you kindly! - - Change your citation format or other application settings here. None of your sources will be - lost, so feel free to play around. - -

- - -
- -
- -
-
- - -
- -
- - Highly recommended, or else existing sources may be deleted. -
-
- -
- -
- - -
-
- - diff --git a/bibstro/configdialog_js.html b/bibstro/configdialog_js.html deleted file mode 100644 index 00bde87f4..000000000 --- a/bibstro/configdialog_js.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - diff --git a/bibstro/controller.gs b/bibstro/controller.gs deleted file mode 100644 index bf1e6ec3c..000000000 --- a/bibstro/controller.gs +++ /dev/null @@ -1,467 +0,0 @@ -// Copyright 2013 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * @fileOverview Google Apps Script and Google Docs demo: Bibstro. Controller - * class. User interaction event handlers call into this class to perform any - * real work. The controller is responsible for showing UI, as well as managing - * interactions with the {@code ScriptDb}-based data store and the - * {@code DocumentApp}-based document model. - * @author Jonathan Rascher - * @author Saurabh Gupta - */ - - - -/** - * Constructs a new instance of the controller class. Though this is not - * enforced in code, the controller should be treated as a singleton; i.e., at - * most once instance should be created by server-side script execution. - * @constructor - */ -var Controller = function() { - /** - * A caching wrapper around the {@code ScriptDb} data store. - * @type {!DataStore} - * @private - */ - this.dataStore_ = new DataStore(); - - /** - * A simple, abstract document model based on the {@code DocumentApp} service. - * @type {!DocumentModel} - * @private - */ - this.documentModel_ = new DocumentModel(DocumentApp.getActiveDocument()); - - var config = this.dataStore_.getConfig(); -}; - - -/** - * The desired width in pixels of {@code citationdialog.html}. - * @type {number} - * @const - * @private - */ -Controller.CITATION_DIALOG_WIDTH_ = 600; - - -/** - * The desired height in pixels of {@code citationdialog.html}. - * @type {number} - * @const - * @private - */ -Controller.CITATION_DIALOG_HEIGHT_ = 575; - - -/** - * The desired width in pixels of {@code configdialog.html}. - * @type {number} - * @const - * @private - */ -Controller.CONFIG_DIALOG_WIDTH_ = 625; - - -/** - * The desired height in pixels of {@code configdialog.html}. - * @type {number} - * @const - * @private - */ -Controller.CONFIG_DIALOG_HEIGHT_ = 525; - - -/** - * The desired width in pixels of {@code referencedialog.html}. - * @type {number} - * @const - * @private - */ -Controller.REFERENCE_DIALOG_WIDTH_ = 700; - - -/** - * The desired height in pixels of {@code referencedialog.html}. - * @type {number} - * @const - * @private - */ -Controller.REFERENCE_DIALOG_HEIGHT = 650; - - -/** - * The desired width in pixels of {@code managereferencessidebar.html}. - * @type {number} - * @const - * @private - */ -Controller.MANAGE_REFERENCES_SIDEBAR_WIDTH_ = 450; - - -/** - * Email address format strings for various cell phone carriers' email-to-SMS - * gateways. - * @enum {string} - * @private - */ -Controller.PhoneCarriersToEmailAddressFormats_ = { - 'att': '%s@txt.att.net', - 'sprint': '%s@messaging.sprintpcs.com', - 'tmobile': '%s@tmomail.net', - 'verizon': '%s@vtext.com' -}; - - -/** - * On initial installation, this method shows a configuration dialog prompting - * the user to chose the citation format of their desire. If configuration - * already exists, this method does nothing. - * @return {boolean} True if the initial setup dialog was shown and false if - * initial setup has already been performed for this document. - */ -Controller.prototype.showInitialSetupDialog = function() { - if (!this.dataStore_.getConfig()) { - // If there's no configuration saved in the data store, this must be the - // first time the user has opened the current document with the extension - // installed. Show a configuration dialog allowing the user to choose a - // citation format. - this.showConfigDialog(true /* opt_initialSetup */); - return true; // Don't proceed with initialization until configured. - } - return false; // If we already have a config, proceed with initialization. -}; - - -/** - * Displays {@code configdialog.html}, allowing the user to add or edit - * Bibstro's configuration for this document. - * @param {boolean=} opt_initialSetup Whether this is the app's initial setup - * step (in which case no configuration currently exists in - * {@code ScriptDb}. - */ -Controller.prototype.showConfigDialog = function(opt_initialSetup) { - var config = this.dataStore_.getConfig(); - var args = {'initialSetup': !!opt_initialSetup, 'bibStrategies': []}; - - // Populate human-readable bibliography strategy names for the various - // citation formats Bibstro supports (e.g., MLA and APA). These names are - // passed into the dialog view. - for (var bibStrategyName in BibStrategy.Implementation) { - var bibStrategy = { - 'name': bibStrategyName, - 'description': BibStrategy.Implementation[bibStrategyName].description - }; - if (config && config.bibStrategy == bibStrategyName) { - bibStrategy['selected'] = true; - } - args['bibStrategies'].push(bibStrategy); - } - - this.showDialog('configdialog.html', Controller.CONFIG_DIALOG_WIDTH_, - Controller.CONFIG_DIALOG_HEIGHT_, args); -}; - - -/** - * Displays {@code referencedialog.html}, allowing the user to insert or update - * a reference in the bibliography data store. - * @param {string=} opt_id If present, the {@code ScriptDb} identifier of the - * existing reference to be updated. If absent, an empty dialog shall be - * shown, allowing the user to insert a new reference instead. - */ -Controller.prototype.showReferenceDialog = function(opt_id) { - this.showDialog('referencedialog.html', Controller.REFERENCE_DIALOG_WIDTH_, - Controller.REFERENCE_DIALOG_HEIGHT, - opt_id ? this.dataStore_.loadReference(opt_id) : undefined); -}; - - -/** - * Displays {@code managereferencessidebar.html}, a live, formatted view of the - * bibliography data store that appears adjacent to the Google Docs document. - */ -Controller.prototype.showManageReferenceSidebar = function() { - this.showSidebar('managereferencessidebar.html', - 'Manage Bibliography Sources', - Controller.MANAGE_REFERENCES_SIDEBAR_WIDTH_); -}; - - -/** - * Attempts to import reference data from an existing document bibliography into - * the document's data store. The preexisting bibliography must be formatted - * according to the currently selected citation format for this to have any - * change of succeeding. - */ -Controller.prototype.importReferencesFromDocument = function() { - var references = this.documentModel_.extractReferences(this.getBibStrategy()); - for (var i = 0; i < references.length; ++i) { - this.dataStore_.insertOrUpdateReference(references[i]); - } -}; - - -/** - * Displays {@code citationdialog.html}, allowing the user to insert an inline - * citation at the current cursor location. If there are no references in the - * data store, the user will be prompted to add a reference instead. - */ -Controller.prototype.showCitationDialog = function() { - var referenceData = this.loadReferenceData(); - - // Before showing the "insert citation" dialog, make sure the user has - // actually put at least one reference into the data store. If no references - // can be found, ask the user if they'd like to add a reference instead. - if (!referenceData['references'].length) { - var confirmation = DocumentApp.getUi().alert( - 'Google Apps Script and Google Docs demo: Bibstro', - 'This document does not currently have any sources to cite. Would you' + - 'like to add a source now?', - DocumentApp.getUi().ButtonSet.YES_NO); - if (confirmation == DocumentApp.getUi().Button.YES) { - this.showReferenceDialog(); - } - return; - } - - this.showDialog('citationdialog.html', - Controller.CITATION_DIALOG_WIDTH_, Controller.CITATION_DIALOG_HEIGHT_, - referenceData); -}; - - -/** - * Returns the document's current bibliography strategy. It's a citation format - * (e.g., MLA or APA), essentially. - * @return {BibStrategy} The document's current bibliography strategy, or null - * if initial setup hasn't yet been performed. - */ -Controller.prototype.getBibStrategy = function() { - var config = this.dataStore_.getConfig(); - return config ? - new BibStrategy.Implementation[this.dataStore_.getConfig().bibStrategy]. - ctor() : - null; -}; - - -/** - * Loads the full list of references from the data store, producing properly - * formatted HTML bibliography entries for each. - * @return {{bibStrategy: string, - * references: !Array.<{id: string, html: string}>, - * highlights: !Array.}} - */ -Controller.prototype.loadReferenceData = function() { - var references = this.dataStore_.loadReferences(). - sort(this.getBibStrategy().getReferenceComparator()); - - var referenceDataList = []; - for (var i = 0; i < references.length; ++i) { - var reference = references[i]; - - // Turn the structured reference data into a chunk of formatted HTML to send - // down to the client. - var referenceData = {}; - referenceData['id'] = reference.id; - referenceData['html'] = this.getBibliographyEntryHtml(reference); - referenceDataList.push(referenceData); - } - - return { - 'references': referenceDataList, - 'highlights': this.dataStore_.getHighlights() - }; -}; - - -/** - * Formats a reference model object from the data store as HTML according to the - * current bibliography strategy (citation format). - * @param {!DataStore.Reference} reference The reference object to be rendered. - * @return {string} HTML that can be rendered directly into an - * {@link HtmlTemplate} view. - */ -Controller.prototype.getBibliographyEntryHtml = function(reference) { - return this.getBibStrategy().getBibliographyEntry(reference). - map(function(bibEntryToken) { - return bibEntryToken.publicationTitle ? - '' + escapeHtml(bibEntryToken.text) + '' : - escapeHtml(bibEntryToken.text); - }).join(''); -}; - - -/** - * Highlights or unhighlights a reference's citations in the document and marks - * the reference as highlighted or not in the {@code ScriptDb} data store. - * @param {string} id The {@code ScriptDb} identifier of the reference to - * highlight or unhighlight. - * @param {boolean=} opt_unhighlight Whether to clear any existing highlights - * for this reference instead of setting new highlights. - */ -Controller.prototype.highlightReference = function(id, opt_unhighlight) { - this.dataStore_.highlightReference(id, opt_unhighlight); - this.documentModel_.highlightMatchingCitations(this.getBibStrategy(), - this.dataStore_.loadReferences(), this.dataStore_.loadReference(id), - opt_unhighlight); -}; - - -/** - * Deletes a reference from the {@code ScriptDb} data store. - * @param {string} id The {@code ScriptDb} identifier of the reference to - * delete. - */ -Controller.prototype.deleteReference = function(id) { - this.dataStore_.deleteReference(id); - this.refreshLiveBibliography(); -}; - - -/** - * Inserts of updates a reference in the {@code ScriptDb} data store. - * @param {!DataStore.Reference} reference The reference model object to update - * (if an {@code id} property is present) or insert (if no ID is present). - */ -Controller.prototype.saveReference = function(reference) { - this.dataStore_.insertOrUpdateReference(reference); - this.refreshLiveBibliography(); -}; - - -/** - * Saves a configuration object containing app settings (e.g., the current - * citation format) back to the data store. - * @param {!DataStore.Config} config The configuration settings to save. - */ -Controller.prototype.saveConfig = function(config) { - this.dataStore_.setConfig(config); -}; - - -/** - * Inserts an inline citation at the document cursor location using the - * currently selected citation format. - * @param {string} referenceId The {@code ScriptDb} identifier of the reference - * being cited. - * @param {number} startPage The first page included in this citation. - * @param {number} endPage The last page included in this citation. - * @param {boolean} firstMention Whether or not this reference has been cited - * earlier in the document. For certain citation formats (e.g., APA), the - * first citation associated with a given reference must contain information - * that should be omitted in later citations. - * @param {boolean} abbreviateCitation If true, the inserted inline citation - * will be abbreviated somehow. Exactly how depends on the specific citation - * format selected; however, in most formats the author(s) will be omitted - * and only the page number(s) will be included. - */ -Controller.prototype.insertCitation = function(referenceId, startPage, endPage, - firstMention, abbreviateCitation) { - this.documentModel_.insertCitationAtCursor(this.getBibStrategy(), - this.dataStore_.loadReference(referenceId), startPage, endPage, - firstMention, abbreviateCitation); -}; - - -/** - * Refreshes the contents of the active document's bibliography (e.g., "Works - * Cited" list for MLA), appending a new bibliography if one doesn't already - * exist. - */ -Controller.prototype.appendLiveBibliography = function() { - this.documentModel_.appendOrUpdateBibliography(this.getBibStrategy(), - this.dataStore_.loadReferences()); -}; - - -/** - * Refreshes the contents of the active document's bibliography (e.g., "Works - * Cited" list for MLA). If no live bibliography currently exists, then no - * action will be taken. - * @param {!BibStrategy=} opt_prevBibStrategy If set, the bibliography strategy - * that was active before a change in citation format. This is used to find - * the document's live bibliography using the previous citation format so - * that it can be updated to the new format. - */ -Controller.prototype.refreshLiveBibliography = function(opt_prevBibStrategy) { - this.documentModel_.appendOrUpdateBibliography(this.getBibStrategy(), - this.dataStore_.loadReferences(), true /* opt_updateOnly */, - opt_prevBibStrategy); -} - - -/** - * Renders an HTML file view in a dialog above the Google Docs document. - * @param {string} fileName The file name of the template to evaluate. - * @param {number} width The desired dialog width in pixels. - * @param {number} height The desired dialog height in pixels. - * @param {!Object=} opt_args If defined, an object whose properties shall be - * copied to the {@code HtmlTemplate} object before the template is - * evaluated. - */ -Controller.prototype.showDialog = function(fileName, width, height, opt_args) { - DocumentApp.getUi().showDialog( - this.evalTemplate(fileName, undefined, opt_args). - setWidth(width). - setHeight(height)); -}; - - -/** - * Renders an HTML file view in the Google Docs sidebar. - * @param {string} fileName The file name of the template to evaluate. - * @param {string} title The title of the page, dialog, or sidebar in which the - * evaluated template will eventually be displayed. - * @param {number} width The desired sidebar width in pixels. - * @param {!Object=} opt_args If defined, an object whose properties shall be - * copied to the {@code HtmlTemplate} object before the template is - * evaluated. - */ -Controller.prototype.showSidebar = function(fileName, title, width, opt_args) { - DocumentApp.getUi().showSidebar(this.evalTemplate(fileName, title, opt_args). - setWidth(width)); -}; - - -/** - * Evaluates an {@code HtmlService} template using the specified page title and - * template arguments. - * @param {string} fileName The file name of the template to evaluate. - * @param {string=} opt_title The title of the page, dialog, or sidebar in which - * the evaluated template will eventually be displayed. - * @param {!Object=} opt_args If defined, an object whose properties shall be - * copied to the {@code HtmlTemplate} object before the template is - * evaluated. - * @return {!HtmlService.HtmlOutput} - */ -Controller.prototype.evalTemplate = function(fileName, opt_title, opt_args) { - var template = HtmlService.createTemplateFromFile(fileName); - var args = opt_args || {}; - for (var key in args) { - if (args.hasOwnProperty(key)) { - template[key] = args[key]; - } - } - - var output = template.evaluate(); - output.setSandboxMode(HtmlService.SandboxMode.NATIVE); // Enable Caja ES5. - if (opt_title != undefined) { - output.setTitle(opt_title); - } - return output; -}; diff --git a/bibstro/datastore.gs b/bibstro/datastore.gs deleted file mode 100644 index 2f4f183ab..000000000 --- a/bibstro/datastore.gs +++ /dev/null @@ -1,533 +0,0 @@ -// Copyright 2013 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * @fileOverview Google Apps Script and Google Docs demo: Bibstro. Data model - * definitions and caching data store built on top of {@code ScriptDb}. - * @author Jonathan Rascher - * @author Saurabh Gupta - */ - - - -/** - * Constructs a new caching data store object. The data store is backed by - * {@code ScriptDb}, a simple NoSQL database that holds JSON-compatible objects. - * Since all {@code ScriptDb} records end up in the same namespace, we tag each - * record with a {@code type} property determining whether the record represents - * a configuration entry or a bibliography reference. - *

- * Additionally, since {@code ScriptDb} has quotas on how often a given script - * can read from the database (i.e., a limit of queries per second or QPS), we - * use a simple write-through cache based on Apps Script's {@code CacheService}. - * @constructor - */ -var DataStore = function() { - /** - * The {@code ScriptDb} database for the active document. - * @type {!ScriptDb.ScriptDbInstance} - * @private - */ - this.scriptDb_ = ScriptDb.getMyDb(); - - - /** - * A simple memcache-style instance used as a write-through cache on top of - * {@code ScriptDb}. We use the public cache since bibliography is scoped to - * the document and is not user-specific in any way. - * @type {!CacheService.Cache} - * @private - */ - this.cache_ = CacheService.getPublicCache(); - - - /** - * The {@code ScriptDb} record representing Bibstro's current configuration, - * or null if Bibstro has not yet been configured. - * @type {ScriptDb.ScriptDbMap} - * @private - */ - this.configRecord_ = this.queryRecord_({'type': DataStore.Type_.CONFIG}); -}; - - -/** - * Key of cache record containing a JSON-formatted array of the {@code ScriptDb} - * identifiers of all references currently stored in the data store. - * @type {string} - */ -DataStore.REFERENCE_LIST_CACHE_KEY = 'refList'; - - -/** - * Key of cache record containing a JSON-formatted array of the {@code ScriptDb} - * identifiers of all references whose associated inline citations are currently - * highlighted in the document. - * @type {string} - */ -DataStore.HIGHLIGHT_LIST_CACHE_KEY = 'hglList'; - - -/** - * Unique prefix for the keys of cache records containing JSON-formatted - * reference data. - * @type {string} - */ -DataStore.REFERENCE_CACHE_KEY_PREFIX = 'ref_'; - - -/** - * Closure-style type definition documenting the model object representing - * Bibstro's configuration. - * @typedef {{bibStrategy: string}} - */ -DataStore.Config; - - -/** - * Closure-style type definition documenting the model object representing a - * single reference in Bibstro's bibliography backend. Note that this represents - * the union of properties supported by all kinds of references. Not all will - * apply under all circumstances; e.g., {@code publication} is valid for journal - * and newspaper, articles but not for books. - *

- * The {@code kind} property determines what sort of reference a given model - * object represents. - * @typedef {{id: string|undefined, - * kind: DataStore.ReferenceKind, - * title: string, - * publicationYear: number, - * publication: string|undefined, - * journalVolume: string|undefined, - * journalIssue: string|undefined, - * startPage: number|undefined, - * endPage: number|undefined, - * edition: string|undefined, - * volume: string|undefined, - * publisher: string|undefined, - * publisherCity: string|undefined}} - */ -DataStore.Reference; - - -/** - * A distinct kind of reference (book, journal article, etc.) currently - * supported by Bibstro. Since this is only a demo app, there are not many - * reference kinds supported. - * @enum {string} - */ -DataStore.ReferenceKind = { - ARTICLE: 'article', - BOOK: 'book' -}; - - -/** - * The various sorts of data that Bibstro stores in {@code ScriptDb}. - * @enum {string} - * @private - */ -DataStore.Type_ = { - CONFIG: 'cfg', - REFERENCE: 'ref', - HIGHLIGHT: 'hgl', -}; - - -/** - * Loads configuration settings from the data store. - * @return {DataStore.Config} The current app configuration, or null if the app - * hasn't yet been configured. - */ -DataStore.prototype.getConfig = function() { - if (!this.configRecord_) { - return null; - } - var config = {bibStrategy: this.configRecord_['bibStrategy']}; - return config; -}; - - -/** - * Saves a new or updated app configuration to the data store. - * @param {!DataStore.Config} The new configuration model object to write out. - */ -DataStore.prototype.setConfig = function(config) { - var newConfigRecord = this.configRecord_ || {}; - newConfigRecord['type'] = DataStore.Type_.CONFIG; - newConfigRecord['bibStrategy'] = config.bibStrategy; - this.configRecord_ = this.scriptDb_.save(newConfigRecord); -}; - - -/** - * Loads data on all references in the bibliography, trying the cache first and - * the database backend second. The references are not guaranteed to be returned - * in any particular order since the sort order could depend on the current - * bibliography strategy (citation format). - * @return {!Array.} The (possibly empty) list of - * references. - */ -DataStore.prototype.loadReferences = function() { - // First, try and read references from the cache. To make sure we don't miss - // out on some references (e.g., because their cache entries have been garbage - // collected), we also read in the full list of reference IDs from the cache. - // If the list of reference IDs is missing, or if we can't find data for a - // reference, then we give up and do a ScriptDb query instead. - var cachedReferenceIds = - JSON.parse(this.cache_.get(DataStore.REFERENCE_LIST_CACHE_KEY)); - if (cachedReferenceIds) { - var cachedReferences = []; - - for (var i = 0; i < cachedReferenceIds.length; ++i) { - var cachedReference = - JSON.parse(this.cache_.get(DataStore.REFERENCE_CACHE_KEY_PREFIX + - cachedReferenceIds[i])); - if (!cachedReference) { - break; - } - cachedReferences.push(cachedReference); - } - - if (cachedReferences.length == cachedReferenceIds.length) { - return cachedReferences; // Successfully loaded all data from the cache! - } - } - - // We aren't certain that we could load all data from the cache, so we play it - // safe and query ScriptDb instead. - var referenceRecords = - this.queryRecords_({'type': DataStore.Type_.REFERENCE}); - - // Convert raw ScriptDbMap records into reference objects that we can safely - // pass outside this class. Also, as we go, insert reference model objects - // into the cache so the next read will (mostly likely) have to hit ScriptDb. - var references = []; - var referenceIds = []; - for (var i = 0; i < referenceRecords.length; ++i) { - var referenceRecord = referenceRecords[i]; - var reference = this.createModelForReferenceRecord_(referenceRecord); - - references.push(reference); - referenceIds.push(reference.id); - - this.cache_.put(DataStore.REFERENCE_CACHE_KEY_PREFIX + reference.id, - JSON.stringify(reference)); - } - - this.cache_.put(DataStore.REFERENCE_LIST_CACHE_KEY, - JSON.stringify(referenceIds)); - - return references; -}; - - -/** - * Loads a single reference from the cache (if possible) or the database - * backend. - * @param {string} id The {@code ScriptDb} identifier of the reference in - * question. - * @return {DataStore.Reference} The reference requested, or null if no - * reference with the specified ID could be found. - */ -DataStore.prototype.loadReference = function(id) { - // First, try to read from the cache. The cache is a consistent storage layer, - // so cached data is guaranteed not to be stale; however, it's entirely - // possible that given record has been completely purged from the cache. - var cachedReference = - JSON.parse(this.cache_.get(DataStore.REFERENCE_CACHE_KEY_PREFIX + id)); - if (cachedReference) { - return cachedReference; - } - - // If we didn't find data for this reference in the cache, try and look up the - // record with the specified ID in ScriptDb, noting such a record might not - // exist. - var referenceRecord = this.scriptDb_.load(id); - return referenceRecord ? - this.createModelForReferenceRecord_(referenceRecord) : null; -}; - - -/** - * Deletes a single reference from the cache and the database backend. - * @param {string} id The {@code ScriptDb} identifier of the reference in - * question. - */ -DataStore.prototype.deleteReference = function(id) { - this.scriptDb_.remove(this.scriptDb_.load(id)); - this.cache_.remove(DataStore.REFERENCE_CACHE_KEY_PREFIX + id); - - // If the reference was highlighted, we need to remove the reference ID from - // the highlight list in the database and in the cache. - this.highlightReference(id, true /* opt_unhighlight */); - - // If a full read of all references has been cached, then we also need to - // remove the reference ID from the cache's list of known references. - var cachedReferenceList = - JSON.parse(this.cache_.get(DataStore.REFERENCE_LIST_CACHE_KEY)); - if (cachedReferenceList) { - var cachedReferenceListIndex = cachedReferenceList.indexOf(id); - if (cachedReferenceListIndex != -1) { - cachedReferenceList.splice(cachedReferenceListIndex, 1); - this.cache_.put(DataStore.REFERENCE_LIST_CACHE_KEY, - JSON.stringify(cachedReferenceList)); - } - } -}; - - -/** - * Determines what references in the bibliography ought to have their inline - * citations highlighted in the active Google Docs document. - * @return {!Array.} The {@code ScriptDb} identifiers of all currently - * highlighted references. - */ -DataStore.prototype.getHighlights = function() { - // First, try to read from the cache. - var cachedHighlightList = - JSON.parse(this.cache_.get(DataStore.HIGHLIGHT_LIST_CACHE_KEY)); - if (cachedHighlightList) { - return cachedHighlightList; - } - - // Otherwise, fall through to ScriptDb. - var highlightListRecord = - this.queryRecord_({'type': DataStore.Type_.HIGHLIGHT}); - return highlightListRecord ? highlightListRecord['ids'] : []; -}; - - -/** - * Marks a reference as highlighted or not in the {@code ScriptDb} data store. - * @param {string} id The {@code ScriptDb} identifier of the reference to - * highlight or unhighlight. - * @param {boolean=} opt_unhighlight Whether to clear the highlight mark instead - * of setting it. - */ -DataStore.prototype.highlightReference = function(id, opt_unhighlight) { - var highlightListRecord = - this.queryRecord_({'type': DataStore.Type_.HIGHLIGHT}) || - {'type': DataStore.Type_.HIGHLIGHT, 'ids': []}; - if (opt_unhighlight) { - var highlightListIndex = highlightListRecord['ids'].indexOf(id); - if (highlightListIndex != -1) { - highlightListRecord['ids'].splice(highlightListIndex, 1); - } - } else { - if (highlightListRecord['ids'].indexOf(id) == -1) { - highlightListRecord['ids'].push(id); - } - } - this.scriptDb_.save(highlightListRecord); - this.cache_.put(DataStore.HIGHLIGHT_LIST_CACHE_KEY, - JSON.stringify(highlightListRecord['ids'])); -}; - - -/** - * Inserts a new reference into the cache and database backend, or updates an - * existing reference. - * @param {!DataStore.Reference} reference The reference model object to be - * inserted (if the {@code id} property is undefined) or updated (if the - * {@code id} property is defined) in the data store. - * @return {string} The identifier of the inserted or updated data store record. - */ -DataStore.prototype.insertOrUpdateReference = function(reference) { - if (!this.validateReferenceModel_(reference)) { - throw new Error('Invalid reference model: ' + JSON.stringify(reference)); - } - - // For insert, simply create a new, empty record object. For update, retrieve - // the existing reference record so we can overwrite it. (ScriptDb doesn't - // allow replacing an object by ID for some reason.) - var referenceRecord = {}; - if (reference.id) { - referenceRecord = this.scriptDb_.load(reference.id); - if (!referenceRecord) { - throw new Error('Could not find reference in data store for update: ' + - reference.id); - } - } - - // Remove all properties from the record. This is required when updating - // existing records, as we may be changing the record type, and so some fields - // might not need to be set anymore. (For example, if we're changing the type - // from BOOK to JOURNAL, the 'publisherCity' field should no longer be set.) - for (var key in referenceRecord) { - if (referenceRecord.hasOwnProperty(key)) { - delete referenceRecord[key]; - } - } - - // Make sure we can look the record up by type later of. - referenceRecord['type'] = DataStore.Type_.REFERENCE; - - // Copy fields from the model object to the underlying ScriptDb record. At the - // moment, this only removes the ID, but it might perform fancier conversions - // in the future. - for (var key in reference) { - if (key != 'id' && reference[key] !== undefined && - reference.hasOwnProperty(key)) { - referenceRecord[key] = reference[key]; - } - } - - // We use CacheService as a write-through cache on top of ScriptDb for - // performance and quota reasons, so do two writes: one to the underlying data - // store and one to the cache. - var id = this.scriptDb_.save(referenceRecord).getId(); - - if (!reference.id) { - reference.id = id; - - // The cached reference list may not exist (either because it expired or - // simply because it hasn't been computed yet). In that case, we don't - // need to update the list. However, if the list does exist, we need to - // append the new reference ID. - var cachedReferenceList = - JSON.parse(this.cache_.get(DataStore.REFERENCE_LIST_CACHE_KEY)); - if (cachedReferenceList) { - cachedReferenceList.push(id); - this.cache_.put(DataStore.REFERENCE_LIST_CACHE_KEY, - JSON.stringify(cachedReferenceList)); - } - } - - this.cache_.put(DataStore.REFERENCE_CACHE_KEY_PREFIX + id, - JSON.stringify(reference)); - - return id; -}; - - -/** - * Helper method that creates a generic JavaScript object from a - * {@code ScriptDbMap}. We preform this seemingly unnecessary conversion so all - * {@code ScriptDb} objects (which could otherwise be used to mutate the - * datastore directly, potentially causing an inconsistent cache state) are - * confined to the {@code DataStore} class. - * @param {!ScriptDb.ScriptDbMap} referenceRecord The raw {@code ScriptDb} - * record. - * @return {!DataStore.Reference} A plain old JavaScript object containing the - * same properties as the {@code ScriptDb}, and also having its {@code id} - * property set to the original record's {@code ScriptDb} identifier. - * @private - */ -DataStore.prototype.createModelForReferenceRecord_ = function(referenceRecord) { - // Copy the properties from the reference record to a new model object - // because we don't want to leak the underlying ScriptDbMap outside this - // class. All modification of raw ScriptDb objects should be done inside - // DataStore. - var reference = {id: referenceRecord.getId()}; - for (var key in referenceRecord) { - if (referenceRecord.hasOwnProperty(key)) { - reference[key] = referenceRecord[key]; - } - } - return reference; -}; - - -/** - * Determines whether or not the given object represents a valid reference - * model. This is the last line of against corrupt data ending up in the data - * store (e.g., because a mischievous user handcrafted a - * {@code google.script.run} call to bypass client-side form validation). - * @param {!DataStore.Reference} reference The reference object to validate. - * @return {boolean} True if the reference is valid and can be stored, false - * otherwise. - * @private - */ -DataStore.prototype.validateReferenceModel_ = function(reference) { - if (!(reference['authors'] instanceof Array)) { - return false; - } - var authors = reference['authors']; - for (var i = 0; i < authors.length; ++i) { - var author = authors[i]; - if (typeof author['lastName'] != 'string' || - typeof author['firstName'] != 'string') { - return false; - } - } - - return (reference['kind'] == DataStore.ReferenceKind.ARTICLE || - reference['kind'] == DataStore.ReferenceKind.BOOK) && - typeof reference['title'] == 'string' && - reference['title'] != '' && - typeof reference['publicationYear'] == 'number' && - reference['publicationYear'] > 0 && - (reference['publication'] === undefined || - typeof reference['publication'] == 'string') && - (reference['journalVolume'] === undefined || - typeof reference['journalVolume'] == 'string') && - (reference['journalIssue'] === undefined || - typeof reference['journalIssue'] == 'string') && - (reference['startPage'] === undefined || - typeof reference['startPage'] == 'number') && - (reference['endPage'] === undefined || - typeof reference['endPage'] == 'number') && - (reference['edition'] === undefined || - typeof reference['edition'] == 'string') && - (reference['volume'] === undefined || - typeof reference['volume'] == 'string') && - (reference['publisher'] === undefined || - typeof reference['publisher'] == 'string') && - (reference['publication'] === undefined || - typeof reference['publication'] == 'string'); -}; - - -/** - * Internal helper method to load a list of records from the database backend. - * @param {!Object} query The {@code ScriptDb} query to execute. This is a - * partially complete template object whose defined fields will be compared - * with the complete objects in the data store. Please see the - * {@code ScriptDb} documentation online for some examples. - * @return {!Array.} A list of all {@code ScriptDb} - * records matching the specified query. - * @private - */ -DataStore.prototype.queryRecords_ = function(query) { - var records = []; - var results = this.scriptDb_.query(query); - while (results.hasNext()) { - records.push(results.next()); - } - return records; -}; - - -/** - * Internal helper method that loads a single record from the database backend. - * This method will throw an exception if more than one record matches that - * query. - * @param {!Object} query The {@code ScriptDb} query to execute. This is a - * partially complete template object whose defined fields will be compared - * with the complete objects in the data store. Please see the - * {@code ScriptDb} documentation online for some examples. - * @return {ScriptDb.ScriptDbMap} The single {@code ScriptDb} record matching - * the specified query, or null if no records matched the query. - * @private - */ -DataStore.prototype.queryRecord_ = function(query) { - var results = this.scriptDb_.query(query); - if (results.getSize() > 1) { - throw new Error(Utilities.formatString( - 'Expected at most one record for query %s, got %d', - JSON.stringify(query), results.getSize())); - } - return results.hasNext() ? results.next() : null; -}; diff --git a/bibstro/documentmodel.gs b/bibstro/documentmodel.gs deleted file mode 100644 index cc527e9d1..000000000 --- a/bibstro/documentmodel.gs +++ /dev/null @@ -1,398 +0,0 @@ -// Copyright 2013 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * @fileOverview Google Apps Script and Google Docs demo: Bibstro. Document - * model class that abstracts away all direct interactions with Google Docs. - * This class is responsible for inserting inline citations at the cursor - * location, finding and updating the live list of references at the end of the - * document, and importing existing references from a new document into - * Bibstro's data store. - * @author Jonathan Rascher - * @author Saurabh Gupta - */ - - - -/** - * Constructs a new {@code DocumentModel} that wraps the specified Google Docs - * document. (Generally, this will be the active document.) - * @param {!DocumentApp.Document} doc - * @constructor - */ -var DocumentModel = function(doc) { - /** - * The document to be read and written by the Bibstro app. - * @type {!DocumentApp.Document} - * @private - */ - this.doc_ = doc; -}; - - -/** - * Inserts an inline citation at the document's current cursor location. - * @param {!BibStrategy} bibStrategy The current bibliography strategy (citation - * format) used to construct the inline citation text for insertion. - * @param {!DataStore.Reference} reference The reference to be cited. - * @param {number} startPage The first page included in this citation. - * @param {number} endPage The last page included in this citation. - * @param {boolean} firstMention Whether or not this reference has been cited - * earlier in the document. For certain citation formats (e.g., APA), the - * first citation associated with a given reference must contain information - * that should be omitted in later citations. - * @param {boolean} abbreviateCitation If true, the inserted inline citation - * will be abbreviated somehow. Exactly how depends on the specific citation - * format selected; however, in most formats the author(s) will be omitted - * and only the page number(s) will be included. - */ -DocumentModel.prototype.insertCitationAtCursor = function(bibStrategy, - reference, startPage, endPage, firstMention, abbreviateCitation) { - var cursor = this.doc_.getCursor(); - var insertedText; - - if (cursor) { - // Determine the text of the new inline citation to insert. - var citation = bibStrategy.getInlineCitationText(reference, startPage, - endPage, firstMention, abbreviateCitation); - - var surroundingText = cursor.getSurroundingText().getText(); - var surroundingTextOffset = cursor.getSurroundingTextOffset(); - - if (surroundingTextOffset > 0 && - surroundingText.charAt(surroundingTextOffset - 1) != ' ') { - // If the cursor follows a non-space character, insert a space and then - // the citation. - insertedText = cursor.insertText(' ' + citation); - } else { - // Otherwise, just insert the citation. - insertedText = cursor.insertText(citation); - } - } - - // Not all cursor locations allow text insertion. If we couldn't insert the - // inline citation, throw an appropriate error. - if (!insertedText) { - throw new Error("Cannot insert text at the cursor location"); - } -}; - - -/** - * @param {!BibStrategy} The bibliography strategy to be used to determine - * matching references according to the current citation format. - * @param {!Array} All possible references in the - * document's bibliography. - * @param {!DataStore.Reference} The particular reference we're looking for. - * @param {boolean=} opt_unhighlight If true, matching citation highlights will - * be cleared rather than set. - */ -DocumentModel.prototype.highlightMatchingCitations = function(bibStrategy, - references, reference, opt_unhighlight) { - // Determine the correct background to apply to matching inline citations. - var attrs = {}; - attrs[DocumentApp.Attribute.BACKGROUND_COLOR] = opt_unhighlight ? - '#ffffff' /* white */ : '#ffff00' /* yellow */; - - // Determine all possible authors to search for before inline citations. - var authors = []; - for (var i = 0; i < references.length; ++i) { - for (var j = 0; j < references[i].authors.length; ++j) { - authors.push(references[i].authors[j]); - } - } - - // For each matching inline citation, apply the background color changes. - this.forEachCitation_( - bibStrategy.getBibliographyTitleText(), - authors, - function(bodyText, citationIndex, citationStr, prevAuthor) { - if (bibStrategy.testInlineCitationMatch(reference, prevAuthor, - citationStr)) { - bodyText.setAttributes(citationIndex, - citationIndex + citationStr.length - 1, attrs); - } - }); -}; - - -/** - * Updates the content's of the document's bibliography with new references from - * the data store, inserting a new bibliography first if necessary. - * @param {!BibStrategy} bibStrategy The current bibliography strategy or - * citation format. This is used to identify the bibliography by looking for - * an appropriately titled header, and also to correctly format new - * bibliography entries. - * @param {!Array.} references All the references or - * sources current stored in the bibliography application's data store. - * @param {boolean=} opt_updateOnly If true, a new bibliography will not be - * appended to the document if no existing bibliography can be found. - * @param {!BibStrategy=} opt_prevBibStrategy The previous bibliography - * strategy, if the user saved configured changes. This is used to find and - * remove the old strategy's bibliography from the document before adding a - * new bibliography using the new strategy (citation format). - */ -DocumentModel.prototype.appendOrUpdateBibliography = function(bibStrategy, - references, opt_updateOnly, opt_prevBibStrategy) { - var prevBibStrategy = opt_prevBibStrategy || bibStrategy; - - // Look for an existing bibliography to update so we don't append a second - // set of bibliography elements in that case. - var titleParagraph = this.findBibliography_(prevBibStrategy); - - if (!titleParagraph) { - // If we didn't find an existing bibliography, either bail out without - // changing the document, or append a new paragraph to serve as the live - // bibliography in the future. - if (opt_updateOnly) { - return; - } - titleParagraph = this.doc_. - getBody(). - appendParagraph(bibStrategy.getBibliographyTitleText()). - setAlignment(DocumentApp.HorizontalAlignment.CENTER); - titleParagraph.insertPageBreak(0); - } else if (bibStrategy.getName() != prevBibStrategy.getName()) { - // Otherwise, if we found an existing live bibliography and we've changed - // citation formats, we might have to update the bibliography title. - titleParagraph.setText(bibStrategy.getBibliographyTitleText()); - titleParagraph.insertPageBreak(0); - } - - this.updateBibliography_(bibStrategy, references, titleParagraph); -}; - - -/** - * Attempts to import existing references by extracting them from the document's - * bibliography using regular expressions and text manipulation. Naturally, this - * is not guaranteed to successfully return data for all references. - * @param {!BibStrategy} bibStrategy The current bibliography strategy or - * citation format. This is used to identify the bibliography by looking for - * an appropriately titled header, and also to determine which citation - * format to use when extracting bibliography entries. - * @return {!Array.} A list of all references that could - * be successfully extracted. - */ -DocumentModel.prototype.extractReferences = function(bibStrategy) { - var referenceParagraph = this.findBibliography_(bibStrategy); - - // If we couldn't even find a reference list in the document, there's no way - // we can import anything. - if (!referenceParagraph) { - return []; - } - - // Otherwise, import whatever we can figure out. - var references = []; - - while (referenceParagraph = referenceParagraph.getNextSibling()) { - if (referenceParagraph.getType() != DocumentApp.ElementType.PARAGRAPH) { - break; - } - - var referenceTextElement = referenceParagraph.editAsText(); - var referenceText = referenceTextElement.getText(); - var referenceAttributeRuns = referenceTextElement.getTextAttributeIndices(); - - var tokens = []; - var prevToken = null; - for (var i = 0; i < referenceAttributeRuns.length; ++i) { - var start = referenceAttributeRuns[i]; - var end = (i + 1 < referenceAttributeRuns.length) ? - referenceAttributeRuns[i + 1] : referenceText.length; - - // Ignore all style expect transitions between italic and roman text. - var text = referenceText.substring(start, end); - var publicationTitle = referenceTextElement.isItalic(start); - - if (prevToken && prevToken.publicationTitle == publicationTitle) { - prevToken.text += text; - } else { - prevToken = {text: text, publicationTitle: publicationTitle}; - tokens.push(prevToken); - } - } - - var reference = bibStrategy.getReferenceForBibliographyEntry(tokens); - if (reference) { - references.push(reference); - } - } - - return references; -}; - - -/** - * Applies an operation to each inline citation found in the document. - * @param {string} bibliographyTitleText The title of the bibliography, which - * indicates when to stop searching for citations. - * @param {{lastName: string, firstName: string}} authors The list of all - * authors associated with bibliography references. - * @param {function(!DocumentApp.Text, number, string, ?string)} callback The - * function to be invoked for each inline citation discovered in the - * document. The callback's function parameters are, in order, a - * {@code Text}-element view of the document's body, the index of the - * current citation in the body, the text of the current citation (including - * surrounding parentheses), and the most recently mentioned author name - * before the citation (which will be null if no authors have been mentioned - * yet). - */ -DocumentModel.prototype.forEachCitation_ = function(bibliographyTitleText, - authors, callback) { - // Extract the plain text contents of the current document's body. - var bodyText = this.doc_.getBody().editAsText(); - var bodyStr = bodyText.getText(); - - // Determine the start of the bibliography in the document so we know where to - // stop looking for inline citations. - var bibliographyTitleIndex = - bodyStr.indexOf('\n' + bibliographyTitleText + '\n'); - if (bibliographyTitleIndex == -1) { - bibliographyTitleIndex = bodyStr.length; - } - - // Build up a regular expression matching the last name of any author - // currently associated with any bibliography reference. - var authorRegExp = RegExp(authors.map(function(author) { - return escapeRegExp(author.lastName); - }).join('|'), 'g'); - - // Determine the last names and locations of all author references in the - // document. - var authorMatches = []; - var authorMatch; - while (authorMatch = authorRegExp.exec(bodyStr)) { - authorMatches.push(authorMatch); - } - - // Keep track of the last author preceding the current citation. - var prevAuthorMatch = null; - var nextAuthorMatchNumber = 0; - - // Iterate over all parenthesized portions of the document, testing if they're - // inline citations. - var citationRegExp = RegExp('\\([^)]+\\)', 'g'); - var citationMatch; - - // If we've already advanced past the start of the document's bibliography, - // stop searching. This avoids giving false positives for parenthesized text - // in bibliography entries (e.g., years for MLA format or issues in APA - // format.) - while ((citationMatch = citationRegExp.exec(bodyStr)) && - citationMatch.index < bibliographyTitleIndex) { - // Find the last author preceding the current citation, if any. This will be - // used to guess the appropriate reference if the current citation doesn't - // directly contain the author's name. - while ((prevAuthorMatch == null || - prevAuthorMatch.index < citationMatch.index) && - nextAuthorMatchNumber < authorMatches.length && - authorMatches[nextAuthorMatchNumber].index < citationMatch.index) { - prevAuthorMatch = authorMatches[nextAuthorMatchNumber++]; - } - - // Invoke the callback function for the current inline citation. - callback(bodyText, citationMatch.index, citationMatch[0], - prevAuthorMatch ? prevAuthorMatch[0] : null); - } -}; - - -/** - * Finds the bibliography at the end of the current document. - * @param {!BibStrategy} bibStrategy The current bibliography strategy or - * citation format. This is used to identify the bibliography by looking for - * an appropriately titled header like "Works Cited". - * @return {DocumentApp.Paragraph} The paragraph containing the bibliography - * title text. The actual bibliography entries will follow this title - * paragraph. The return value will be null if no bibliography is found in - * the document at this time. - */ -DocumentModel.prototype.findBibliography_ = function(bibStrategy) { - var titleParagraph = null; - - var body = this.doc_.getBody(); - var searchResult = null; - while (searchResult = body.findElement(DocumentApp.ElementType.PARAGRAPH, - searchResult)) { - var paragraph = searchResult.getElement().asParagraph(); - if (paragraph.editAsText().getText() == - bibStrategy.getBibliographyTitleText()) { - titleParagraph = paragraph; - break; - } - } - - return titleParagraph; -}; - - -/** - * Updates the content's of the document's bibliography with new references from - * the app's data store. - * @param {!BibStrategy} bibStrategy The current bibliography strategy or - * citation format. This is used to identify the bibliography by looking for - * an appropriately titled header, and also to correctly format new - * bibliography entries. - * @param {!Array.} references All the references or - * sources current stored in the bibliography application's data store. - * @param {!DocumentApp.Paragraph} titleParagraph The bibliography's title - * paragraph found, as determined by {@code findBibliography_}. - * @private - */ -DocumentModel.prototype.updateBibliography_ = function(bibStrategy, references, - titleParagraph) { - // Remove all references from the document's bibliography. - var referenceParagraph; - while (referenceParagraph = titleParagraph.getNextSibling()) { - if (referenceParagraph.getType() != DocumentApp.ElementType.PARAGRAPH) { - break; - } - // While there are still bibliography entry paragraphs after the bibliograpy - // title, remove the next entry's content and merge the title into the (now - // empty) paragraph. We also need to reset the bibliography title's style - // attributes post-merge. - referenceParagraph. - clear(). - setAlignment(DocumentApp.HorizontalAlignment.CENTER). - setIndentStart(0). - merge(); - } - - // Add the latest reference data to the document's bibliography. - references.sort(bibStrategy.getReferenceComparator()); - - var bibParent = titleParagraph.getParent(); - var nextIndex = bibParent.getChildIndex(titleParagraph); - for (var i = 0; i < references.length; ++i) { - // For each bibliography entry, append a new paragraph with a half-inch - // hanging indent. - referenceParagraph = bibParent.insertParagraph(++nextIndex, ''). - setIndentStart(36 /* pt, or 0.5 in */). - setIndentFirstLine(0); - - // Next, get the formatted bibliography entry as an array of - // BibStrategy.BibEntryToken objects; then append the tokens' contents to - // the current reference paragraph. Each token can have a different style; - // for example, tokens representing book or journal titles will be - // italicized when added to the document. - var tokens = bibStrategy.getBibliographyEntry(references[i]); - for (var j = 0; j < tokens.length; ++j) { - var token = tokens[j]; - var tokenText = referenceParagraph.appendText(token.text); - tokenText.setItalic(token.publicationTitle); - } - } -}; diff --git a/bibstro/events.gs b/bibstro/events.gs deleted file mode 100644 index 48df2b4ae..000000000 --- a/bibstro/events.gs +++ /dev/null @@ -1,371 +0,0 @@ -// Copyright 2013 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * @fileOverview Google Apps Script and Google Docs demo: Bibstro. Event - * dispatcher. The scripting infrastructure calls function in this file to - * handle events like "document opened" or "menu item clicked", and this file - * invokes the appropriate methods on the {@code Controller} class. - * @author Jonathan Rascher - * @author Saurabh Gupta - */ - - -// This application loosely follows the model-view-controller pattern. Requests -// from the client are dispatched into this file (events.gs) and funnelled into -// the Controller class. At that point, the following model classes into play: -// -// * DataStore: Stores bibliography references (called "sources" in the user -// interface) in the ScriptDb NoSQL database with a write-through cache based on -// CacheService. -// -// * BibStrategy: Represents the current bibliography strategy or citation -// format. This class encapsulates all logic for converting references between -// format-agnostic DataStore model objects and format-specific text entries. -// -// * DocumentModel: Encapsulates all code that directly interacts with the -// current Google Docs document. This class is responsible for maintaining the -// bibliography at the end of the document, adding and highlighting inline -// citations, etc. -// -// Some additional, shared functions live in common.gs. Finally, bibliography -// data and configuration are displayed and edited using the following view -// HtmlService templates: -// -// * configdialog.html: Configuration settings live here. -// -// * managereferencessidebar.html: This shows the user their current references -// (bibliography sources) according to the selected citation format, allows the -// user to add and edit references, etc. -// -// * referencedialog.html: Contains form fields and validation logic needed for -// adding new references and updating existing references. -// -// * citationdialog.html: This dialog lets the user insert an inline citation at -// the document's current cursor location. - - -/** - * Handles application initialization when Google Docs is opened. - */ -function onOpen() { - // Add some menu items to the Google Docs word processor. These menu items - // call into the Apps Script handlers listed below when clicked. - DocumentApp.getUi().createMenu('Bibstro'). - addItem('Add inline citation...', addCitationMenuItem_onClick.name). - addItem('Refresh document bibliography', - appendLiveBibMenuItem_onClick.name). - addSeparator(). - addItem('Add new source...', addReferenceMenuItem_onClick.name). - addItem('Manage sources', manageReferencesMenuItem_onClick.name). - addItem('Import existing bibliography', - importReferencesMenuItem_onClick.name). - addSeparator(). - addSubMenu(DocumentApp.getUi().createMenu('Settings'). - addItem('Configure', configureMenuItem_onClick.name). - addItem('Reset data store [debug]...', - resetDataStoreMenuItem_onClick.name)). - addItem('About Bibstro', aboutMenuItem_onClick.name). - addToUi(); -} - - -/** - * Inserts an inline citation into the document at the current cursor location - * using the bibliography reference and page numbers specified by the user. - * @param {!Object.} Populated form fields from {@code - * citationdialog.html}. - */ -function onInsertCitation(e) { - new Controller().insertCitation(e['referenceId'], Number(e['startPage']), - Number(e['endPage']), !!e['firstMention'], !!e['abbreviateCitation']); -} - - -/** - * Shows a dialog allowing a user to insert a new reference into the - * bibliography or to edit an existing reference. - * @param {string=} opt_id If present, the {@code ScriptDb} identifier of the - * reference whose data should be fetched and entered into the reference - * dialog form for editing. If absent, then the form fields shall be left - * blank. - */ -function onReferenceShow(opt_id) { - new Controller().showReferenceDialog(opt_id); -} - - -/** - * Removes the reference with the specified ID from the underlying data store - * when the user clicks the reference sidebar's "remove" button. - * @param {string} id The {@code ScriptDb} identifier of the reference record to - * be removed. - */ -function onReferenceRemove(id) { - new Controller().deleteReference(id); -} - - -/** - * Toggles highlighting of inline citations in the document associated with the - * specified reference. - * @param {{id: string, unhighlight: boolean}} e An object identifying the - * reference in question and whether highlights of associated citations - * should be set or cleared. - */ -function onReferenceHighlight(e) { - new Controller().highlightReference(e['id'], e['unhighlight']); -} - - -/** - * Processes a form submission from the reference dialog, saving data entered by - * the user into the bibliography backend's {@code ScriptDb} data store. - * @param {!Object.} e Form field values from - * {@code referencedialog.html}. - */ -function onReferenceSave(e) { - var reference = {}; - if (e['id']) { - reference.id = e['id']; - } - reference.kind = e['kind']; - - // Author fields have names like 'lastName[0]', 'firstName[0]', 'lastName[1]', - // 'firstName[1]', etc. We need to convert these to an array of author objects - // having keys named 'lastName' and 'firstName'. (If we named all our form - // fields just 'lastName' and 'firstName', Apps Script would provide us with - // two parallel arrays for free, but unfortunately the jQuery Validate plugin - // does not support form fields with duplicate names.) - var lastNames = extractArrayFields(e, 'lastName'); - var firstNames = extractArrayFields(e, 'firstName'); - - reference.authors = []; - for (var i = 0; i < lastNames.length; ++i) { - // The arrays passed up from the client can have gaps in them if author rows - // are removed from the form before it is submitted. - if (lastNames[i] != undefined && firstNames[i] != undefined) { - reference.authors.push({ - lastName: lastNames[i], - firstName: firstNames[i] - }); - } - } - - reference.title = e['title']; - reference.publicationYear = Number(e['publicationYear']); - - // We don't do any real server-side validation here; the client-side - // validation code should prevent non-malicious/mischievous users from causing - // trouble, and DataStore itself contains code to prevent saving objects that - // violate the underlying data model. - switch (e.kind) { - case DataStore.ReferenceKind.ARTICLE: - reference.publication = e['publication']; - if (e['journalVolume']) { - reference.journalVolume = e['journalVolume']; - } - if (e['journalIssue']) { - reference.journalIssue = e['journalIssue']; - } - reference.startPage = Number(e['startPage']); - reference.endPage = Number(e['endPage']); - break; - - case DataStore.ReferenceKind.BOOK: - if (e['edition']) { - reference.edition = e['edition']; - } - if (e['volume']) { - reference.volume = e['volume']; - } - reference.publisher = e['publisher']; - reference.publisherCity = e['publisherCity']; - break; - } - - new Controller().saveReference(reference); -} - - -/** - * Shows a configuration dialog ({@code configdialog.html}) allowing the user to - * set the current citation format. - */ -function onAppendLiveBibliography() { - new Controller().appendLiveBibliography(); -} - - -/** - * Persists possibly updated configuration options back to the {@code ScriptDb} - * data store. - * @param {!Object.} e Form field values from - * {@code configdialog.html}. - */ -function onConfigSave(e) { - var controller = new Controller(); - var prevBibStrategy = controller.getBibStrategy(); - - controller.saveConfig(e); - - if (e['initialSetup']) { - if (e['importReferences']) { - controller.importReferencesFromDocument(); - } - } else { - controller.refreshLiveBibliography(prevBibStrategy || undefined); - } -} - - -/** - * Loads the full list of references from the data store, producing properly - * formatted HTML bibliography entries for each. - * @return {{bibStrategy: string, - * references: !Array.<{id: string, html: string}>, - * highlights: !Array.}} - */ -function loadReferenceData() { - return new Controller().loadReferenceData(); -} - - -/** - * Shows {@code citationdialog.html} in response to a menu item click. - */ -function addCitationMenuItem_onClick() { - var controller = new Controller(); - if (controller.showInitialSetupDialog()) { - return; - } - controller.showCitationDialog(); -} - - -/** - * Appends a live-updating, formatted bibliography to the Google Docs document - * or forces an update of the existing bibliography if one already exists. - */ -function appendLiveBibMenuItem_onClick() { - var controller = new Controller(); - if (controller.showInitialSetupDialog()) { - return; - } - controller.appendLiveBibliography(); -} - - -/** - * Shows {@code referencedialog.html} in response to a menu item click. - */ -function addReferenceMenuItem_onClick() { - var controller = new Controller(); - if (controller.showInitialSetupDialog()) { - return; - } - controller.showReferenceDialog(); -} - - -/** - * Shows {@code managereferencessidebar.html} in response to a menu item click. - */ -function manageReferencesMenuItem_onClick() { - var controller = new Controller(); - if (controller.showInitialSetupDialog()) { - return; - } - controller.showManageReferenceSidebar(); -} - - -/** - * Attempts to import reference from an existing document bibliography into the - * document's data store. - */ -function importReferencesMenuItem_onClick() { - var controller = new Controller(); - if (controller.showInitialSetupDialog()) { - return; - } - controller.importReferencesFromDocument(); -} - - -/** - * Shows {@code configdialog.html} in response to a menu item click. - */ -function configureMenuItem_onClick() { - var controller = new Controller(); - if (controller.showInitialSetupDialog()) { - return; - } - controller.showConfigDialog(); -} - - -/** - * Irreversibly removes all objects from {@code ScriptDb}. This is intended to - * allow reseting the data store for debugging/development purposes. Since this - * operation could lead to unwanted data loss, we ask the user for confirmation - * before deleting anything. - */ -function resetDataStoreMenuItem_onClick() { - var ui = DocumentApp.getUi(); - var confirmation = ui.alert( - 'Google Apps Script and Google Docs demo: Bibstro', - 'This will erase all sources and configuration! Are you sure?', - ui.ButtonSet.YES_NO); - - if (confirmation == ui.Button.YES) { - var scriptDb = ScriptDb.getMyDb(); - var cache = CacheService.getPublicCache(); - - var results = scriptDb.query({}); - while (results.hasNext()) { - var result = results.next(); - - // Remove record from the data store and the write-through cache. - scriptDb.remove(result); - cache.remove(DataStore.REFERENCE_CACHE_KEY_PREFIX + result.getId()); - } - - // Clear out the write-through cache's list of active reference IDs. - cache.remove(DataStore.REFERENCE_LIST_CACHE_KEY); - - // Clear out the write-through cache's list of highlighted reference IDs. - cache.remove(DataStore.HIGHLIGHT_LIST_CACHE_KEY); - - ui.alert( - 'Google Apps Script and Google Docs demo: Bibstro', - 'All bibliography data has been erased. Please reload the document.', - ui.ButtonSet.OK); - } -} - - -/** - * Shows a simple "about" dialog to the user. - */ -function aboutMenuItem_onClick() { - DocumentApp.getUi().alert( - 'Google Apps Script and Google Docs demo: Bibstro', - 'Bibstro is a simple bibliography manager and citation formatting tool ' + - 'for Google Docs. Developed for Google I/O 2013, Bibstro is built ' + - 'in Google Apps Script. Bibstro is only a demo, but it attempts to ' + - 'show how developers can solve real-world problems using the Apps ' + - 'Script platform.', - DocumentApp.getUi().ButtonSet.OK); -} diff --git a/bibstro/managereferencessidebar.html b/bibstro/managereferencessidebar.html deleted file mode 100644 index 8dd772378..000000000 --- a/bibstro/managereferencessidebar.html +++ /dev/null @@ -1,89 +0,0 @@ - -Google Apps Script and Google Docs demo: Bibstro - - - - -

- Loading... -
-
    -
    - -
    - - -
    - - diff --git a/bibstro/managereferencessidebar_js.html b/bibstro/managereferencessidebar_js.html deleted file mode 100644 index 2d7c01c85..000000000 --- a/bibstro/managereferencessidebar_js.html +++ /dev/null @@ -1,112 +0,0 @@ - - diff --git a/bibstro/referencedialog.html b/bibstro/referencedialog.html deleted file mode 100644 index 31f6cdc37..000000000 --- a/bibstro/referencedialog.html +++ /dev/null @@ -1,182 +0,0 @@ - -Google Apps Script and Google Docs demo: Bibstro - - - - -
    -
    - Add a newEdit an existing source - - - -
    - -
    - -
    -
    - - -
    - -
    - - - -
    -
    - - -
    -
    - -
    -
    - -
    - -
    - -
    -
    - -
    style="display: none;"> - -
    - -
    -
    - -
    style="display: none;"> - -
    - - ed., Vol. - -
    -
    - -
    style="display: none;"> - -
    - - . - -
    -
    - -
    - -
    - -
    -
    - -
    style="display: none;"> - -
    - -
    -
    - -
    style="display: none;"> - -
    - -
    -
    - -
    style="display: none;"> - -
    - - – - -
    -
    -
    - -
    - - -
    -
    - - diff --git a/bibstro/referencedialog_js.html b/bibstro/referencedialog_js.html deleted file mode 100644 index 70b1717bb..000000000 --- a/bibstro/referencedialog_js.html +++ /dev/null @@ -1,120 +0,0 @@ - - - - diff --git a/calendar/vacation_calendar.gs b/calendar/vacation_calendar.gs new file mode 100644 index 000000000..53f55dd57 --- /dev/null +++ b/calendar/vacation_calendar.gs @@ -0,0 +1,170 @@ +// [START calendar] +var TEAM_CALENDAR_ID = 'ENTER_TEAM_CALENDAR_ID_HERE'; +var KEYWORDS = ['vacation', 'ooo', 'out of office']; +var MONTHS_IN_ADVANCE = 3; + +// The maximum script run time under Apps Script Pro is 30 minutes; this setting +// will be used to report when the script is about to reach that limit. +var MAX_PRO_RUNTIME_MS = 29 * 60 * 1000; + +/** + * Look through the domain users' public calendars and add any + * 'vacation' or 'out of office' events to the team calendar. + */ +function syncTeamVacationCalendar() { + // Define the calendar event date range to search. + var today = new Date(); + var futureDate = new Date(); + futureDate.setMonth(futureDate.getMonth() + MONTHS_IN_ADVANCE); + var lastRun = PropertiesService.getScriptProperties().getProperty('lastRun'); + lastRun = lastRun ? new Date(lastRun) : null; + + // Get the list of users in the domain. + var users = getDomainUsers(); + + // For each user, find events having one or more of the keywords in the event + // summary in the specified date range. Import each of those to the team + // calendar. + var count = 0; + var timeout = false; + for (var i = 0; i < users.length; i++) { + if (isTimeUp(today, new Date())) { + timeout = true; + break; + } + var user = users[i]; + var username = user.split('@')[0]; + KEYWORDS.forEach(function(keyword) { + var events = findEvents(user, keyword, today, futureDate, lastRun); + events.forEach(function(event) { + event.summary = '[' + username + '] ' + event.summary; + event.organizer = { + id: TEAM_CALENDAR_ID + }; + event.attendees = []; + Logger.log('Importing: %s', event.summary); + try { + Calendar.Events.import(event, TEAM_CALENDAR_ID); + count++; + } catch (e) { + Logger.log( + 'Error attempting to import event: %s. Skipping.', e.toString()); + } + }); + }); + } + PropertiesService.getScriptProperties().setProperty('lastRun', today); + Logger.log('Imported ' + count + ' events'); + if (timeout) { + Logger.log('Execution time about to hit quota limit; execution stopped.'); + } + var executionTime = ((new Date()).getTime() - today.getTime()) / 1000.0; + Logger.log('Total execution time (s) : ' + executionTime);; +} + +/** + * In a given user's calendar, look for occurrences of the given keyword + * in events within the specified date range and return any such events + * found. + * @param user the user's primary email String. + * @param keyword the keyword String to look for. + * @param start the starting Date of the range to examine. + * @param end the ending Date of the range to examine. + * @param opt_since a Date indicating the last time this script was run. + * @return an array of calendar event Objects. + */ +function findEvents(user, keyword, start, end, opt_since) { + var params = { + q: keyword, + timeMin: formatDate(start), + timeMax: formatDate(end), + showDeleted: true + }; + if (opt_since) { + // This prevents the script from examining events that have not been + // modified since the specified date (that is, the last time the + // script was run). + params['updatedMin'] = formatDate(opt_since); + } + var results = []; + try { + var response = Calendar.Events.list(user, params); + results = response.items.filter(function(item) { + // Filter out events where the keyword did not appear in the summary + // (that is, the keyword appeared in a different field, and are thus + // is not likely to be relevant). + if (item.summary.toLowerCase().indexOf(keyword) < 0) { + return false; + } + // If the event was created by someone other than the user, only include + // it if the user has marked it as 'accepted'. + if (item.organizer && item.organizer.email != user) { + if (!item.attendees) { + return false; + } + var matching = item.attendees.filter(function(attendee) { + return attendee.self; + }); + return matching.length > 0 && matching[0].status == 'accepted'; + } + return true; + }); + } catch (e) { + Logger.log('Error retriving events for %s, %s: %s; skipping', + user, keyword, e.toString()); + results = []; + } + return results; +} + +/** + * Return a list of the primary emails of users in this domain. + * @return array of user email Strings. + */ +function getDomainUsers() { + var pageToken, page; + var userEmails = []; + do { + page = AdminDirectory.Users.list({ + customer: 'my_customer', + orderBy: 'givenName', + maxResults: 100, + pageToken: pageToken, + viewType: 'domain_public' + }); + var users = page.users; + if (users) { + userEmails = userEmails.concat(users.map(function(user) { + return user.primaryEmail; + })); + } else { + Logger.log('No domain users found.'); + } + pageToken = page.nextPageToken; + } while (pageToken); + return userEmails; +} + +/** + * Return an RFC3339 formated date String corresponding to the given + * Date object. + * @param date a Date. + * @return a formatted date string. + */ +function formatDate(date) { + return Utilities.formatDate(date, 'UTC', 'yyyy-MM-dd\'T\'HH:mm:ssZ'); +} + +/** + * Compares two Date objects and returns true if the difference + * between them is more than the maximum specified run time. + * + * @param start the first Date object. + * @param now the (later) Date object. + * @returns true if the time difference is greater than + * MAX_PROP_RUNTIME_MS (in milliseconds). + */ +function isTimeUp(start, now) { + return now.getTime() - start.getTime() > MAX_PRO_RUNTIME_MS; +} +// [END calendar] diff --git a/cursor_inspector/Code.gs b/docs/cursor_inspector/Code.gs similarity index 100% rename from cursor_inspector/Code.gs rename to docs/cursor_inspector/Code.gs diff --git a/cursor_inspector/README.md b/docs/cursor_inspector/README.md similarity index 100% rename from cursor_inspector/README.md rename to docs/cursor_inspector/README.md diff --git a/cursor_inspector/Sidebar.css.html b/docs/cursor_inspector/Sidebar.css.html similarity index 100% rename from cursor_inspector/Sidebar.css.html rename to docs/cursor_inspector/Sidebar.css.html diff --git a/cursor_inspector/Sidebar.html b/docs/cursor_inspector/Sidebar.html similarity index 100% rename from cursor_inspector/Sidebar.html rename to docs/cursor_inspector/Sidebar.html diff --git a/cursor_inspector/Sidebar.js.html b/docs/cursor_inspector/Sidebar.js.html similarity index 100% rename from cursor_inspector/Sidebar.js.html rename to docs/cursor_inspector/Sidebar.js.html diff --git a/translate/Code.gs b/docs/translate/Code.gs similarity index 100% rename from translate/Code.gs rename to docs/translate/Code.gs diff --git a/translate/README.md b/docs/translate/README.md similarity index 100% rename from translate/README.md rename to docs/translate/README.md diff --git a/translate/Sidebar.css.html b/docs/translate/Sidebar.css.html similarity index 100% rename from translate/Sidebar.css.html rename to docs/translate/Sidebar.css.html diff --git a/translate/Sidebar.html b/docs/translate/Sidebar.html similarity index 100% rename from translate/Sidebar.html rename to docs/translate/Sidebar.html diff --git a/translate/Sidebar.js.html b/docs/translate/Sidebar.js.html similarity index 100% rename from translate/Sidebar.js.html rename to docs/translate/Sidebar.js.html diff --git a/gmail/mailmerge.gs b/gmail/mailmerge.gs new file mode 100644 index 000000000..514cc68a0 --- /dev/null +++ b/gmail/mailmerge.gs @@ -0,0 +1,67 @@ +// [START mail_merge] +function sendEmails() { + var ss = SpreadsheetApp.getActiveSpreadsheet(); + var dataSheet = ss.getSheets()[0]; + // [START dataRange] + var dataRange = dataSheet.getRange(2, 1, dataSheet.getMaxRows() - 1, 4); + // [END dataRange] + + // [START template] + var templateSheet = ss.getSheets()[1]; + var emailTemplate = templateSheet.getRange("A1").getValue(); + // [END template] + + // [START objects] + // Create one JavaScript object per row of data. + var objects = getRowsData(dataSheet, dataRange); + // [END objects] + + // For every row object, create a personalized email from a template and send + // it to the appropriate person. + for (var i = 0; i < objects.length; ++i) { + // Get a row object + var rowData = objects[i]; + + // [START emailText] + // Generate a personalized email. + // Given a template string, replace markers (for instance ${"First Name"}) with + // the corresponding value in a row object (for instance rowData.firstName). + var emailText = fillInTemplateFromObject(emailTemplate, rowData); + // [END emailText] + var emailSubject = "Tutorial: Simple Mail Merge"; + + // [START sendEmail] + MailApp.sendEmail(rowData.emailAddress, emailSubject, emailText); + // [END sendEmail] + } +} + +// Replaces markers in a template string with values define in a JavaScript data object. +// Arguments: +// - template: string containing markers, for instance ${"Column name"} +// - data: JavaScript object with values to that will replace markers. For instance +// data.columnName will replace marker ${"Column name"} +// Returns a string without markers. If no data is found to replace a marker, it is +// simply removed. +function fillInTemplateFromObject(template, data) { + var email = template; + // [START templateVars] + // Search for all the variables to be replaced, for instance ${"Column name"} + var templateVars = template.match(/\$\{\"[^\"]+\"\}/g); + // [END templateVars] + + // Replace variables from the template with the actual values from the data object. + // If no value is available, replace with the empty string. + for (var i = 0; i < templateVars.length; ++i) { + // normalizeHeader ignores ${"} so we can call it directly here. + // [START variableData] + var variableData = data[normalizeHeader(templateVars[i])]; + // [END variableData] + // [START replace] + email = email.replace(templateVars[i], variableData || ""); + // [END replace] + } + + return email; +} +// [END mail_merge] diff --git a/gmail/sending_emails.gs b/gmail/sending_emails.gs new file mode 100644 index 000000000..d49cfdd6d --- /dev/null +++ b/gmail/sending_emails.gs @@ -0,0 +1,47 @@ +// [START sendEmails] +function sendEmails() { + var sheet = SpreadsheetApp.getActiveSheet(); + var startRow = 2; // First row of data to process + var numRows = 2; // Number of rows to process + // Fetch the range of cells A2:B3 + var dataRange = sheet.getRange(startRow, 1, numRows, 2) + // Fetch values for each row in the Range. + var data = dataRange.getValues(); + for (i in data) { + var row = data[i]; + var emailAddress = row[0]; // First column + var message = row[1]; // Second column + var subject = "Sending emails from a Spreadsheet"; + MailApp.sendEmail(emailAddress, subject, message); + } +} +// [END sendEmails] + +// [START sendEmails2] +// This constant is written in column C for rows for which an email +// has been sent successfully. +var EMAIL_SENT = "EMAIL_SENT"; + +function sendEmails2() { + var sheet = SpreadsheetApp.getActiveSheet(); + var startRow = 2; // First row of data to process + var numRows = 2; // Number of rows to process + // Fetch the range of cells A2:B3 + var dataRange = sheet.getRange(startRow, 1, numRows, 3) + // Fetch values for each row in the Range. + var data = dataRange.getValues(); + for (var i = 0; i < data.length; ++i) { + var row = data[i]; + var emailAddress = row[0]; // First column + var message = row[1]; // Second column + var emailSent = row[2]; // Third column + if (emailSent != EMAIL_SENT) { // Prevents sending duplicates + var subject = "Sending emails from a Spreadsheet"; + MailApp.sendEmail(emailAddress, subject, message); + sheet.getRange(startRow + i, 3).setValue(EMAIL_SENT); + // Make sure the cell is updated right away in case the script is interrupted + SpreadsheetApp.flush(); + } + } +} +// [END sendEmails2] diff --git a/import_export_development/.gitignore b/import_export_development/.gitignore deleted file mode 100644 index cc159b500..000000000 --- a/import_export_development/.gitignore +++ /dev/null @@ -1,22 +0,0 @@ -# blog sample files and dirs # -############################## -#This is an example of a file pattern for your personal dev files for a /dev deployment -#Commented here so that a sample file can be included in the blog post repository -#TODO: (sample user) Uncomment to prevent your personal override/test files from being added to git. -#/**/debug.local.* -#TODO: (sample user) Uncomment to prevent the upload tool's staging folders from being added to git. -#build - -# npm files and dirs # -###################### -node_modules - -# OS generated files # -###################### -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db \ No newline at end of file diff --git a/import_export_development/README.md b/import_export_development/README.md deleted file mode 100644 index 7b11dd96d..000000000 --- a/import_export_development/README.md +++ /dev/null @@ -1,57 +0,0 @@ -This sample shows how to use a task runner and command line tools to develop Apps Script projects. There are quite a few setup steps, but they do mimic some typical one-time tasks you'd be doing in any given development project. - -### Getting started - -This sample uses the community project [node-google-apps-script](https://www.npmjs.com/package/node-google-apps-script). Before working with the sample, perform the following steps: - -1. run `npm install -g node-google-apps-script` to install it globally. This will allow you to run a few of the steps below from the command line. -2. Go through the [configuration and authorization steps](https://www.npmjs.com/package/node-google-apps-script). -2. Run `npm install -g gulp` if you do not already have the [gulp](http://gulpjs.com/) task runner installed globally. -3. Clone this repository, `cd google-apps-script-samples/import_export_development`, and run `npm install`. This will set up all of the local dependencies for the project. - -### Demonstrating the developer flow -To get the initial setup working for you, perform the following steps. In a real project, each developer would perform these steps, so that they can do development in isolation from other developers: - -1. Create a [new Google Spreadsheet](https://docs.google.com/spreadsheets/create), and copy the ID of the file. The file ID is found in the URL to the spreadsheet: - docs.google.com/spreadsheets/d/***DRIVE_FILE_ID***/edit#gid=123 -2. Open the file *src/environments/dev/debug.local.config.js*, and replace DRIVE_FILE_ID with the ID that you copied. -3. Create a new standalone [Google Apps Script](https://script.google.com) project, and copy the ID of the script. The file ID is found in the URL to the script project: - script.google.com/a/macros/google.com/d/***DRIVE_FILE_ID***/edit -4. Perform the following commands: - - ``` - mkdir build - cd build - mkdir dev - cd dev - gapps init *DRIVE_FILE_ID* - cd ../.. - gulp upload-latest --env dev - ``` - -5. Refresh your Apps Script project. You should now see a copy of some of the files from the local source location. - -You can run the code either by publishing it as a web app, or testing it as an addon (It just counts the number of sheets in your spreadsheet). Because the code finds a valid Spreadsheet ID in the configuration, it uses that instead of `SpreadsheetApp.getActiveSpreadsheet`. - - -### Deploying for testing -This second phase mimics the setup to isolate testing from development. This would typically be a single Apps Script file for the project, and would have some different code added to enable testing scenarios. In a real project, a testing coordinator or other similar role would perform these steps: - -1. Create a [new Google Spreadsheet](https://docs.google.com/spreadsheets/create), and copy the ID of the file. Add 4 additional sheets to the file (the test looks for 5 sheets). -2. Open the file *src/environments/tst/a.myproj.tst.config.js*, and replace DRIVE_FILE_ID with the ID that you copied. -3. Perform the same steps #3 and #4 from above, replacing `dev` with `tst`. -4. Refresh the Apps Script project, open the file `a.myproj.tests.server.main.js`, and then run the function `runAllTests`. Once the run completes, open the log view to see the output. - -Once again, this example uses the configured spreadsheet to allow unit testing without having to change code for testing in the primary codebase. - - -### Deploying for production -This final phase mimics the setup to prepare for a production deployment. This would typically be a single Apps Script file for the project, with the "edit" permission granted to a very limited number of users. In a real project, a deployment coordinator or other similar role would perform these steps: - - -1. Perform the same steps #3 and #4 from above, replacing `dev` with `prd`. -2. Refresh the Apps Script project, open the project properties (File --> Project properties), and click on the "Scopes" tab. You will see that the spreadsheet scope now only allows the current attached doc (`spreadsheets.currentonly`), which is what we would want for production. -3. You can also create a new spreadsheet, set up for add-on testing, and the code will run successfully. - -For production, the configuration file does *not* have a specific spreadsheet, and has the annotation @onlycurrent doc. That will ensure that only Spreadsheets that the code has been linked via the Add-on can be accessed. This limiting of scope is a good practice to provide more confidence to your users that you are only working on sheets that they have linked to your add-on. - diff --git a/import_export_development/gulpfile.js b/import_export_development/gulpfile.js deleted file mode 100644 index c57052205..000000000 --- a/import_export_development/gulpfile.js +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -'use strict'; - -var gulp = require('gulp'); -var jshint = require('gulp-jshint'); -var shell = require('gulp-shell'); -var minimist = require('minimist'); -var rename = require('gulp-rename'); -var debug = require('gulp-debug'); -var del = require('del'); - -// minimist structure and defaults for this task configuration -var knownOptions = { - string: ['env'], - 'default': { - env: 'dev' - } -}; -var options = minimist(process.argv.slice(2), knownOptions); - -// The root working directory where code is edited -var srcRoot = 'src'; -// The root staging folder for gapps configurations -var dstRoot = 'build/' + options.env + '/src'; - -// Runs the copy-latest task, then calls gapps upload in the correct -// configuration directory based on the target environment -gulp.task('upload-latest', ['copy-latest'], shell.task(['gapps upload'], - {cwd: 'build/' + options.env})); - -// Copies all files based on the current target environment. -// Completion of "clean-deployment" is a prerequisite for starting the copy -// process. -gulp.task('copy-latest', ['clean-deployment'], function() { - copyEnvironmentSpecific(); - copyServerCode(); - copyClientCode(); -}); - -// Copies all .js that will be run by the Apps Script runtime -function copyServerCode() { - return gulp.src([ - srcRoot + '/server/*.js', - srcRoot + '/libs/*.js', - srcRoot + '/ui/*.server.js']) - .pipe(gulp.dest(dstRoot)); -} - -// Appends ".html" to any css, and any js that will be included in client code -// Then copies those .html files to the upload staging folder. -function copyClientCode() { - return gulp.src([ - srcRoot + '/ui/*.html', - srcRoot + '/ui/*.css', - srcRoot + '/ui/*.client.js']) - .pipe( - rename(function(path) { - if (path.extname !== '.html') { - path.extname = path.extname + '.html'; - } - return path; - })) - .pipe(gulp.dest(dstRoot)); -} - -// Does any environment specific work. -// the "lint" step is also here, as that is only done on "dev" -// targeted updates. -function copyEnvironmentSpecific() { - // Do target environment specific work - switch (options.env) { - case 'dev': - gulp.src(srcRoot + '/**/*.js') - .pipe(jshint()) - .pipe(jshint.reporter('jshint-stylish')); - break; - - case 'tst': - //Copy test scripts, if target is "tst" - gulp.src(srcRoot + '/tests/*.js') - .pipe(gulp.dest(dstRoot)); - break; - - default: - break; - } - - return gulp.src(srcRoot + '/environments/' + options.env + '/*.js') - .pipe(gulp.dest(dstRoot)); -} - -// Utility tasks -gulp.task('clean-deployment', function(cb) { - return del([ - dstRoot + '/*.*' - ]); -}); - -gulp.task('clean-deployments', function(cb) { - return del([ - 'build/dev/src/*.*', - 'build/tst/src/*.*' , - 'build/prd/src/*.*' - ]); -}); - -gulp.task('lint', function() { - return gulp.src(srcRoot + '/**/*.js') - .pipe(jshint()) - .pipe(jshint.reporter('jshint-stylish')); -}); - - diff --git a/import_export_development/package.json b/import_export_development/package.json deleted file mode 100644 index 7b73627c7..000000000 --- a/import_export_development/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "author": "mhessinger@google.com", - "dependencies": { - "del": "^2.2.0", - "gulp": "^3.9.0", - "gulp-debug": "^2.1.2", - "gulp-jshint": "^1.12.0", - "gulp-rename": "^1.2.2", - "gulp-shell": "^0.5.1", - "jshint-stylish": "^2.0.1", - "minimist": "^1.2.0", - "node-google-apps-script": "^1.1.3", - "vinyl-paths": "^2.1.0" - }, - "description": "A sample showing command line tool techniques that can be used when developing for Google Apps Script projects.", - "keywords": [ - "google", - "apps-script" - ], - "license": "SEE LICENSE IN FILE license.txt", - "main": "gulpfile.js", - "name": "dev-with-apps-script", - "repository": { - "type": "git", - "url": "https://github.com/tbd" - }, - "scripts": {}, - "version": "1.0.0" -} diff --git a/import_export_development/src/environments/dev/debug.local.config.js b/import_export_development/src/environments/dev/debug.local.config.js deleted file mode 100644 index 88653a0cd..000000000 --- a/import_export_development/src/environments/dev/debug.local.config.js +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -// For testing - broadens the OAuth scope to allow opening any -// Spreadsheet on the current user's Drive -/** @NotOnlyCurrentDoc */ - - -/** - * @param {myproj.json.Configuration} configuration - * The current configuration settings. - * @return {myproj.json.Configuration} configuration - * The current configuration settings, updated with test settings. - */ - function provideEnvironmentConfiguration_(configuration) { - //TODO: (blog post reader) Change ID below to a Spreadsheet ID you can read - configuration.sheets.debugSpreadsheetId = - 'DRIVE_FILE_ID'; - configuration.debug = true; - return configuration; -} diff --git a/import_export_development/src/environments/prd/a.myproj.prd.config.js b/import_export_development/src/environments/prd/a.myproj.prd.config.js deleted file mode 100644 index bdb89f906..000000000 --- a/import_export_development/src/environments/prd/a.myproj.prd.config.js +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// For production, this script will now only be able to act on Spreadsheets that -// it is attached to via a user installing and activating the add-on. -/** @OnlyCurrentDoc */ -// Put additional production configuration here diff --git a/import_export_development/src/environments/tst/a.myproj.tst.config.js b/import_export_development/src/environments/tst/a.myproj.tst.config.js deleted file mode 100644 index e4024ab07..000000000 --- a/import_export_development/src/environments/tst/a.myproj.tst.config.js +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2013 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// For testing - broadens the OAuth scope to allow opening any -// Spreadsheet on the current user's Drive -/** @NotOnlyCurrentDoc */ - - -/** - * @param {myproj.json.Configuration} configuration - * The current configuration settings. - * @return {myproj.json.Configuration} configuration - * The current configuration settings, updated with test settings. - */ - function provideEnvironmentConfiguration_(configuration) { - //TODO: (blog post reader) Change ID below to a Spreadsheet ID you can read - configuration.sheets.debugSpreadsheetId = 'DRIVE_FILE_ID'; - return configuration; -} diff --git a/import_export_development/src/libs/lib.configuration.js b/import_export_development/src/libs/lib.configuration.js deleted file mode 100644 index 9eafe64b1..000000000 --- a/import_export_development/src/libs/lib.configuration.js +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -var Configuration = { - /** - * Returns the default global settings object. If a developer has added - * function 'provideEnvironmentConfiguration_(globals)' to their project, that - * will be added to the global namespace, and will be visible here to allow - * the dev to set specific values for their run. - * @return {myproj.json.Configuration} - */ - getCurrent: function() { - if (typeof getDefaultConfiguration_ === 'undefined') { - throw 'You must provide an implementation of getDefaultConfiguration_' + - 'to use this configuration library.'; - } - var defaultConfiguration = getDefaultConfiguration_(); - if (typeof provideEnvironmentConfiguration_ !== 'undefined') { - return provideEnvironmentConfiguration_(defaultConfiguration); - } else { - return defaultConfiguration; - } - } -}; - diff --git a/import_export_development/src/libs/lib.utils.server.js b/import_export_development/src/libs/lib.utils.server.js deleted file mode 100644 index df16cea07..000000000 --- a/import_export_development/src/libs/lib.utils.server.js +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -/** - * Logs an Apps Script exception, including the call stack - * @private - * @param {Object} e Apps Script runtime error object - */ -function logException_(e) { - Logger.log('Apps Script runtime exception:'); - Logger.log(e.message); - Logger.log('\n' + e.stack + '\n'); -} diff --git a/import_export_development/src/libs/lib.utils.sheets.js b/import_export_development/src/libs/lib.utils.sheets.js deleted file mode 100644 index 7900b911f..000000000 --- a/import_export_development/src/libs/lib.utils.sheets.js +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -/** - * Composes the correct URL to open a Google Sheet from Drive by URL. - * @private - * @param {String} fileId Drive file ID for the spreadsheet. - * @param {String} gid The sheet ID to open. - * Value is in "gid=" param in the URL. - * @return {String} Full URL to open the spreadsheet on Drive. - */ -function getSpreadsheetUrl_(fileId, gid) { - return 'https://docs.google.com/spreadsheets/d/' + fileId + - '/edit#gid=' + gid; -} - - - -/** - * Assists in working in a standalone project that may be dynamically bound to - * a Spreadsheet (as an add-on), but also may run separate (testing, Execution - * API, etc.). - * @constructor - * @param {myproj.json.Configuration} config a valid configuration object - * instance. - */ -function SheetsUtilitiesLibrary(config) { - this.config = config; -} - - -/** - * Returns a Spreadsheet instance. Instance is retrieved by ID if an ID - * has been set by test code in the configuration. - * @return {SpreadSheet} - */ -SheetsUtilitiesLibrary.prototype.getCurrentActiveSpreadsheet = function() { - var loadLocal = ((typeof this.config.sheets !== 'undefined') && - (typeof this.config.sheets.debugSpreadsheetId !== 'undefined') && - (this.config.sheets.debugSpreadsheetId !== '')); - if (loadLocal) { - return SpreadsheetApp.openById(this.config.sheets.debugSpreadsheetId); - } else { - return SpreadsheetApp.getActiveSpreadsheet(); - } -}; - - -/** - * Shows a toast message in the current active spreadsheet. - * @param {String} msg The message to display. - * @param {String} title The title of the toast. - */ -SheetsUtilitiesLibrary.prototype.showToast = function(msg, title) { - this.getCurrentActiveSpreadsheet().toast(msg, title, 4); -}; - - -/** - * Returns true if the current user is the owner of the current spreadsheet. - * @this {SheetsUtilitiesLibrary} current instance - * @return {Boolean} True if the current user is the owner of the Drive file. - */ -SheetsUtilitiesLibrary.prototype.currentUserIsOwner = function() { - var ownerEmail = this.getCurrentActiveSpreadsheet().getOwner().getEmail(); - return (ownerEmail === Session.getEffectiveUser().getEmail()); -}; - - -/** - * Sheets-specific utility. Find a sheet within a spreadsheet with - * the given id. If not present, return null. - * @param {Object} spreadsheet a Spreadsheet object. - * @param {Number} sheetId a Sheet id. - * @return {Object} a Sheet object, or null if not found. - */ -SheetsUtilitiesLibrary.prototype.getSheetById = function(spreadsheet, sheetId) { - if ((typeof sheet === 'undefined') || (!sheetId)) { - return null; - } - var sheets = spreadsheet.getSheets(); - for (var i = 0; i < sheets.length; i++) { - if (sheets[i].getSheetId() === sheetId) { - return sheets[i]; - } - } - return null; -}; - - -/** - * Sheets-specific utility. Given a base title for a sheet, check - * for that it is unique in the spreadsheet. If not, find an integer - * suffix to append to it to make it unique and return. This function - * is used to avoid name collisions while adding or renaming sheets - * automatically. - * @param {Object} spreadsheet a Spreadsheet. - * @param {String} baseName the initial suggested title for a sheet. - * @return {String} a unique title for the sheet, based on the - * given base title. - */ -SheetsUtilitiesLibrary.prototype.getUniqueSheetName = - function(spreadsheet, baseName) { - var sheetName = baseName; - var i = 2; - while (spreadsheet.getSheetByName(sheetName) !== null) { - sheetName = baseName + ' ' + i++; - } - return sheetName; -}; - - -/** - * Sheets-specific utility. Given a spreadsheet and a triggerId string, - * return the user trigger that corresponds to that ID. Returns null - * if no such trigger exists. - * @param {Spreadsheet} spreadsheet container of the user triggers. - * @param {String} triggerId trigger ID string. - * @return {Trigger} corresponding user trigger, or null if not found. - */ -SheetsUtilitiesLibrary.prototype.getUserTriggerById = - function(spreadsheet, triggerId) { - var triggers = ScriptApp.getUserTriggers(spreadsheet); - for (var i = 0; i < triggers.length; i++) { - if (triggers[i].getUniqueId() === triggerId) { - return triggers[i]; - } - } -}; - - -/** - * Sheets-specific utility. Given a String sheet id, activate that - * sheet if it exists. - * @this {SheetsUtilitiesLibrary} current instance - * @param {String} sheetId the sheet ID. - */ -SheetsUtilitiesLibrary.prototype.activateById = function(sheetId) { - var spreadsheet = this.getCurrentActiveSpreadsheet; - var sheet = this.getSheetById(spreadsheet, parseInt(sheetId)); - if (sheet !== null) { - sheet.activate(); - } -}; - diff --git a/import_export_development/src/libs/lib.utils.ui.server.js b/import_export_development/src/libs/lib.utils.ui.server.js deleted file mode 100644 index 1236393b2..000000000 --- a/import_export_development/src/libs/lib.utils.ui.server.js +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -/** - * Used to insert any HTML file in the project into an outer HTML file. - * Called from within the outer HTML file. - * @param {String} filename Name of the file in the project. - * Do not include ".html". - * @return {String} HTML markup for the requested file. - */ -function include(filename) { - return HtmlService.createHtmlOutputFromFile(filename) - .getContent(); -} diff --git a/import_export_development/src/server/a.myproj.server.addon.js b/import_export_development/src/server/a.myproj.server.addon.js deleted file mode 100644 index ed7acec5a..000000000 --- a/import_export_development/src/server/a.myproj.server.addon.js +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -/** - * Called when an add-on is installed. - * @param {Object} e Apps Script onInstall event object - */ -function onInstall(e) { - onOpen(e); -} - - -/** - * Called when a spreadsheet that is associated with this add-on is opened. - * @param {Object} e Apps Script onInstall event object - */ -function onOpen(e) { - var addonMenu = SpreadsheetApp.getUi().createAddonMenu(); - addonMenu.addItem('Show sidebar', 'onShowSidebar'); - addonMenu.addToUi(); -} - - -function onShowSidebar() { - var html = HtmlService.createTemplateFromFile('a.myproj.home.view'); - html.mode = 'addon'; - SpreadsheetApp.getUi() - .showSidebar(html.evaluate() - .setSandboxMode(HtmlService.SandboxMode.IFRAME)); -} - - diff --git a/import_export_development/src/server/a.myproj.server.main.js b/import_export_development/src/server/a.myproj.server.main.js deleted file mode 100644 index fc67351f5..000000000 --- a/import_export_development/src/server/a.myproj.server.main.js +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -/** - * An example function that does something with the "active" spreadsheet, - * which may be overridden by the developers debug function. - * @return {Integer} - */ -function countSheets() { - var config = Configuration.getCurrent(); - var currentSpreadsheet = new SheetsUtilitiesLibrary(config) - .getCurrentActiveSpreadsheet(); - var count = currentSpreadsheet.getSheets().length; - if (config.debug) { - Logger.log( - Utilities.formatString('main::countSheets:\nThere %s %d sheet%s.', - (count > 1) ? 'are' : 'is', count, (count > 1) ? 's' : '')); - } - return count; -} - - -/** - * Passed into the configuration factory constructor - * @return {myproj.json.Configuration} Default configuration settings. - */ -function getDefaultConfiguration_() { - return { - debug: false, - sheets: { - debugSpreadsheetId: null - } - }; -} - diff --git a/import_export_development/src/server/a.myproj.server.web.js b/import_export_development/src/server/a.myproj.server.web.js deleted file mode 100644 index 75ac7fe1e..000000000 --- a/import_export_development/src/server/a.myproj.server.web.js +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -/** - * Sample implementation of the function called when this script - * is invoked as a web app. - * @return {String} HTML as string for Apps Script to render. - */ -function doGet() { - var html = HtmlService.createTemplateFromFile('a.myproj.home.view'); - html.mode = 'web'; - return html.evaluate() - .setSandboxMode(HtmlService.SandboxMode.IFRAME); -} diff --git a/import_export_development/src/tests/a.myproj.tests.server.main.js b/import_export_development/src/tests/a.myproj.tests.server.main.js deleted file mode 100644 index 69bec543e..000000000 --- a/import_export_development/src/tests/a.myproj.tests.server.main.js +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -function runAllTests() { - var sampleTests = new SampleTests(); - sampleTests.run(); -} - - - -/** - * Base class for all tests. - * @constructor - */ -var Tests = function() { - // Do nothing. -}; - - -/** - * Runs all member functions in the child class whose names begin with "test". - */ -Tests.prototype.run = function() { - var testCount = 0; - var failCount = 0; - var failNames = []; - Logger.log('Starting test run.'); - for (var member in this) { - if ((typeof this[member] !== 'undefined') && - (member.indexOf('test') === 0)) { - try { - testCount++; - this[member].call(this); - Logger.log('[PASS] : ' + member); - } catch (error) { - failCount++; - failNames.push('\t' + member + '\t\t' + error); - Logger.log('[FAIL] : ' + member + '\n' + error); - } - } - } - Logger.log(testCount + ' tests completed in this run.'); - Logger.log(failCount + ' tests failed:\n' + failNames.join('\n')); - Logger.log(''); -}; - - -var SampleTests = function() { - Tests.call(this); -}; -inherit_(SampleTests, Tests); - - -/** - * Test to confirm that the test spreadsheet has 5 sheets. - */ -SampleTests.prototype.testCorrectCount = function() { - var testActual = countSheets(); - assertEquals_(5, testActual); -}; - - -/** - * Test to confirm that the test spreadsheet does not have 10 sheets. - */ -SampleTests.prototype.testIncorrectCount = function() { - var testActual = countSheets(); - assertEquals_(10, testActual); -}; diff --git a/import_export_development/src/tests/lib.testing.js b/import_export_development/src/tests/lib.testing.js deleted file mode 100644 index d8264104e..000000000 --- a/import_export_development/src/tests/lib.testing.js +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -/** - * Asserts that two values are equal. Throws an error if they are not. - * @private - * @param {*} expected The expected value. - * @param {*} actual The actual value. - * @param {string=} opt_message The message to include in the error - */ -function assertEquals_(expected, actual, opt_message) { - if (expected != actual) { - var message = opt_message || 'The values are not equal.'; - message += ' (Expected: "' + expected + '", Actual: "' + actual + '")'; - throw new Error(message); - } -} - - -/** - * Asserts that the condition is true. Throws an error if it is not. - * @private - * @param {boolean} condition The condition. - * @param {string=} opt_message The message to include in the error - */ -function assertTrue_(condition, opt_message) { - if (!condition) { - var message = opt_message || 'The condition is not true.'; - throw new Error(message); - } -} - - -/** - * Asserts that the value isn't null. Throws an error if it is. - * @private - * @param {*} value The value to test. - * @param {string=} opt_message The message to include in the error - */ -function assertNotNull_(value, opt_message) { - if (value === null) { - var message = opt_message || 'The value is null.'; - throw new Error(message); - } -} - - -/** - * Asserts that the value isn't null or the empty string. - * @private - * Throws an error if it is. - * @param {*} value The value to test. - * @param {string=} opt_message The message to include in the error - */ -function assertNotNullOrEmpty_(value, opt_message) { - assertNotNull(value, message); - if (value === '') { - var message = opt_message || 'The value is empty.'; - throw new Error(message); - } -} - - -/** - * Asserts that the value is of a certain type. Throws an error if it is not. - * @private - * @param {*} value The value to test. - * @param {string} type The expected type. - * @param {string=} opt_message The message to include in the error - */ -function assertTypeOf_(value, type, opt_message) { - if (typeof value != type) { - var message = opt_message || 'The type does not match.'; - message += ' (Expected: "' + type + '", Actual: "' + typeof value + '")'; - throw new Error(message); - } -} - - -/** - * Configures a child class to inherit from a parent class. - * @private - * @param {function(...)} childClass The child class. - * @param {function(...)} parentClass The parent class. - */ -function inherit_(childClass, parentClass) { - /** - * Empty wrapper class. - * @constructor - */ - var TempClass = function() {}; - TempClass.prototype = parentClass.prototype; - childClass.prototype = new TempClass(); - childClass.prototype.super_ = parentClass.prototype; - childClass.prototype.constructor = childClass; -} - -function unique_(prefix) { - return (prefix ? prefix : '') + (new Date()).getTime(); -} - -function random_() { - return Math.floor(Math.random() * 10); -} diff --git a/import_export_development/src/ui/a.myproj.home.controller.client.js b/import_export_development/src/ui/a.myproj.home.controller.client.js deleted file mode 100644 index be529d01a..000000000 --- a/import_export_development/src/ui/a.myproj.home.controller.client.js +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -function runServer_onclick() { - google.script.run - .withSuccessHandler(runSomeAppsScriptSuccess) - .runSomeAppsScript(mode); -} - -function runSomeAppsScriptSuccess(result) { - console.log('runSomeAppsScript result', result); -} - -function runClient_onclick() { - console.log('Mode:', mode); - console.log({ - message: 'This is a response from a client-side function.', - status: 'ok' }); -} diff --git a/import_export_development/src/ui/a.myproj.home.controller.server.js b/import_export_development/src/ui/a.myproj.home.controller.server.js deleted file mode 100644 index 76f858c78..000000000 --- a/import_export_development/src/ui/a.myproj.home.controller.server.js +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -/** - * Sample server function that will be called using google.script.run. - * @param {String} mode Execution mode (add-on or webapp). - * @return {Object} Sample JSON object response. - */ -function runSomeAppsScript(mode) { - var config = Configuration.getCurrent(); - var count = countSheets(); - try { - var msg = Utilities.formatString('There %s %d sheet%s.', - (count > 1) ? 'are' : 'is', count, (count > 1) ? 's' : ''); - if (mode === 'addon') { - new SheetsUtilitiesLibrary(config).showToast(msg, 'Sheet Count'); - } else { - Logger.log(msg); - } - return { - status: 'ok', - sheetCount: count, - message: 'This is a response from a server-side function.' }; - } catch (e) { - logException_(e); - return { status: 'error', message: e.message, stack: e.stack }; - } -} diff --git a/import_export_development/src/ui/a.myproj.home.view.html b/import_export_development/src/ui/a.myproj.home.view.html deleted file mode 100644 index 89c32449c..000000000 --- a/import_export_development/src/ui/a.myproj.home.view.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - -
    - - -
    - - - diff --git a/import_export_development/src/ui/a.myproj.styles.css b/import_export_development/src/ui/a.myproj.styles.css deleted file mode 100644 index f0cf9aa6b..000000000 --- a/import_export_development/src/ui/a.myproj.styles.css +++ /dev/null @@ -1,5 +0,0 @@ -.sample-container { - margin:10px; - text-align: center; - width: 100% -} diff --git a/sheets/bracketmaker.gs b/sheets/bracketmaker.gs new file mode 100644 index 000000000..2eb6e2983 --- /dev/null +++ b/sheets/bracketmaker.gs @@ -0,0 +1,105 @@ +// This script works with the Brackets Test spreadsheet to create a tournament bracket +// given a list of players or teams. + +var RANGE_PLAYER1 = 'FirstPlayer'; +var SHEET_PLAYERS = 'Players'; +var SHEET_BRACKET = 'Bracket'; +var CONNECTOR_WIDTH = 15; + +// This method adds a custom menu item to run the script +function onOpen() { + var ss = SpreadsheetApp.getActiveSpreadsheet(); + ss.addMenu("Bracket Maker", + [{ name: "Create Bracket", functionName: "createBracket" }]); +} + +// This method creates the brackets based on the data provided on the players +function createBracket() { + var ss = SpreadsheetApp.getActiveSpreadsheet(); + var rangePlayers = ss.getRangeByName(RANGE_PLAYER1); + var sheetControl = ss.getSheetByName(SHEET_PLAYERS); + var sheetResults = ss.getSheetByName(SHEET_BRACKET); + + // Get the players from column A. We assume the entire column is filled here. + rangePlayers = rangePlayers.offset(0, 0, sheetControl.getMaxRows() - + rangePlayers.getRowIndex() + 1, 1); + var players = rangePlayers.getValues(); + + // Now figure out how many players there are(ie don't count the empty cells) + var numPlayers = 0; + for (var i = 0; i < players.length; i++) { + if (!players[i][0] || players[i][0].length == 0) { + break; + } + numPlayers++; + } + players = players.slice(0, numPlayers); + + // Provide some error checking in case there are too many or too few players/teams. + if (numPlayers > 64) { + Browser.msgBox("Sorry, currently this script can only create brackets for 64 or fewer players."); + return; // Early exit + } + + if (numPlayers < 3) { + Browser.msgBox("Sorry, you must have at least 3 players."); + return; // Early exit + } + + // First clear the results sheet and all formatting + sheetResults.clear(); + + var upperPower = Math.ceil(Math.log(numPlayers) / Math.log(2)); + + // Find out what is the number that is a power of 2 and lower than numPlayers. + var countNodesUpperBound = Math.pow(2, upperPower); + + // Find out what is the number that is a power of 2 and higher than numPlayers. + var countNodesLowerBound = countNodesUpperBound / 2; + + // This is the number of nodes that will not show in the 1st level. + var countNodesHidden = numPlayers - countNodesLowerBound; + + // Enter the players for the 1st round + var currentPlayer = 0; + for (var i = 0; i < countNodesLowerBound; i++) { + if (i < countNodesHidden) { + // Must be on the first level + var rng = sheetResults.getRange(i * 4 + 1, 1); + setBracketItem_(rng, players); + setBracketItem_(rng.offset(2, 0, 1, 1), players); + setConnector_(sheetResults, rng.offset(0, 1, 3, 1)); + setBracketItem_(rng.offset(1, 2, 1, 1)); + } else { + // This player gets a bye + setBracketItem_(sheetResults.getRange(i * 4 + 2, 3), players); + } + } + + // Now fill in the rest of the bracket + upperPower--; + for (var i = 0; i < upperPower; i++) { + var pow1 = Math.pow(2, i + 1); + var pow2 = Math.pow(2, i + 2); + var pow3 = Math.pow(2, i + 3); + for (var j = 0; j < Math.pow(2, upperPower - i - 1); j++) { + setBracketItem_(sheetResults.getRange((j * pow3) + pow2, i * 2 + 5)); + setConnector_(sheetResults, sheetResults.getRange((j * pow3) + pow1, i * 2 + 4, pow2 + 1, 1)); + } + } +} + +// Sets the value of an item in the bracket and the color. +function setBracketItem_(rng, players) { + if (players) { + var rand = Math.ceil(Math.random() * players.length); + rng.setValue(players.splice(rand - 1, 1)[0][0]); + } + rng.setBackgroundColor('yellow'); +} + +// Sets the color and width for connector cells. +function setConnector_(sheet, rng) { + sheet.setColumnWidth(rng.getColumnIndex(), CONNECTOR_WIDTH); + rng.setBackgroundColor('green'); +} diff --git a/date_add_and_subtract/Code.gs b/sheets/date_add_and_subtract/Code.gs similarity index 100% rename from date_add_and_subtract/Code.gs rename to sheets/date_add_and_subtract/Code.gs diff --git a/date_add_and_subtract/Moment.gs b/sheets/date_add_and_subtract/Moment.gs similarity index 100% rename from date_add_and_subtract/Moment.gs rename to sheets/date_add_and_subtract/Moment.gs diff --git a/date_add_and_subtract/README.md b/sheets/date_add_and_subtract/README.md similarity index 100% rename from date_add_and_subtract/README.md rename to sheets/date_add_and_subtract/README.md diff --git a/sheets/maps_tutorial.gs b/sheets/maps_tutorial.gs new file mode 100644 index 000000000..ff9e9f2b8 --- /dev/null +++ b/sheets/maps_tutorial.gs @@ -0,0 +1,227 @@ +// [START restaurantLocationsMap] +function restaurantLocationsMap() { + // Get the sheet named 'restaurants' + var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('restaurants'); + + // Store the restaurant name and address data in a 2-dimensional array called + // restaurantInfo. This is the data in cells A2:B4 + var restaurantInfo = sheet.getRange(2, 1, sheet.getLastRow() - 1, 2).getValues(); + + // Create a new StaticMap + var restaurantMap = Maps.newStaticMap(); + + // Create a new UI Application, which we use to display the map + var ui = UiApp.createApplication(); + + // Create a grid widget to use for displaying the text of the restaurant names + // and addresses. Start by populating the header row in the grid. + var grid = ui.createGrid(restaurantInfo.length + 1, 3); + grid.setWidget(0, 0, ui.createLabel('Store #').setStyleAttribute('fontWeight', 'bold')); + grid.setWidget(0, 1, ui.createLabel('Store Name').setStyleAttribute('fontWeight', 'bold')); + grid.setWidget(0, 2, ui.createLabel('Address').setStyleAttribute('fontWeight', 'bold')); + + // For each entry in restaurantInfo, create a map marker with the address and + // the style we want. Also add the address info for this restaurant to the + // grid widget. + for (var i = 0; i < restaurantInfo.length; i++) { + restaurantMap.setMarkerStyle(Maps.StaticMap.MarkerSize.MID, + Maps.StaticMap.Color.GREEN, + i + 1); + restaurantMap.addMarker(restaurantInfo[i][1]); + + grid.setWidget(i + 1, 0, ui.createLabel((i + 1).toString())); + grid.setWidget(i + 1, 1, ui.createLabel(restaurantInfo[i][0])); + grid.setWidget(i + 1, 2, ui.createLabel(restaurantInfo[i][1])); + } + + // Create a Flow Panel widget. We add the map and the grid to this panel. + // The height needs to be able to accomodate the number of restaurants, so we + // use a calculation to scale it based on the number of restaurants. + var panel = ui.createFlowPanel().setSize('500px', 515 + (restaurantInfo.length * 25) + 'px'); + + // Get the URL of the restaurant map and use that to create an image and add + // it to the panel. Next add the grid to the panel. + panel.add(ui.createImage(restaurantMap.getMapUrl())); + panel.add(grid); + + // Finally, add the panel widget to our UI instance, and set its height, + // width, and title. + ui.add(panel); + ui.setHeight(515 + (restaurantInfo.length * 25)); + ui.setWidth(500); + ui.setTitle('Restaurant Locations'); + + // Make the UI visible in the spreadsheet. + SpreadsheetApp.getActiveSpreadsheet().show(ui); +} +// [END restaurantLocationsMap] + +// [START getDrivingDirections] +function getDrivingDirections() { + // Set starting and ending addresses + var start = '1600 Amphitheatre Pkwy, Mountain View, CA 94043'; + var end = '345 Spear St, San Francisco, CA 94105'; + + // These regular expressions will be used to strip out + // unneeded HTML tags + var r1 = new RegExp('', 'g'); + var r2 = new RegExp('', 'g'); + var r3 = new RegExp('
    ', 'g'); + var r4 = new RegExp('
    ', 'g'); + + // points is used for storing the points in the step-by-step directions + var points = []; + + // currentLabel is used for number the steps in the directions + var currentLabel = 0; + + // This will be the map on which we display the path + var map = Maps.newStaticMap().setSize(500, 350); + + // Create a new UI Application, which we use to display the map + var ui = UiApp.createApplication(); + // Create a Flow Panel widget, which we use for the directions text + var directionsPanel = ui.createFlowPanel(); + + // Create a new DirectionFinder with our start and end addresses, and request the directions + // The response is a JSON object, which contains the directions + var directions = Maps.newDirectionFinder().setOrigin(start).setDestination(end).getDirections(); + + // Much of this code is based on the template referenced in + // http://googleappsdeveloper.blogspot.com/2010/06/automatically-generate-maps-and.html + for (var i in directions.routes) { + for (var j in directions.routes[i].legs) { + for (var k in directions.routes[i].legs[j].steps) { + // Parse out the current step in the directions + var step = directions.routes[i].legs[j].steps[k]; + + // Call Maps.decodePolyline() to decode the polyline for + // this step into an array of latitudes and longitudes + var path = Maps.decodePolyline(step.polyline.points); + points = points.concat(path); + + // Pull out the direction information from step.html_instructions + // Because we only want to display text, we will strip out the + // HTML tags that are present in the html_instructions + var text = step.html_instructions; + text = text.replace(r1, ' '); + text = text.replace(r2, ' '); + text = text.replace(r3, ' '); + text = text.replace(r4, ' '); + + // Add each step in the directions to the directionsPanel + directionsPanel.add(ui.createLabel((++currentLabel) + ' - ' + text)); + } + } + } + + // be conservative and only sample 100 times to create our polyline path + var lpoints=[]; + if (points.length < 200) + lpoints = points; + else { + var pCount = (points.length / 2); + var step = parseInt(pCount / 100); + for (var i = 0; i < 100; ++i) { + lpoints.push(points[i * step * 2]); + lpoints.push(points[(i * step * 2) + 1]); + } + } + + // make the polyline + if (lpoints.length > 0) { + // Maps.encodePolyline turns an array of latitudes and longitudes + // into an encoded polyline + var pline = Maps.encodePolyline(lpoints); + + // Once we have the encoded polyline, add that path to the map + map.addPath(pline); + } + + // Create a FlowPanel to hold the map + var panel = ui.createFlowPanel().setSize('500px', '350px'); + + // Get the URL of the map and use that to create an image and add + // it to the panel. + panel.add(ui.createImage(map.getMapUrl())); + + // Add both the map panel and the directions panel to the UI instance + ui.add(panel); + ui.add(directionsPanel); + + // Next set the title, height, and width of the UI instance + ui.setTitle('Driving Directions'); + ui.setHeight(525); + ui.setWidth(500); + + // Finally, display the UI within the spreadsheet + SpreadsheetApp.getActiveSpreadsheet().show(ui); +} +// [END getDrivingDirections] + +// [START foo] +function analyzeLocations() { + // Select the sheet named 'geocoder and elevation' + var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('geocoder and elevation'); + + // Store the address data in an array called + // locationInfo. This is the data in cells A2:A20 + var locationInfo = sheet.getRange(2, 1, sheet.getLastRow() - 1, 1).getValues(); + + // Set up some values to use for comparisons. + // latitudes run from -90 to 90, so we start with a max of -90 for comparison + var maxLatitude = -90; + var indexOfMaxLatitude = 0; + + // Set the starting max elevation to 0, or sea level + var maxElevation = 0; + var indexOfMaxElevation = 0; + + // geoResults will hold the JSON results array that we get when calling geocode() + var geoResults; + + // elevationResults will hold the results object that we get when calling sampleLocation() + var elevationResults; + + // lat and lng will temporarily hold the latitude and longitude of each + // address + var lat, lng; + + for (var i = 0; i < locationInfo.length; i++) { + // Get the latitude and longitude for an address. For more details on + // the JSON results array, geoResults, see + // http://code.google.com/apis/maps/documentation/geocoding/#Results + geoResults = Maps.newGeocoder().geocode(locationInfo[i]); + + // Get the latitude and longitude + lat = geoResults.results[0].geometry.location.lat; + lng = geoResults.results[0].geometry.location.lng; + + // Use the latitude and longitude to call sampleLocation and get the + // elevation. For more details on the JSON-formatted results object, + // elevationResults, see + // http://code.google.com/apis/maps/documentation/elevation/#ElevationResponses + elevationResults = Maps.newElevationSampler().sampleLocation(parseFloat(lat), parseFloat(lng)); + + // Check to see if the current latitude is greater than our max latitude + // so far. If so, set maxLatitude and indexOfMaxLatitude + if (lat > maxLatitude) { + maxLatitude = lat; + indexOfMaxLatitude = i; + } + + // Check if elevationResults has a good status and also if the current + // elevation is greater than the max elevation so far. If so, set + // maxElevation and indexOfMaxElevation + if (elevationResults.status == 'OK' && elevationResults.results[0].elevation > maxElevation) { + maxElevation = elevationResults.results[0].elevation; + indexOfMaxElevation = i; + } + } + + // User Browser.msgBox as a simple way to display the info about highest + // elevation and northernmost office. + Browser.msgBox('The US Google office with the highest elevation is: ' + locationInfo[indexOfMaxElevation] + + '. The northernmost US Google office is: ' + locationInfo[indexOfMaxLatitude]); +} +// [END foo] diff --git a/sheets/removing_duplicates.gs b/sheets/removing_duplicates.gs new file mode 100644 index 000000000..926304b3e --- /dev/null +++ b/sheets/removing_duplicates.gs @@ -0,0 +1,29 @@ +// [START removeDuplicates] +function removeDuplicates() { + // [START sheet] + var sheet = SpreadsheetApp.getActiveSheet(); + var data = sheet.getDataRange().getValues(); + // [END sheet] + // [START newData] + var newData = new Array(); + // [END newData] + for(i in data){ + var row = data[i]; + var duplicate = false; + for(j in newData){ + if(row.join() == newData[j].join()){ + duplicate = true; + } + } + // [START duplicate] + if(!duplicate){ + newData.push(row); + } + // [END duplicate] + } + // [START clear] + sheet.clearContents(); + sheet.getRange(1, 1, newData.length, newData[0].length).setValues(newData); + // [END clear] +} +// [END removeDuplicates] diff --git a/sheets/reporting-codelab.md b/sheets/reporting-codelab.md new file mode 100644 index 000000000..e00414225 --- /dev/null +++ b/sheets/reporting-codelab.md @@ -0,0 +1,3 @@ +# Google Sheets API Codelab + +See https://g.co/codelabs/sheets to get started with the Sheets Codelab. diff --git a/slides/image-slides.gs b/slides/image-slides.gs new file mode 100644 index 000000000..9d4571d31 --- /dev/null +++ b/slides/image-slides.gs @@ -0,0 +1,84 @@ +// [START create] +var NAME = "My favorite images"; +var deck = SlidesApp.create(NAME); +// [END create] + +// [START addImageSlide] + function addImageSlide(imageUrl, index) { + var slide = deck.appendSlide(SlidesApp.PredefinedLayout.BLANK); + var image = slide.insertImage(imageUrl); + } +// [END addImageSlide] + +// [START main] +function main() { + var images = [ + "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png", + "http://www.google.com/services/images/phone-animation-results_2x.png", + "http://www.google.com/services/images/section-work-card-img_2x.jpg", + "http://gsuite.google.com/img/icons/product-lockup.png", + "http://gsuite.google.com/img/home-hero_2x.jpg" + ]; + var [title, subtitle] = deck.getSlides()[0].getPageElements(); + title.asShape().getText().setText(NAME); + subtitle.asShape().getText().setText("Google Apps Script\nSlides Service demo"); + images.forEach(addImageSlide); +} +// [END main] + +// [START addImageSlide] +function addImageSlide(imageUrl, index) { + var slide = presentation.appendSlide(SlidesApp.PredefinedLayout.BLANK); + var image = slide.insertImage(imageUrl); + var imgWidth = image.getWidth(); + var imgHeight = image.getHeight(); + var pageWidth = presentation.getPageWidth(); + var pageHeight = presentation.getPageHeight(); + var newX = pageWidth/2. - imgWidth/2.; + var newY = pageHeight/2. - imgHeight/2.; + image.setLeft(newX).setTop(newY); +} +// [END addImageSlide] + + +// [START fullscript] +var NAME = "My favorite images"; +var presentation = SlidesApp.create(NAME); + +/** + * Creates a single slide using the image from the given link; + * used directly by foreach(), hence the parameters are fixed. + * + * @param {Date} link A String object representing an image URL + * @param {Date} index The index into the array; unused (req'd by forEach) + */ +function addImageSlide(imageUrl, index) { + var slide = presentation.appendSlide(SlidesApp.PredefinedLayout.BLANK); + var image = slide.insertImage(imageUrl); + var imgWidth = image.getWidth(); + var imgHeight = image.getHeight(); + var pageWidth = presentation.getPageWidth(); + var pageHeight = presentation.getPageHeight(); + var newX = pageWidth/2. - imgWidth/2.; + var newY = pageHeight/2. - imgHeight/2.; + image.setLeft(newX).setTop(newY); +} + +/** + * The driver application features an array of image URLs, setting of the + * main title & subtitle, and creation of individual slides for each image. + */ +function main() { + var images = [ + "http://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png", + "http://www.google.com/services/images/phone-animation-results_2x.png", + "http://www.google.com/services/images/section-work-card-img_2x.jpg", + "http://gsuite.google.com/img/icons/product-lockup.png", + "http://gsuite.google.com/img/home-hero_2x.jpg" + ]; + var [title, subtitle] = presentation.getSlides()[0].getPageElements(); + title.asShape().getText().setText(NAME); + subtitle.asShape().getText().setText("Google Apps Script\nSlides Service demo"); + images.forEach(addImageSlide); +} +// [END fullscript] diff --git a/slides/selection.gs b/slides/selection.gs new file mode 100644 index 000000000..3a670e7ce --- /dev/null +++ b/slides/selection.gs @@ -0,0 +1,235 @@ +// [START getSelection] +var selection = SlidesApp.getActivePresentation().getSelection(); +// [END getSelection] + +// [START getCurrentPage] +var currentPage = SlidesApp.getActivePresentation().getSelection().getCurrentPage(); +// [END getCurrentPage] + +// [START selectionType] +var selection = SlidesApp.getActivePresentation().getSelection(); +var selectionType = selection.getSelectionType(); +var currentPage; +switch (selectionType) { + case SlidesApp.SelectionType.NONE: + Logger.log('Nothing selected'); + break; + case SlidesApp.SelectionType.CURRENT_PAGE: + currentPage = selection.getCurrentPage(); + Logger.log('Selection is a page with ID: ' + currentPage.getObjectId()); + break; + case SlidesApp.SelectionType.PAGE_ELEMENT: + var pageElements = selection.getPageElementRange().getPageElements(); + Logger.log('There are ' + pageElements.length + ' page elements selected.'); + break; + case SlidesApp.SelectionType.TEXT: + var tableCellRange = selection.getTableCellRange(); + if (tableCellRange != null) { + var tableCell = tableCellRange.getTableCells()[0]; + Logger.log('Selected text is in a table at row ' + + tableCell.getRowIndex() + ', column ' + + tableCell.getColumnIndex()); + } + var textRange = selection.getTextRange(); + if (textRange.getStartIndex() == textRange.getEndIndex()) { + Logger.log('Text cursor position: ' + textRange.getStartIndex()); + } else { + Logger.log('Selection is a text range from: ' + textRange.getStartIndex() + ' to: ' + + textRange.getEndIndex() + ' is selected'); + } + break; + case SlidesApp.SelectionType.TABLE_CELL: + var tableCells = selection.getTableCellRange().getTableCells(); + var table = tableCells[0].getParentTable(); + Logger.log('There are ' + tableCells.length + ' table cells selected.'); + break; + case SlidesApp.SelectionType.PAGE: + var pages = selection.getPageRange().getPages(); + Logger.log('There are ' + pages.length + ' pages selected.'); + break; + default: + break; +} +// [END selectionType] + +// [START select] +// Select the first slide as the current page selection and remove any previous selection. +var selection = SlidesApp.getActivePresentation().getSelection(); +var slide = SlidesApp.getActivePresentation().getSlides()[0]; +slide.selectAsCurrentPage(); +// State of selection +// +// selection.getSelectionType() = SlidesApp.SelectionType.CURRENT_PAGE +// selection.getCurrentPage() = slide +// +// [END select] + +// [START selectPageElement] +var slide = SlidesApp.getActivePresentation().getSlides()[0]; +var pageElement = slide.getPageElements()[0]; +// Only select this page element and remove any previous selection. +pageElement.select(); +// State of selection +// +// selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT +// selection.getCurrentPage() = slide +// selection.getPageElementRange().getPageElements()[0] = pageElement +// +// [END selectPageElement] + +// [START selectMultiplePageElements] +var slide = SlidesApp.getActivePresentation().getSlides()[0]; +// First select the slide page, as the current page selection. +slide.selectAsCurrentPage(); +// Then select all the page elements in the selected slide page. +var pageElements = slide.getPageElements(); +for (var i = 0; i < pageElements.length; i++) { + pageElements[i].select(false); +} +// State of selection +// +// selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT +// selection.getCurrentPage() = slide +// selection.getPageElementRange().getPageElements() = pageElements +// +// [END selectMultiplePageElements] + +// [START transformSelection] +var slide = SlidesApp.getActivePresentation().getSlides()[0]; +var shape1 = slide.getPageElements()[0].asShape(); +var shape2 = slide.getPageElements()[1].asShape(); +// Select both the shapes. +shape1.select(); +shape2.select(false); +// State of selection +// +// selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT +// selection.getCurrentPage() = slide +// selection.getPageElementRange().getPageElements() = [shape1, shape2] +// +// Remove one shape. +shape2.remove(); +// State of selection +// +// selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT +// selection.getCurrentPage() = slide +// selection.getPageElementRange().getPageElements() = [shape1] +// +// [END transformSelection] + +// [START rangeSelection] +var slide = SlidesApp.getActivePresentation().getSlides()[0]; +var shape = slide.getPageElements()[0].asShape(); +shape.getText().setText("Hello"); +// Range selection: Select the text range "He". +shape.getText().getRange(0, 2).select(); +// State of selection +// +// selection.getSelectionType() = SlidesApp.SelectionType.TEXT +// selection.getCurrentPage() = slide +// selection.getPageElementRange().getPageElements()[0] = shape +// selection.getTextRange().getStartIndex() = 0 +// selection.getTextRange().getEndIndex() = 2 +// +// [END rangeSelection] + +// [START cursorSelection] +var slide = SlidesApp.getActivePresentation().getSlides()[0]; +var shape = slide.getPageElements()[0].asShape(); +shape.getText().setText("Hello"); +// Cursor selection: Place the cursor after "H" like "H|ello". +shape.getText().getRange(1, 1).select(); +// State of selection +// +// selection.getSelectionType() = SlidesApp.SelectionType.TEXT +// selection.getCurrentPage() = slide +// selection.getPageElementRange().getPageElements()[0] = shape +// selection.getTextRange().getStartIndex() = 1 +// selection.getTextRange().getEndIndex() = 1 +// +// [END cursorSelection] + +// [START rangeSelectionInTable] +var slide = SlidesApp.getActivePresentation().getSlides()[0]; +var table = slide.getPageElements()[0].asTable(); +var tableCell = table.getCell(0, 1); +tableCell.getText().setText("Hello"); +// Range selection: Select the text range "He". +tableCell.getText().getRange(0, 2).select(); +// State of selection +// +// selection.getSelectionType() = SlidesApp.SelectionType.TEXT +// selection.getCurrentPage() = slide +// selection.getPageElementRange().getPageElements()[0] = table +// selection.getTableCellRange().getTableCells()[0] = tableCell +// selection.getTextRange().getStartIndex() = 0 +// selection.getTextRange().getEndIndex() = 2 +// +// [END rangeSelectionInTable] + +// [START cursorSelectionInTableCell] +var slide = SlidesApp.getActivePresentation().getSlides()[0]; +var table = slide.getPageElements()[0].asTable(); +var tableCell = table.getCell(0, 1); +tableCell.getText().setText("Hello"); +// Cursor selection: Place the cursor after "H" like "H|ello". +tableCell.getText().getRange(1, 1).select(); +// State of selection +// +// selection.getSelectionType() = SlidesApp.SelectionType.TEXT +// selection.getCurrentPage() = slide +// selection.getPageElementRange().getPageElements()[0] = table +// selection.getTableCellRange().getTableCells()[0] = tableCell +// selection.getTextRange().getStartIndex() = 1 +// selection.getTextRange().getEndIndex() = 1 +// +// [END cursorSelectionInTableCell] + +// [START selectionTransformation] +var slide = SlidesApp.getActivePresentation().getSlides()[0]; +var shape = slide.getPageElements()[0].asShape(); +var textRange = shape.getText(); +textRange.setText("World"); +// Select all the text "World". +textRange.select(); +// State of selection +// +// selection.getSelectionType() = SlidesApp.SelectionType.TEXT +// selection.getCurrentPage() = slide +// selection.getPageElementRange().getPageElements()[0] = shape +// selection.getTextRange().getStartIndex() = 0 +// selection.getTextRange().getEndIndex() = 6 +// +// Add some text to the shape, and the selection will be transformed. +textRange.insertText(0, 'Hello '); + +// State of selection +// +// selection.getSelectionType() = SlidesApp.SelectionType.TEXT +// selection.getCurrentPage() = slide +// selection.getPageElementRange().getPageElements()[0] = shape +// selection.getTextRange().getStartIndex() = 0 +// selection.getTextRange().getEndIndex() = 12 +// +// [END selectionTransformation] + +// [START unselecting] +// Unselect one or more page elements already selected. +// +// In case one or more page elements in the first slide are selected, setting the +// same (or any other) slide page as the current page would do the unselect. +// +var slide = SlidesApp.getActivePresentation().getSlides()[0]; +slide.selectAsCurrentPage(); +// [END unselecting] + +// [START selecting] +// Unselect one or more page elements already selected. +// +// In case one or more page elements in the first slide are selected, +// selecting any pageElement in the first slide (or any other pageElement) would +// do the unselect and select that pageElement. +// +var slide = SlidesApp.getActivePresentation().getSlides()[0]; +slide.getPageElements()[0].select(); +// [END selecting] diff --git a/slides/sidebar/code.gs b/slides/sidebar/code.gs new file mode 100644 index 000000000..7308c4d50 --- /dev/null +++ b/slides/sidebar/code.gs @@ -0,0 +1,67 @@ +/** + * @OnlyCurrentDoc Adds progress bars to a presentation. + */ +var BAR_ID = 'PROGRESS_BAR_ID'; +var BAR_HEIGHT = 10; // px +var presentation = SlidesApp.getActivePresentation(); + +/** + * 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(); +} + +/** + * Trigger for opening a presentation. + * @param {object} e The onOpen event. + */ +function onOpen(e) { + SlidesApp.getUi().createAddonMenu() + .addItem('Show progress bar', 'createBars') + .addItem('Hide progress bar', 'deleteBars') + .addToUi(); +} + +/** + * Create a rectangle on every slide with different bar widths. + */ +function createBars() { + deleteBars(); // Delete any existing progress bars + var slides = presentation.getSlides(); + for (var i = 0; i < slides.length; ++i) { + var ratioComplete = (i / (slides.length - 1)); + var x = 0; + var y = presentation.getPageHeight() - BAR_HEIGHT; + var barWidth = presentation.getPageWidth() * ratioComplete; + if (barWidth > 0) { + var bar = slides[i].insertShape(SlidesApp.ShapeType.RECTANGLE, x, y, + barWidth, BAR_HEIGHT); + bar.getBorder().setTransparent(); + bar.setLinkUrl(BAR_ID); + } + } +} + +/** + * Deletes all progress bar rectangles. + */ +function deleteBars() { + var slides = presentation.getSlides(); + for (var i = 0; i < slides.length; ++i) { + var elements = slides[i].getPageElements(); + for (var j = 0; j < elements.length; ++j) { + var el = elements[j]; + if (el.getPageElementType() === SlidesApp.PageElementType.SHAPE && + el.asShape().getLink() && + el.asShape().getLink().getUrl() === BAR_ID) { + el.remove(); + } + } + } +} diff --git a/slides/style.gs b/slides/style.gs new file mode 100644 index 000000000..8a1238336 --- /dev/null +++ b/slides/style.gs @@ -0,0 +1,66 @@ +// [START hello] +var slide = SlidesApp.getActivePresentation().getSlides()[0]; +var shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 100, 200, 300, 60); +var textRange = shape.getText(); +textRange.setText('Hello world!'); +Logger.log('Start: ' + textRange.getStartIndex() + '; End: ' + + textRange.getEndIndex() + '; Content: ' + textRange.asString()); +var subRange = textRange.getRange(0, 5); +Logger.log('Sub-range Start: ' + subRange.getStartIndex() + '; Sub-range End: ' + + subRange.getEndIndex() + '; Sub-range Content: ' + subRange.asString()); +// [END hello] + +// [START insertText] +var slide = SlidesApp.getActivePresentation().getSlides()[0]; +var shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 100, 200, 300, 60); +var textRange = shape.getText(); +textRange.setText('Hello world!'); +textRange.clear(6, 11); +textRange.insertText(6, 'galaxy'); +Logger.log('Start: ' + textRange.getStartIndex() + '; End: ' + + textRange.getEndIndex() + '; Content: ' + textRange.asString()); +// [END insertText] + +// [START styleText] +var slide = SlidesApp.getActivePresentation().getSlides()[0]; +var shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 100, 200, 300, 60); +var textRange = shape.getText(); +textRange.setText('Hello '); +var insertedText = textRange.appendText('world!'); +insertedText.getTextStyle() + .setBold(true) + .setLinkUrl('www.example.com') + .setForegroundColor('#ff0000'); +var helloRange = textRange.getRange(0, 5); +Logger.log('Text: ' + helloRange.asString() + '; Bold: ' + helloRange.getTextStyle().isBold()); +Logger.log('Text: ' + insertedText.asString() + '; Bold: ' + insertedText.getTextStyle().isBold()); +Logger.log('Text: ' + textRange.asString() + '; Bold: ' + textRange.getTextStyle().isBold()); +// [END styleText] + +// [START paragraphStyling] +var slide = SlidesApp.getActivePresentation().getSlides()[0]; +var shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 50, 50, 300, 300); +var textRange = shape.getText(); +textRange.setText('Paragraph 1\nParagraph2\nParagraph 3\nParagraph 4'); +var paragraphs = textRange.getParagraphs(); +for (var i = 0; i < 3; i++) { + var paragraphStyle = paragraphs[i].getRange().getParagraphStyle(); + paragraphStyle.setParagraphAlignment(SlidesApp.ParagraphAlignment.CENTER); +} +// [END paragraphStyling] + +// [START listStyling] +var slide = SlidesApp.getActivePresentation().getSlides()[0]; +var shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 50, 50, 300, 300); +var textRange = shape.getText(); +textRange.appendText('Item 1\n') + .appendText('\tItem 2\n') + .appendText('\t\tItem 3\n') + .appendText('Item 4'); +textRange.getListStyle().applyListPreset(SlidesApp.ListPreset.DIGIT_ALPHA_ROMAN); +var paragraphs = textRange.getParagraphs(); +for (var i = 0; i < paragraphs.length; i++) { + var listStyle = paragraphs[i].getRange().getListStyle(); + Logger.log('Paragraph ' + (i + 1) + ''s nesting level: ' + listStyle.getNestingLevel());'; +} +// [END listStyling] diff --git a/slides/translate/code.gs b/slides/translate/code.gs new file mode 100644 index 000000000..55ebc75bb --- /dev/null +++ b/slides/translate/code.gs @@ -0,0 +1,100 @@ +/** + * @OnlyCurrentDoc Limits the script to only accessing the current presentation. + */ + +/** + * Create a open translate menu item. + */ +function onOpen(event) { + SlidesApp.getUi().createAddonMenu() + .addItem('Open Translate', 'showSidebar') + .addToUi(); +} + +/** + * Open the Add-on upon install. + */ +function onInstall(event) { + onOpen(event); +} + +/** + * Opens a sidebar in the document containing the add-on's user interface. + */ +function showSidebar() { + var ui = HtmlService + .createHtmlOutputFromFile('Sidebar') + .setTitle('Translate'); + SlidesApp.getUi().showSidebar(ui); +} + +/** + * Recursively gets child text elements a list of elements. + * @param {PageElement[]} elements The elements to get text from. + * @return {Text[]} An array of text elements. + */ +function getElementTexts(elements) { + var texts = []; + elements.forEach(function(element) { + switch (element.getPageElementType()) { + case SlidesApp.PageElementType.GROUP: + element.asGroup().getChildren().forEach(function(child) { + texts = texts.concat(getElementTexts(child)); + }); + break; + case SlidesApp.PageElementType.TABLE: + var table = element.asTable(); + for (var y = 0; y < table.getNumColumns(); ++y) { + for (var x = 0; x < table.getNumRows(); ++x) { + texts.push(table.getCell(x, y).getText()); + } + } + break; + case SlidesApp.PageElementType.SHAPE: + texts.push(element.asShape().getText()); + break; + } + }); + return texts; +} + +/** + * Translates selected slide elements to the target language using Apps Script's Language service. + * + * @param {string} targetLanguage The two-letter short form for the target language. (ISO 639-1) + * @return {number} The number of elements translated. + */ +function translateSelectedElements(targetLanguage) { + // Get selected elements. + var selection = SlidesApp.getActivePresentation().getSelection(); + var selectionType = selection.getSelectionType(); + var texts = []; + switch (selectionType) { + case SlidesApp.SelectionType.PAGE: + var pages = selection.getPageRange().getPages().forEach(function(page) { + texts = texts.concat(getElementTexts(page.getPageElements())); + }); + break; + case SlidesApp.SelectionType.PAGE_ELEMENT: + var pageElements = selection.getPageElementRange().getPageElements(); + texts = texts.concat(getElementTexts(pageElements)); + break; + case SlidesApp.SelectionType.TABLE_CELL: + var cells = selection.getTableCellRange().getTableCells().forEach(function(cell) { + texts.push(cell.getText()); + }); + break; + case SlidesApp.SelectionType.TEXT: + var elements = selection.getPageElementRange().getPageElements().forEach(function(element) { + texts.push(element.asShape().getText()); + }); + break; + } + + // Translate all elements in-place. + texts.forEach(function(text) { + text.setText(LanguageApp.translate(text.asRenderedString(), '', targetLanguage)); + }) + + return texts.length; +} diff --git a/slides/translate/sidebar.html b/slides/translate/sidebar.html new file mode 100644 index 000000000..28e8f616a --- /dev/null +++ b/slides/translate/sidebar.html @@ -0,0 +1,84 @@ + + + + + + + + + + + + + From 5cee0bf5045ebf6c0749e7c6a1f1e3e5997b4e60 Mon Sep 17 00:00:00 2001 From: Grant Timmerman Date: Fri, 1 Dec 2017 15:20:46 -0800 Subject: [PATCH 034/374] Fixes deprecated method use. Reported on GitHub. https://github.com/googlesamples/apps-script/issues/17 https://developers.google.com/apps-script/reference/document/range Change-Id: Ida7edacee9e8589b1e06e6d05e12664a8da219b2 --- docs/translate/Code.gs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/translate/Code.gs b/docs/translate/Code.gs index e8b57353d..90d2b0998 100644 --- a/docs/translate/Code.gs +++ b/docs/translate/Code.gs @@ -44,7 +44,7 @@ function getSelectedText() { var selection = DocumentApp.getActiveDocument().getSelection(); if (selection) { var text = []; - var elements = selection.getSelectedElements(); + var elements = selection.getRangeElements(); for (var i = 0; i < elements.length; i++) { if (elements[i].isPartial()) { var element = elements[i].getElement().asText(); @@ -138,7 +138,7 @@ function insertText(newText) { var selection = DocumentApp.getActiveDocument().getSelection(); if (selection) { var replaced = false; - var elements = selection.getSelectedElements(); + var elements = selection.getRangeElements(); if (elements.length == 1 && elements[0].getElement().getType() == DocumentApp.ElementType.INLINE_IMAGE) { From c4caab405ecfc60409b2f0ea579ba34324179e93 Mon Sep 17 00:00:00 2001 From: Grant Timmerman Date: Mon, 11 Dec 2017 13:30:35 -0800 Subject: [PATCH 035/374] Add other Apps Script samples for use on GitHub and with gasp. - Form Add-ons - https://github.com/googlesamples/apps-script-form-notifications-addon - Combining the sample with all other samples in this repo. - Added READMEs to major folders (advanced/ not included) - Moved some projects to folders for better gasp integration. Standardized filenames to be camelCase. After some research, Node vs AirBnB vs Angular vs Google vs Facebook all have different guides. camelCase is a readable and reasonable choice. Change-Id: Ic01741031784e11b6310e5298d0486b264e06538 --- README.md | 6 +- advanced/{admin-sdk.gs => adminSDK.gs} | 0 .../{apps-activity.gs => appsActivity.gs} | 0 .../{fusion-tables.gs => fusionTables.gs} | 0 advanced/{google-plus.gs => googlePlus.gs} | 0 ...e-plus-domains.gs => googlePlusDomains.gs} | 0 ...shopping-content.gs => shoppingContent.gs} | 0 advanced/{tag-manager.gs => tagManager.gs} | 0 .../{url-shortener.gs => urlShortener.gs} | 0 ...utube-analytics.gs => youtubeAnalytics.gs} | 0 ...tube-content-id.gs => youtubeContentId.gs} | 0 android/README.md | 5 + calendar/README.md | 5 + .../vacationCalendar.gs} | 0 docs/README.md | 9 + .../README.md | 0 .../cursorInspector.gs} | 0 .../screenshot.png | Bin .../sidebar.css.html} | 0 .../sidebar.html} | 4 +- .../sidebar.js.html} | 0 docs/translate/Sidebar.html | 5 +- docs/translate/{Code.gs => translate.gs} | 4 +- forms/README.md | 10 + forms/notifications/README.md | 44 +++ forms/notifications/about.html | 24 ++ forms/notifications/authorizationEmail.html | 19 ++ forms/notifications/creatorNotification.html | 20 ++ forms/notifications/notification.gs | 291 ++++++++++++++++++ .../notifications/respondentNotification.html | 9 + forms/notifications/sidebar.html | 258 ++++++++++++++++ gmail/README.md | 11 + gmail/{ => mailmerge}/mailmerge.gs | 0 .../sendingEmails.gs} | 0 sheets/README.md | 31 ++ sheets/{ => bracketmaker}/bracketmaker.gs | 0 .../README.md | 0 .../dateAddAndSubtract.gs} | 0 .../moment.gs} | 0 .../screenshot.png | Bin sheets/forms/forms.gs | 178 +++++++++++ sheets/macros/macros.gs | 165 ++++++++++ sheets/{maps_tutorial.gs => maps/maps.gs} | 0 .../removingDuplicates.gs} | 0 sheets/reporting-codelab.md | 3 - {simple_tasks => simpleTasks}/README.md | 0 .../javascript.html | 0 .../Page.html => simpleTasks/page.html | 4 +- {simple_tasks => simpleTasks}/screenshot.png | Bin .../Code.gs => simpleTasks/simpleTasks.gs | 0 .../stylesheet.html | 0 slides/README.md | 15 + .../imageSlides.gs} | 0 slides/{ => selection}/selection.gs | 0 slides/sidebar/{code.gs => sidebar.gs} | 0 slides/{ => style}/style.gs | 0 slides/translate/{code.gs => translate.gs} | 0 57 files changed, 1104 insertions(+), 16 deletions(-) rename advanced/{admin-sdk.gs => adminSDK.gs} (100%) rename advanced/{apps-activity.gs => appsActivity.gs} (100%) rename advanced/{fusion-tables.gs => fusionTables.gs} (100%) rename advanced/{google-plus.gs => googlePlus.gs} (100%) rename advanced/{google-plus-domains.gs => googlePlusDomains.gs} (100%) rename advanced/{shopping-content.gs => shoppingContent.gs} (100%) rename advanced/{tag-manager.gs => tagManager.gs} (100%) rename advanced/{url-shortener.gs => urlShortener.gs} (100%) rename advanced/{youtube-analytics.gs => youtubeAnalytics.gs} (100%) rename advanced/{youtube-content-id.gs => youtubeContentId.gs} (100%) create mode 100644 android/README.md create mode 100644 calendar/README.md rename calendar/{vacation_calendar.gs => vacationCalendar/vacationCalendar.gs} (100%) create mode 100644 docs/README.md rename docs/{cursor_inspector => cursorInspector}/README.md (100%) rename docs/{cursor_inspector/Code.gs => cursorInspector/cursorInspector.gs} (100%) rename docs/{cursor_inspector => cursorInspector}/screenshot.png (100%) rename docs/{cursor_inspector/Sidebar.css.html => cursorInspector/sidebar.css.html} (100%) rename docs/{cursor_inspector/Sidebar.html => cursorInspector/sidebar.html} (95%) rename docs/{cursor_inspector/Sidebar.js.html => cursorInspector/sidebar.js.html} (100%) rename docs/translate/{Code.gs => translate.gs} (98%) create mode 100644 forms/README.md create mode 100644 forms/notifications/README.md create mode 100644 forms/notifications/about.html create mode 100644 forms/notifications/authorizationEmail.html create mode 100644 forms/notifications/creatorNotification.html create mode 100644 forms/notifications/notification.gs create mode 100644 forms/notifications/respondentNotification.html create mode 100644 forms/notifications/sidebar.html create mode 100644 gmail/README.md rename gmail/{ => mailmerge}/mailmerge.gs (100%) rename gmail/{sending_emails.gs => sendingEmails/sendingEmails.gs} (100%) create mode 100644 sheets/README.md rename sheets/{ => bracketmaker}/bracketmaker.gs (100%) rename sheets/{date_add_and_subtract => dateAddAndSubtract}/README.md (100%) rename sheets/{date_add_and_subtract/Code.gs => dateAddAndSubtract/dateAddAndSubtract.gs} (100%) rename sheets/{date_add_and_subtract/Moment.gs => dateAddAndSubtract/moment.gs} (100%) rename sheets/{date_add_and_subtract => dateAddAndSubtract}/screenshot.png (100%) create mode 100644 sheets/forms/forms.gs create mode 100644 sheets/macros/macros.gs rename sheets/{maps_tutorial.gs => maps/maps.gs} (100%) rename sheets/{removing_duplicates.gs => removingDuplicates/removingDuplicates.gs} (100%) delete mode 100644 sheets/reporting-codelab.md rename {simple_tasks => simpleTasks}/README.md (100%) rename simple_tasks/JavaScript.html => simpleTasks/javascript.html (100%) rename simple_tasks/Page.html => simpleTasks/page.html (85%) rename {simple_tasks => simpleTasks}/screenshot.png (100%) rename simple_tasks/Code.gs => simpleTasks/simpleTasks.gs (100%) rename simple_tasks/Stylesheet.html => simpleTasks/stylesheet.html (100%) create mode 100644 slides/README.md rename slides/{image-slides.gs => imageSlides/imageSlides.gs} (100%) rename slides/{ => selection}/selection.gs (100%) rename slides/sidebar/{code.gs => sidebar.gs} (100%) rename slides/{ => style}/style.gs (100%) rename slides/translate/{code.gs => translate.gs} (100%) diff --git a/README.md b/README.md index d7c66c6a7..7a9252595 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Google Apps Script Samples -Various sample code and projects for the Google Apps Script platform. Learn -more at +Various sample code and projects for the Google Apps Script platform. Learn more at [https://developers.google.com/apps-script](https://developers.google.com/apps-script). * Advanced: Apps Script's advanced services @@ -9,7 +8,6 @@ more at * Calendar: Create a vacation calendar * Docs: Cursor inspector and translate add-ons * Gmail: Sending mail and mailmerge +* Forms: Forms notification add-on * Sheets: Sheets add-ons and custom functions * Slides: Sidebar and translate add-ons - -[![Analytics](https://ga-beacon.appspot.com/UA-42085206-1/google-apps-script-samples)](https://github.com/igrigorik/ga-beacon) diff --git a/advanced/admin-sdk.gs b/advanced/adminSDK.gs similarity index 100% rename from advanced/admin-sdk.gs rename to advanced/adminSDK.gs diff --git a/advanced/apps-activity.gs b/advanced/appsActivity.gs similarity index 100% rename from advanced/apps-activity.gs rename to advanced/appsActivity.gs diff --git a/advanced/fusion-tables.gs b/advanced/fusionTables.gs similarity index 100% rename from advanced/fusion-tables.gs rename to advanced/fusionTables.gs diff --git a/advanced/google-plus.gs b/advanced/googlePlus.gs similarity index 100% rename from advanced/google-plus.gs rename to advanced/googlePlus.gs diff --git a/advanced/google-plus-domains.gs b/advanced/googlePlusDomains.gs similarity index 100% rename from advanced/google-plus-domains.gs rename to advanced/googlePlusDomains.gs diff --git a/advanced/shopping-content.gs b/advanced/shoppingContent.gs similarity index 100% rename from advanced/shopping-content.gs rename to advanced/shoppingContent.gs diff --git a/advanced/tag-manager.gs b/advanced/tagManager.gs similarity index 100% rename from advanced/tag-manager.gs rename to advanced/tagManager.gs diff --git a/advanced/url-shortener.gs b/advanced/urlShortener.gs similarity index 100% rename from advanced/url-shortener.gs rename to advanced/urlShortener.gs diff --git a/advanced/youtube-analytics.gs b/advanced/youtubeAnalytics.gs similarity index 100% rename from advanced/youtube-analytics.gs rename to advanced/youtubeAnalytics.gs diff --git a/advanced/youtube-content-id.gs b/advanced/youtubeContentId.gs similarity index 100% rename from advanced/youtube-content-id.gs rename to advanced/youtubeContentId.gs diff --git a/android/README.md b/android/README.md new file mode 100644 index 000000000..bd1e9774a --- /dev/null +++ b/android/README.md @@ -0,0 +1,5 @@ +# Apps Scripts for Android + +## [Converting Android Apps into Android Add-ons](https://developers.google.com/apps-script/add-ons/mobile/android) + +This sample describes how to convert an existing Android app into an Android add-on. diff --git a/calendar/README.md b/calendar/README.md new file mode 100644 index 000000000..9174ad519 --- /dev/null +++ b/calendar/README.md @@ -0,0 +1,5 @@ +# Apps Scripts for Google Calendar + +## [Vacation Calendar](https://developers.google.com/apps-script/articles/vacation-calendar) + +This tutorial allows a user in a domain to automatically populate a team vacation calendar. diff --git a/calendar/vacation_calendar.gs b/calendar/vacationCalendar/vacationCalendar.gs similarity index 100% rename from calendar/vacation_calendar.gs rename to calendar/vacationCalendar/vacationCalendar.gs diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..79542a2bc --- /dev/null +++ b/docs/README.md @@ -0,0 +1,9 @@ +# Google Docs Add-ons + +## Cursor Inspector + +This add-on allows you to inspect the current state of the cursor or selection within a document. The information is presented in a sidebar and updates automatically every few seconds. + +## Translate + +This add-on allows you to translate selected text from a set of source languages to a set of destination languages. diff --git a/docs/cursor_inspector/README.md b/docs/cursorInspector/README.md similarity index 100% rename from docs/cursor_inspector/README.md rename to docs/cursorInspector/README.md diff --git a/docs/cursor_inspector/Code.gs b/docs/cursorInspector/cursorInspector.gs similarity index 100% rename from docs/cursor_inspector/Code.gs rename to docs/cursorInspector/cursorInspector.gs diff --git a/docs/cursor_inspector/screenshot.png b/docs/cursorInspector/screenshot.png similarity index 100% rename from docs/cursor_inspector/screenshot.png rename to docs/cursorInspector/screenshot.png diff --git a/docs/cursor_inspector/Sidebar.css.html b/docs/cursorInspector/sidebar.css.html similarity index 100% rename from docs/cursor_inspector/Sidebar.css.html rename to docs/cursorInspector/sidebar.css.html diff --git a/docs/cursor_inspector/Sidebar.html b/docs/cursorInspector/sidebar.html similarity index 95% rename from docs/cursor_inspector/Sidebar.html rename to docs/cursorInspector/sidebar.html index 55590ce84..37d02f01f 100644 --- a/docs/cursor_inspector/Sidebar.html +++ b/docs/cursorInspector/sidebar.html @@ -1,4 +1,4 @@ - +
    Loading ...
    @@ -49,4 +49,4 @@
    - + diff --git a/docs/cursor_inspector/Sidebar.js.html b/docs/cursorInspector/sidebar.js.html similarity index 100% rename from docs/cursor_inspector/Sidebar.js.html rename to docs/cursorInspector/sidebar.js.html diff --git a/docs/translate/Sidebar.html b/docs/translate/Sidebar.html index 90c70c854..42128c873 100644 --- a/docs/translate/Sidebar.html +++ b/docs/translate/Sidebar.html @@ -10,7 +10,7 @@ - +