Index: chrome/common/extensions/docs/examples/apps/cycler/cycler.js |
diff --git a/chrome/common/extensions/docs/examples/apps/cycler/cycler.js b/chrome/common/extensions/docs/examples/apps/cycler/cycler.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..1a57bbddb31a446b28f70885d43662235e765768 |
--- /dev/null |
+++ b/chrome/common/extensions/docs/examples/apps/cycler/cycler.js |
@@ -0,0 +1,383 @@ |
+// Copyright (c) 2012 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+function $(criterion) { |
+ return document.querySelector(criterion); |
+} |
+ |
+var cyclerUI = new (function () { |
Jeffrey Yasskin
2012/08/09 19:27:04
Do you ever use 'cyclerUI'? I'm not a JS expert, b
clintstaley
2012/08/10 00:33:13
I'm fairly new to it myself, and am just imitating
|
+ |
+ /** |
+ * Note to reviewers: This is a (perhaps excessively?) fancy |
Jeffrey Yasskin
2012/08/09 19:27:04
Grammar-o: "This is a fancy these functions into .
clintstaley
2012/08/10 00:33:13
Done.
|
+ * these functions into the cyclerUI object wouldn't |
+ * group them so nicely, but would also avoid a lot of 'bind' calls. I'm |
+ * still sorting out JS design, and would value your opinion on this. |
Jeffrey Yasskin
2012/08/09 19:27:04
I'm not at all an expert on JS design, but it seem
clintstaley
2012/08/10 00:33:13
Yeah, this is what I was worried about, too. If y
|
+ * |
+ * Enum for different UI states, showing variously a capture page, |
+ * a page for choosing a playback, one for showing an already-chosen |
+ * playback, and one for indicating that no playback choices yet exist. |
+ * @enum {{set: !function()}} |
+ * @private |
Jeffrey Yasskin
2012/08/09 19:27:04
I think being a local variable in the object closu
clintstaley
2012/08/10 00:33:13
It surely does. I was thinking of the @private as
|
+ */ |
+ var EnableState_ = { |
+ capture: { |
+ set: |
+ function() { |
+ if (this.enableState != EnableState_.capture) { |
+ this.enableState = EnableState_.capture; |
+ this.enableTab_(this.captureTabLabel, this.captureTab); |
+ this.captureTab.hidden = false; |
+ this.playbackTab.hidden = true; |
+ } |
+ } |
+ }, |
+ choosePlayback: { |
+ set: |
+ function() { |
+ if (this.enableState != EnableState_.choosePlayback) { |
+ this.enableState = EnableState_.choosePlayback; |
+ this.enableTab_(this.playbackTabLabel, this.playbackTab); |
+ this.captureTab.hidden = true; |
+ this.playbackTab.hidden = false; |
+ this.noCaptures.hidden = true; |
+ this.yesCaptures.hidden = false; |
+ this.playbackDetails.hidden = true; |
+ } |
+ } |
+ }, |
+ showPlayback: { |
+ set: |
+ function() { |
+ if (this.enableState != EnableState_.showPlayback) { |
+ this.enableState = EnableState_.showPlayback; |
+ this.enableTab_(this.playbackTabLabel, this.playbackTab); |
+ this.captureTab.hidden = true; |
+ this.playbackTab.hidden = false; |
+ this.noCaptures.hidden = true; |
+ this.yesCaptures.hidden = false; |
+ this.playbackDetails.hidden = false; |
+ } |
+ } |
+ }, |
+ showNoCaptures: { |
+ set: |
+ function() { |
+ if (this.enableState != EnableState_.showNoCaptures) { |
+ this.enableState = EnableState_.showNoCaptures; |
+ this.enableTab_(this.playbackTabLabel, this.playbackTab); |
+ this.captureTab.hidden = true; |
+ this.playbackTab.hidden = false; |
+ this.noCaptures.hidden = false; |
+ this.yesCaptures.hidden = true; |
+ } |
+ } |
+ } |
+ }; |
+ |
+ // Members for all UI elements subject to programmatic adjustment. |
+ this.captureTab = $('#capture-tab'); |
+ this.captureTabLabel = $('#capture-tab-label'); |
+ this.captureName = $('#capture-name'); |
+ this.captureURLs = $('#capture-urls'); |
+ this.doCaptureButton = $('#do-capture'); |
+ |
+ this.playbackTab = $('#playback-tab'); |
+ this.noCaptures = $('#no-captures'); |
+ this.yesCaptures = $('#yes-captures'); |
+ |
+ this.playbackTabLabel = $('#playback-tab-label'); |
+ this.playbackName = $('#playback-name'); |
+ this.playbackDetails = $('#playback-details'); |
+ this.playbackURLs = $('#playback-urls'); |
+ this.playbackRepeats = $('#playback-repeats'); |
+ this.playbackExtension = $('#playback-extension'); |
+ this.doPlaybackButton = $('#do-playback'); |
+ this.doDeleteButton = $('#do-delete'); |
+ |
+ this.popupDialog = $('#popup'); |
+ this.popupContent = $('#popup-content'); |
+ this.doPopupDismiss = $('#do-popup-dismiss'); |
+ |
+ // currentCaptureName_ is most recently done capture, or most recently |
+ // selected stored capture. |
+ this.currentCaptureName_ = null; |
+ |
+ /** |
+ * Enable the tab indciated by |tabLabel| and |tab|, the HTML elements |
Jeffrey Yasskin
2012/08/09 19:27:04
sp: indciated
clintstaley
2012/08/10 00:33:13
Done.
|
+ * showing the selection-label for the tab, and the tab itself, respectively. |
+ * @param {!HTMLDivElement} tabLabel The <div> for the label of the |
+ * tab to enable. |
+ * @param {!HTMLDivElement} tab The <div> for the tab itself. |
+ * @private |
+ */ |
+ this.enableTab_ = function(tabLabel, tab) { |
+ var tabList = document.querySelectorAll('.tab'); |
+ var tabLabelList = document.querySelectorAll('.tab-label'); |
+ |
+ for (var i = 0; i < tabList.length; i++) |
+ tabList[i].hidden = tabList[i] != tab; |
+ |
+ for (var i = 0; i < tabLabelList.length; i++) |
+ if (tabLabelList[i] == tabLabel) { |
+ tabLabelList[i].classList.add('select'); |
+ } else { |
+ tabLabelList[i].classList.remove('select'); |
+ } |
+ } |
+ |
+ /** |
+ * Show an overlay with a message, a dismiss button with configurable |
+ * label, and an action to call upon dismissal. |
+ * @param {!string} content The message to display. |
+ * @param {!string} dismissLabel The label on the dismiss button. |
+ * @param {function()} action Additional action to take, if any, upon |
+ * dismissal. |
+ * @private |
+ */ |
+ this.showMessage_ = function(content, dismissLabel, action) { |
+ this.popupContent.innerText = content; |
+ this.doPopupDismiss.innerText = dismissLabel; |
+ this.popupDialog.hidden = false; |
+ if (action != null) |
+ doPopupDismiss.addEventListener('click', action); |
+ } |
+ |
+ /** |
+ * Default action for popup dismissal button, performed in addition to |
+ * any other actions that may be specified in showMessage_ call. |
+ * @private |
+ */ |
+ this.clearMessage_ = function() { |
+ this.popupDialog.hidden = true; |
+ } |
+ |
+ /* Mockup of capture saving system. Sets up a simple array of known |
+ * captures, and adjusts that array. This code will all be replaced |
+ * in the next CL. |
+ */ |
+ this.currentCaptures = ['alpha', 'beta', 'gamma']; |
+ |
+ /** |
+ * Save a capture from a specified cache directory on the browser side, |
+ * storing it in HTML side FileSystem storage. Update the capture choices |
+ * HTML select element. |
+ * @param {!string} name The name of the capture. |
+ * @param {!Array.<!string>} urlList List of URLs to capture. |
+ * @param {!string} directory The cache directory name. |
+ */ |
+ this.saveCapture = function(name, urlList, directory) { |
+ console.log('Saving capture ' + name + ' on ' + urlList + |
+ ' from browserside directory ' + directory); |
+ this.currentCaptures.push(name); |
+ } |
+ |
+ /** |
+ * Return a list of currently stored captures in the local FileSystem. |
+ * @return {Array.<!string>} Names of all the current captures. |
+ */ |
+ this.getCaptures = function() { |
+ return this.currentCaptures; |
+ } |
+ |
+ /** |
+ * Delete capture |name| from the local FileSystem, and update the |
Jeffrey Yasskin
2012/08/09 19:27:04
Does this actually delete from the filesystem?
clintstaley
2012/08/10 00:33:13
No, like the comment says above, it's a mockup for
|
+ * capture choices HTML select element. |
+ * @param {!string} name The name of the capture to delete. |
+ */ |
+ this.deleteCapture = function(name) { |
+ this.currentCaptures.splice(this.currentCaptures.indexOf(name), 1); |
+ this.updatePlaybackChoices_(); |
+ } |
+ |
+ /* End capture saving mockup */ |
+ |
+ /** |
+ * Do a capture using current data from the capture tab. Post an error |
+ * dialog if said data is incorrect or incomplete. Otherwise pass |
+ * control to the browser side. |
+ * @private |
+ */ |
+ this.doCapture_ = function() { |
+ var errors = []; |
+ var captureName = this.captureName.value.trim(); |
+ var urlList; |
+ |
+ urlList = this.captureURLs.value.split('\n'); |
+ if (captureName.length == 0) |
+ errors.push('Must give a capture name'); |
+ if (urlList.length == 0) |
+ errors.push('Must give at least one URL'); |
+ |
+ if (errors.length > 0) { |
+ this.showMessage_(errors.join('\n'), 'Ok'); |
+ } else { |
+ this.doCaptureButton.disabled = true; |
+ chrome.experimental.record.captureURLs(captureName, urlList, |
Jeffrey Yasskin
2012/08/09 19:27:04
Can captureURLs throw exceptions that need to be e
clintstaley
2012/08/10 00:33:13
Not across an extension API boundary that I know o
|
+ this.onCaptureDone.bind(this)); |
+ } |
+ } |
+ |
+ /** |
+ * Delete the capture with name |currentCaptureName_|. For this method |
+ * to be callable, there must be a selected currentCaptureName_. Change |
+ * the displayed HTML to show no captures if none exist now, or to show |
+ * none selected (since we just deleted the selected one). |
+ * @private |
+ */ |
+ this.doDelete_ = function() { |
+ this.deleteCapture(this.currentCaptureName_); |
+ this.currentCaptureName_ = null; |
+ this.updatePlaybackChoices_(); |
+ if (this.getCaptures().length == 0) { |
+ EnableState_.showNoCaptures.set.bind(this)(); |
+ } else { |
+ EnableState_.choosePlayback.set.bind(this)(); |
+ } |
+ } |
+ |
+ /** |
+ * Callback for completed (or possibly failed) capture. Post a message |
+ * box, either with errors or "Success!" message. Save the capture cache |
+ * directory in the event of success. |
+ * @param {!Array.<string>} errors List of errors that occured |
+ * during capture, if any. |
+ * @param {!string} cacheDirectory Cache directory name for successful |
+ * capture. |
+ * @private |
+ */ |
+ this.onCaptureDone = function(errors, cacheDirectory) { |
+ this.doCaptureButton.disabled = false; |
+ |
+ if (errors.length > 0) { |
+ this.showMessage_(errors.join('\n'), 'Ok'); |
+ } else { |
+ this.showMessage_('Success!', 'Ok'); |
+ this.currentCaptureName_ = this.captureName.value.trim(); |
+ this.saveCapture(this.currentCaptureName_, cacheDirectory); |
+ this.updatePlaybackChoices_(); |
+ } |
+ } |
+ |
+ /** |
+ * Display the playback tab, showing either its "no captures", "choose |
+ * a capture" or "display chosen capture" form, depending on the state |
+ * of existing captures and |currentCaptureName_|. |
+ * @private |
+ */ |
+ this.displayPlayback = function() { |
+ if (this.getCaptures().length == 0) { |
+ EnableState_.showNoCaptures.set.bind(this)(); |
+ } else if (this.currentCaptureName_ == null) { |
+ EnableState_.choosePlayback.set.bind(this)(); |
+ } else { |
+ EnableState_.showPlayback.set.bind(this)(); |
+ } |
+ } |
+ |
+ /** |
+ * Utility function to refresh the selection list of captures that may |
+ * be chosen for playback. Show all current captures, and also a |
+ * "Choose a capture" default text if no capture is currently selected. |
+ * @private |
+ */ |
+ this.updatePlaybackChoices_ = function() { |
+ var captureNames = this.getCaptures(); |
+ var options = this.playbackName.options; |
+ var nextIndex = 0; |
+ |
+ options.length = 0; |
+ |
+ if (this.currentCaptureName_ == null) { |
+ options.add(new Option('Choose a capture', null)); |
+ options.selectedIndex = nextIndex++; |
+ } |
+ for (var i = 0; i < captureNames.length; i++) { |
+ options.add(new Option(captureNames[i], captureNames[i])); |
+ if (captureNames[i] == this.currentCaptureName_) { |
+ options.selectedIndex = nextIndex; |
+ } |
+ nextIndex++; |
+ } |
+ } |
+ |
+ /** |
+ * Event callback for selection of a capture to play back. Save the |
+ * choice in |currentCaptureName_|. Update the selection list because the |
+ * default reminder message is no longer needed when a capture is chosen. |
+ * Change playback tab to show-chosen-capture mode. |
+ */ |
+ this.selectPlaybackName = function() { |
+ this.currentCaptureName_ = this.playbackName.value; |
+ this.updatePlaybackChoices_(); |
+ EnableState_.showPlayback.set.bind(this)(); |
+ } |
+ |
+ /** |
+ * Event callback for pressing the playback button, which button is only |
+ * enabled if a capture has been chosen for playback. Check for errors |
+ * on playback page, and either display a message with such errors, or |
+ * call the extenion api to initiate a playback. |
+ */ |
+ this.doPlayback_ = function() { |
+ var extensionPath = this.playbackExtension.value; |
+ var repeatCount = parseInt(this.playbackRepeats.value); |
+ var errors = []; |
+ |
+ // Check local errors |
+ if (isNaN(repeatCount)) { |
+ errors.push('Enter a number for repeat count'); |
+ } else if (repeatCount < 1 || repeatCount > 100) { |
+ errors.push('Repeat count must be between 1 and 100'); |
+ } |
+ |
+ if (errors.length > 0) { |
+ this.showMessage_(errors.join('\n'), 'Ok'); |
+ } else { |
+ this.doPlaybackButton.disabled = true; |
+ chrome.experimental.record.replayURLs( |
+ this.currentCaptureName_, |
+ repeatCount, |
+ {'extensionPath': extensionPath}, |
+ this.onPlaybackDone.bind(this)); |
+ } |
+ } |
+ |
+ /** |
+ * Extension API calls this back when a playback is done. |
+ * @param {!number} runtime Number of ms it took to run the playback. |
+ * @param {!string} stats Statistics from the playback. |
+ * @param {!Array.<!string>} errors List of error messages, if any, |
+ * resuting from the playback. |
+ */ |
+ this.onPlaybackDone = function(runTime, stats, errors) { |
+ this.doPlaybackButton.disabled = false; |
+ |
+ if (errors.length > 0) { |
+ this.showMessage_(errors.join('\n'), 'Ok'); |
+ } else { |
+ this.showMessage_('Test took ' + runTime + 'mS :\n' + stats, 'Ok'); |
Jeffrey Yasskin
2012/08/09 19:27:04
I understand this is just the initial version, but
clintstaley
2012/08/10 00:33:13
The design of the C++ side of this is a whole noth
|
+ } |
+ } |
+ |
+ // Set up listeners on all buttons. |
+ this.doCaptureButton.addEventListener('click', this.doCapture_.bind(this)); |
+ this.doPlaybackButton.addEventListener('click', this.doPlayback_.bind(this)); |
+ this.doDeleteButton.addEventListener('click', this.doDelete_.bind(this)); |
+ this.doPopupDismiss.addEventListener('click', this.clearMessage_.bind(this)); |
+ |
+ // Set up listeners on tab labels. |
+ this.captureTabLabel.addEventListener('click', |
+ EnableState_.capture.set.bind(this)); |
+ this.playbackTabLabel.addEventListener('click', |
+ this.displayPlayback.bind(this)); |
+ |
+ // Set up initial selection list for existing captures, and selection |
+ // event listener. |
+ this.updatePlaybackChoices_(); |
+ this.playbackName.addEventListener('change', |
+ this.selectPlaybackName.bind(this)); |
+ |
+ // Start with capture tab displayed. |
+ EnableState_.capture.set.bind(this)(); |
+})(); |