├── LICENSE
├── README
├── api_bridge.js
├── asana.js
├── background.js
├── extension_server.js
├── icon128.png
├── icon16.png
├── icon48.png
├── jquery-1.7.1.min.js
├── jquery-ui-1.8.10.custom.min.js
├── manifest.json
├── nopicture.png
├── options.css
├── options.html
├── options.js
├── options_init.js
├── options_page.js
├── popup.css
├── popup.html
├── popup.js
├── server_model.js
├── sprite-retina.png
└── sprite.png
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2013-2015 Asana
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 |
--------------------------------------------------------------------------------
/README:
--------------------------------------------------------------------------------
1 | This is a free, open-source, sample application demonstrating use of the
2 | Asana API. It takes the form of a Chrome Extension that, when installed,
3 | integrates Asana into your web experience in the following ways:
4 |
5 | * Creates a button in your button-bar which, when clicked, pops up a
6 | QuickAdd window to create a new task associated with the current web page.
7 | You can click a button to populate the task name with the page title and
8 | the URL and current selected text in the notes.
9 |
10 | * Installs the special Asana ALT+A keyboard shortcut. When this key combo
11 | is pressed from any web page, it brings up the same popup.
12 | This functionality will operate on any window opened after the extension
13 | is loaded.
14 |
15 | See: http://developer.asana.com/
16 |
17 | Files of special interest:
18 |
19 | api_bridge.js:
20 | Handles generic communication with the API.
21 |
22 | server_model.js:
23 | Wraps specifics of individual API calls to return objects to calling code.
24 | This is not a real ORM, just the bare bones necessary to get a few
25 | simple things done.
26 |
27 | popup.html
28 | Source for the popup window, contains the top-level logic which drives
29 | most of the user-facing functionality.
30 |
31 | To install:
32 |
33 | 1. Download the code, e.g. `git clone git://github.com/Asana/Chrome-Extension-Example.git`
34 | 2. Navigate chrome to `chrome://extensions`
35 | 3. Check the `Developer mode` toggle
36 | 4. Click on `Load Unpacked Extension...`
37 | 5. Select the folder containing the extension
--------------------------------------------------------------------------------
/api_bridge.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Functionality to communicate with the Asana API. This should get loaded
3 | * in the "server" portion of the chrome extension because it will make
4 | * HTTP requests and needs cross-domain privileges.
5 | *
6 | * The bridge does not need to use an auth token to connect to
7 | * the API. Since it is a browser extension it can access the user's cookies
8 | * and can use them to authenticate to the API. This capability is specific
9 | * to browser extensions, and other types of applications would have to obtain
10 | * an auth token to communicate with the API.
11 | */
12 | Asana.ApiBridge = {
13 |
14 | /**
15 | * @type {String} Version of the Asana API to use.
16 | */
17 | API_VERSION: "1.0",
18 |
19 | /**
20 | * @type {Integer} How long an entry stays in the cache.
21 | */
22 | CACHE_TTL_MS: 15 * 60 * 1000,
23 |
24 | /**
25 | * @type {Boolean} Set to true on the server (background page), which will
26 | * actually make the API requests. Clients will just talk to the API
27 | * through the ExtensionServer.
28 | *
29 | */
30 | is_server: false,
31 |
32 | /**
33 | * @type {dict} Map from API path to cache entry for recent GET requests.
34 | * date {Date} When cache entry was last refreshed
35 | * response {*} Cached request.
36 | */
37 | _cache: {},
38 |
39 | /**
40 | * @param opt_options {dict} Options to use; if unspecified will be loaded.
41 | * @return {String} The base URL to use for API requests.
42 | */
43 | baseApiUrl: function(opt_options) {
44 | var options = opt_options || Asana.Options.loadOptions();
45 | return 'https://' + options.asana_host_port + '/api/' + this.API_VERSION;
46 | },
47 |
48 | /**
49 | * Make a request to the Asana API.
50 | *
51 | * @param http_method {String} HTTP request method to use (e.g. "POST")
52 | * @param path {String} Path to call.
53 | * @param params {dict} Parameters for API method; depends on method.
54 | * @param callback {Function(response: dict)} Callback on completion.
55 | * status {Integer} HTTP status code of response.
56 | * data {dict} Object representing response of API call, depends on
57 | * method. Only available if response was a 200.
58 | * error {String?} Error message, if there was a problem.
59 | * @param options {dict?}
60 | * miss_cache {Boolean} Do not check cache before requesting
61 | */
62 | request: function(http_method, path, params, callback, options) {
63 | var me = this;
64 | http_method = http_method.toUpperCase();
65 |
66 | // If we're not the server page, send a message to it to make the
67 | // API request.
68 | if (!me.is_server) {
69 | console.info("Client API Request", http_method, path, params);
70 | chrome.runtime.sendMessage({
71 | type: "api",
72 | method: http_method,
73 | path: path,
74 | params: params,
75 | options: options || {}
76 | }, callback);
77 | return;
78 | }
79 |
80 | console.info("Server API Request", http_method, path, params);
81 |
82 | // Serve from cache first.
83 | if (!options.miss_cache && http_method === "GET") {
84 | var data = me._readCache(path, new Date());
85 | if (data) {
86 | console.log("Serving request from cache", path);
87 | callback(data);
88 | return;
89 | }
90 | }
91 |
92 | // Be polite to Asana API and tell them who we are.
93 | var manifest = chrome.runtime.getManifest();
94 | var client_name = [
95 | "chrome-extension",
96 | chrome.i18n.getMessage("@@extension_id"),
97 | manifest.version,
98 | manifest.name
99 | ].join(":");
100 |
101 | var url = me.baseApiUrl() + path;
102 | var body_data;
103 | if (http_method === "PUT" || http_method === "POST") {
104 | // POST/PUT request, put params in body
105 | body_data = {
106 | data: params,
107 | options: { client_name: client_name }
108 | };
109 | } else {
110 | // GET/DELETE request, add params as URL parameters.
111 | var url_params = Asana.update({ opt_client_name: client_name }, params);
112 | url += "?" + $.param(url_params);
113 | }
114 |
115 | console.log("Making request to API", http_method, url);
116 |
117 | chrome.cookies.get({
118 | url: url,
119 | name: 'ticket'
120 | }, function(cookie) {
121 | if (!cookie) {
122 | callback({
123 | status: 401,
124 | error: "Not Authorized"
125 | });
126 | return;
127 | }
128 |
129 | // Note that any URL fetched here must be matched by a permission in
130 | // the manifest.json file!
131 | var attrs = {
132 | type: http_method,
133 | url: url,
134 | timeout: 30000, // 30 second timeout
135 | headers: {
136 | "X-Requested-With": "XMLHttpRequest",
137 | "X-Allow-Asana-Client": "1"
138 | },
139 | accept: "application/json",
140 | success: function(data, status, xhr) {
141 | if (http_method === "GET") {
142 | me._writeCache(path, data, new Date());
143 | }
144 | callback(data);
145 | },
146 | error: function(xhr, status, error) {
147 | // jQuery's ajax() method has some rather funky error-handling.
148 | // We try to accommodate that and normalize so that all types of
149 | // errors look the same.
150 | if (status === "error" && xhr.responseText) {
151 | var response;
152 | try {
153 | response = $.parseJSON(xhr.responseText);
154 | } catch (e) {
155 | response = {
156 | errors: [{message: "Could not parse response from server" }]
157 | };
158 | }
159 | callback(response);
160 | } else {
161 | callback({ errors: [{message: error || status }]});
162 | }
163 | },
164 | xhrFields: {
165 | withCredentials: true
166 | }
167 | };
168 | if (http_method === "POST" || http_method === "PUT") {
169 | attrs.data = JSON.stringify(body_data);
170 | attrs.dataType = "json";
171 | attrs.processData = false;
172 | attrs.contentType = "application/json";
173 | }
174 | $.ajax(attrs);
175 | });
176 | },
177 |
178 | _readCache: function(path, date) {
179 | var entry = this._cache[path];
180 | if (entry && entry.date >= date - this.CACHE_TTL_MS) {
181 | return entry.response;
182 | }
183 | return null;
184 | },
185 |
186 | _writeCache: function(path, response, date) {
187 | this._cache[path] = {
188 | response: response,
189 | date: date
190 | };
191 | }
192 | };
193 |
--------------------------------------------------------------------------------
/asana.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Define the top-level Asana namespace.
3 | */
4 | Asana = {
5 |
6 | // When popping up a window, the size given is for the content.
7 | // When resizing the same window, the size must include the chrome. Sigh.
8 | CHROME_TITLEBAR_HEIGHT: 24,
9 | // Natural dimensions of popup window. The Chrome popup window adds 10px
10 | // bottom padding, so we must add that as well when considering how tall
11 | // our popup window should be.
12 | POPUP_UI_HEIGHT: 310 + 10,
13 | POPUP_UI_WIDTH: 410,
14 | // Size of popup when expanded to include assignee list.
15 | POPUP_EXPANDED_UI_HEIGHT: 310 + 10 + 129,
16 |
17 | // If the modifier key is TAB, amount of time user has from pressing it
18 | // until they can press Q and still get the popup to show up.
19 | QUICK_ADD_WINDOW_MS: 5000
20 |
21 |
22 | };
23 |
24 | /**
25 | * Things borrowed from asana library.
26 | */
27 |
28 |
29 | Asana.update = function(to, from) {
30 | for (var k in from) {
31 | to[k] = from[k];
32 | }
33 | return to;
34 | };
35 |
36 | Asana.Node = {
37 |
38 | /**
39 | * Ensures that the bottom of the element is visible. If it is not then it
40 | * will be scrolled up enough to be visible.
41 | *
42 | * Note: this does not take account of the size of the window. That's ok for
43 | * now because the scrolling element is not the top-level element.
44 | */
45 | ensureBottomVisible: function(node) {
46 | var el = $(node);
47 | var pos = el.position();
48 | var element_from_point = document.elementFromPoint(
49 | pos.left, pos.top + el.height());
50 | if (element_from_point === null ||
51 | $(element_from_point).closest(node).size() === 0) {
52 | node.scrollIntoView(/*alignWithTop=*/ false);
53 | }
54 | }
55 |
56 | };
57 |
58 | if (!RegExp.escape) {
59 | // Taken from http://simonwillison.net/2006/Jan/20/escape/
60 | RegExp.escape = function(text, opt_do_not_escape_spaces) {
61 | if (opt_do_not_escape_spaces !== true) {
62 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); // nolint
63 | } else {
64 | // only difference is lack of escaping \s
65 | return text.replace(/[-[\]{}()*+?.,\\^$|#]/g, "\\$&"); // nolint
66 | }
67 | };
68 | }
69 |
--------------------------------------------------------------------------------
/background.js:
--------------------------------------------------------------------------------
1 | Asana.ExtensionServer.listen();
2 | Asana.ServerModel.startPrimingCache();
3 |
4 | // Modify referer header sent to typekit, to allow it to serve to us.
5 | // See http://stackoverflow.com/questions/12631853/google-chrome-extensions-with-typekit-fonts
6 | chrome.webRequest.onBeforeSendHeaders.addListener(function(details) {
7 | var requestHeaders = details.requestHeaders;
8 | for (var i = 0; i < requestHeaders.length; ++i) {
9 | if (requestHeaders[i].name.toLowerCase() === 'referer') {
10 | // The request was certainly not initiated by a Chrome extension...
11 | return;
12 | }
13 | }
14 | // Set Referer
15 | requestHeaders.push({
16 | name: 'referer',
17 | // Host must match the domain in our Typekit kit settings
18 | value: 'https://abkfopjdddhbjkiamjhkmogkcfedcnml'
19 | });
20 | return {
21 | requestHeaders: requestHeaders
22 | };
23 | }, {
24 | urls: ['*://use.typekit.net/*'],
25 | types: ['stylesheet', 'script']
26 | }, ['requestHeaders','blocking']);
27 |
--------------------------------------------------------------------------------
/extension_server.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The "server" portion of the chrome extension, which listens to events
3 | * from other clients such as the popup or per-page content windows.
4 | */
5 | Asana.ExtensionServer = {
6 |
7 | /**
8 | * Call from the background page: listen to chrome events and
9 | * requests from page clients, which can't make cross-domain requests.
10 | */
11 | listen: function() {
12 | var me = this;
13 |
14 | // Mark our Api Bridge as the server side (the one that actually makes
15 | // API requests to Asana vs. just forwarding them to the server window).
16 | Asana.ApiBridge.is_server = true;
17 |
18 | chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
19 | if (request.type === "api") {
20 | // Request to the API. Pass it on to the bridge.
21 | Asana.ApiBridge.request(
22 | request.method, request.path, request.params, sendResponse,
23 | request.options || {});
24 | return true; // will call sendResponse asynchronously
25 | }
26 | });
27 | }
28 |
29 | };
30 |
--------------------------------------------------------------------------------
/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Asana/Chrome-Extension-Example/05782e42f6b56f0fdd87cf31a32d33378703e8ec/icon128.png
--------------------------------------------------------------------------------
/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Asana/Chrome-Extension-Example/05782e42f6b56f0fdd87cf31a32d33378703e8ec/icon16.png
--------------------------------------------------------------------------------
/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Asana/Chrome-Extension-Example/05782e42f6b56f0fdd87cf31a32d33378703e8ec/icon48.png
--------------------------------------------------------------------------------
/jquery-1.7.1.min.js:
--------------------------------------------------------------------------------
1 | /*! jQuery v1.7.1 jquery.com | jquery.org/license */
2 | (function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cv(a){if(!ck[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cl||(cl=c.createElement("iframe"),cl.frameBorder=cl.width=cl.height=0),b.appendChild(cl);if(!cm||!cl.createElement)cm=(cl.contentWindow||cl.contentDocument).document,cm.write((c.compatMode==="CSS1Compat"?"":"")+"