Created
March 26, 2014 00:12
-
-
Save robcee/9774292 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* This Source Code Form is subject to the terms of the Mozilla Public | |
* License, v. 2.0. If a copy of the MPL was not distributed with this file, | |
* You can obtain one at http://mozilla.org/MPL/2.0/. */ | |
var __SCRATCHPAD__ = !(typeof(window) == "undefined"); | |
if (__SCRATCHPAD__ && (typeof(window.gBrowser) == "undefined")) { | |
throw new Error("Must be run in a browser scratchpad."); | |
} | |
// If we're developing in scratchpad, shutdown the previous run | |
// before continuing. | |
if (__SCRATCHPAD__ && (typeof(shutdown) != "undefined")) { | |
shutdown(); | |
} | |
// should be consts but redefining makes this a pain. | |
var { interfaces: Ci, utils: Cu } = Components; | |
Cu.import("resource://gre/modules/Services.jsm"); | |
// Pref stores the auth token we were given by gist. | |
const kAuthTokenPref = "devtools.scratchpad.gist.authtoken"; | |
// Pref stores the user we authenticated as. | |
const kUserPref = "devtools.scratchpad.gist.userid"; | |
const kAuthNote = "Scratchpad"; | |
const kLabelStyle = "margin-top: 4px;"; | |
function strPref(key) Services.prefs.prefHasUserValue(key) ? Services.prefs.getCharPref(key) : null; | |
function ScratchpadGist(win) | |
{ | |
this.win = win; | |
this.doc = win.document; | |
this.updateUI = this.updateUI.bind(this); | |
Services.obs.addObserver(this.updateUI, "sp-gist-auth", false); | |
this.addCommands(); | |
this.addMenu(); | |
this.addToolbar(); | |
this.addToolbarButtons(); | |
this.updateUI(); | |
} | |
ScratchpadGist.prototype = { | |
get authtoken() strPref(kAuthTokenPref), | |
get authUser() strPref(kUserPref), | |
get menu() this.doc.getElementById("sp-gist-menu"), | |
get toolbar() this.doc.getElementById("sp-gist-toolbar"), | |
get toolbarLink() this.doc.getElementById("sp-gist-link"), | |
get nbox() this.doc.getElementById("scratchpad-notificationbox"), | |
get commandset() this.doc.getElementById("sp-gist-commands"), | |
get historyPopup() this.doc.getElementById("sp-gist-history"), | |
get fileButton() this.doc.getElementById("sp-gist-file"), | |
get filesPopup() this.doc.getElementById("sp-gist-files"), | |
destroy: function() { | |
Services.obs.removeObserver(this.updateUI, "sp-gist-auth", false); | |
if (this._authListener) { | |
let item = this.doc.getElementById("sp-gist-auth"); | |
item.removeEventListener("command", this._authListener, false); | |
delete this._authListener; | |
} | |
this.menu.remove(); | |
this.commandset.remove(); | |
// remove toolbar buttons and things | |
this.doc.getElementById("sp-gist-label").remove(); | |
this.toolbarLink.remove(); | |
this.doc.getElementById("sp-gist-springy").remove(); | |
this.fileButton.remove(); | |
this.doc.getElementById("sp-gist-refresh").remove(); | |
this.doc.getElementById("sp-gist-history-button").remove(); | |
this.doc.getElementById("sp-gist-fork").remove(); | |
this.doc.getElementById("sp-gist-post").remove(); | |
let notification = this.nbox.getNotificationWithValue("gist-notification"); | |
if (notification) { | |
this.nbox.removeNotification(notification); | |
} | |
if (this.toolbar) | |
this.toolbar.remove(); | |
let tbar = this.doc.getElementById("sp-toolbar"); | |
tbar.remove(); | |
this.doc.getElementById("sp-gist-toolbox").remove(); | |
this.doc.documentElement.insertBefore(tbar, this.nbox); | |
}, | |
/** | |
* A few dom helpers... | |
*/ | |
clear: function(elt) { | |
while (elt.hasChildNodes()) { | |
elt.removeChild(elt.firstChild); | |
} | |
}, | |
addChild: function(parent, tag, attributes) { | |
let element = this.doc.createElement(tag); | |
for (let item of Object.getOwnPropertyNames(attributes)) { | |
element.setAttribute(item, attributes[item]); | |
} | |
parent.appendChild(element); | |
return element; | |
}, | |
addCommand: function(options) { | |
let command = this.doc.createElement("command"); | |
command.setAttribute("id", options.id); | |
if (options.label) { | |
command.setAttribute("label", options.label); | |
} | |
command.addEventListener("command", options.handler, true); | |
this.commandset.appendChild(command); | |
}, | |
addCommands: function() { | |
let commands = this.doc.createElement("commandset"); | |
commands.setAttribute("id", "sp-gist-commands"); | |
this.doc.documentElement.appendChild(commands); | |
this.addCommand({ | |
id: "sp-gist-cmd-signin", | |
label: "Sign In", | |
handler: () => this.signIn() | |
}); | |
this.addCommand({ | |
id: "sp-gist-cmd-signout", | |
label: "Sign Out", | |
handler: () => this.signOut() | |
}); | |
this.addCommand({ | |
id: "sp-gist-cmd-attach", | |
label: "Attach to Gist...", | |
handler: () => this.attach() | |
}); | |
this.addCommand({ | |
id: "sp-gist-cmd-detach", | |
label: "Detach from Gist", | |
handler: () => this.attached(null) | |
}); | |
this.addCommand({ | |
id: "sp-gist-cmd-create-private", | |
label: "Create Private Gist", | |
handler: () => this.create(false) | |
}); | |
this.addCommand({ | |
id: "sp-gist-cmd-create-public", | |
label: "Create Public Gist", | |
handler: () => this.create(true) | |
}); | |
this.addCommand({ | |
id: "sp-gist-cmd-refresh", | |
label: "Load Latest", | |
handler: () => this.refresh() | |
}); | |
this.addCommand({ | |
id: "sp-gist-cmd-update", | |
label: "Post", | |
handler: () => this.update() | |
}); | |
this.addCommand({ | |
id: "sp-gist-cmd-fork", | |
label: "Fork", | |
handler: () => this.fork() | |
}); | |
}, | |
addMenu: function() { | |
let doc = this.doc; | |
let menubar = doc.getElementById("sp-menubar"); | |
if (!menubar) { | |
return; | |
} | |
let menu = doc.createElement("menu"); | |
menu.setAttribute("id", "sp-gist-menu"); | |
menu.setAttribute("label", "Gist"); | |
let popup = this.addChild(menu, "menupopup", { id: "sp-gist-popup" }); | |
this.addChild(popup, "menuitem", { id: "sp-gist-auth" }); | |
this.addChild(popup, "menuseparator", { class: "sp-gist-authed" }); | |
this.addChild(popup, "menuitem", { | |
id: "sp-gist-attach", | |
class: "sp-gist-authed" | |
}); | |
this.addChild(popup, "menuitem", { | |
command: "sp-gist-cmd-create-public", | |
class: "sp-gist-authed sp-gist-detached" | |
}); | |
this.addChild(popup, "menuitem", { | |
command: "sp-gist-cmd-create-private", | |
class: "sp-gist-authed sp-gist-detached" | |
}); | |
this.addChild(popup, "menuseparator", { class: "sp-gist-authed sp-gist-attached" }); | |
this.addChild(popup, "menuitem", { | |
command: "sp-gist-cmd-refresh", | |
class: "sp-gist-authed sp-gist-attached" | |
}); | |
this.addChild(popup, "menuitem", { | |
command: "sp-gist-cmd-fork", | |
class: "sp-gist-authed sp-gist-attached sp-gist-other" | |
}); | |
this.addChild(popup, "menuitem", { | |
command: "sp-gist-cmd-update", | |
class: "sp-gist-authed sp-gist-owned" | |
}); | |
let help = doc.getElementById("sp-help-menu"); | |
menubar.insertBefore(menu, help); | |
}, | |
addToolbar: function() { | |
let sp_toolbar = this.doc.getElementById("sp-toolbar"); | |
let toolbox = this.doc.createElement("toolbox"); | |
toolbox.id = "sp-gist-toolbox"; | |
toolbox.class = "devtools-toolbar"; | |
this.doc.documentElement.insertBefore(toolbox, this.nbox); | |
sp_toolbar.remove(); | |
toolbox.appendChild(sp_toolbar); | |
this.addChild(toolbox, "toolbar", { | |
id: "sp-gist-toolbar", | |
class: "devtools-toolbar" | |
}); | |
}, | |
addToolbarButtons: function() { | |
let toolbar = this.toolbar; | |
this.addChild(toolbar, "label", { | |
style: kLabelStyle, | |
id: "sp-gist-label", | |
value: "Gist:" | |
}); | |
this.addChild(toolbar, "label", { | |
style: kLabelStyle, | |
id: "sp-gist-link", | |
class: "text-link", | |
}); | |
this.addChild(toolbar, "toolbarspring", {id: "sp-gist-springy"}); | |
let fileSelector = this.addChild(toolbar, "toolbarbutton", { | |
id: "sp-gist-file", | |
class: "devtools-toolbarbutton sp-gist-multifile", | |
type: "menu" | |
}); | |
this.addChild(fileSelector, "menupopup", { | |
id: "sp-gist-files" | |
}); | |
this.addChild(toolbar, "toolbarbutton", { | |
command: "sp-gist-cmd-refresh", | |
id: "sp-gist-refresh", | |
class: "devtools-toolbarbutton" | |
}); | |
let history = this.addChild(toolbar, "toolbarbutton", { | |
label: "History", | |
class: "devtools-toolbarbutton", | |
id: "sp-gist-history-button", | |
type: "menu" | |
}); | |
this.addChild(history, "menupopup", { | |
id: "sp-gist-history" | |
}); | |
this.addChild(toolbar, "toolbarbutton", { | |
id: "sp-gist-fork", | |
command: "sp-gist-cmd-fork", | |
class: "devtools-toolbarbutton sp-gist-other" | |
}); | |
this.addChild(toolbar, "toolbarbutton", { | |
id: "sp-gist-post", | |
command: "sp-gist-cmd-update", | |
class: "devtools-toolbarbutton sp-gist-owned" | |
}); | |
}, | |
updateUI: function() { | |
let auth = this.doc.getElementById("sp-gist-auth"); | |
auth.setAttribute("command", this.authtoken ? "sp-gist-cmd-signout" : "sp-gist-cmd-signin"); | |
let attach = this.doc.getElementById("sp-gist-attach"); | |
attach.setAttribute("command", this.attachedGist ? "sp-gist-cmd-detach" : "sp-gist-cmd-attach"); | |
let authed = !!this.authtoken; | |
let attached = !!this.attachedGist; | |
let own = attached && this.attachedGist.user && (this.attachedGist.user.id == this.authUser); | |
let multifile = this.attachedGist && Object.getOwnPropertyNames(this.attachedGist.files).length > 1; | |
// Update the visibility of the toolbar buttons and menu items. | |
// They have a set of class names which correspond to state. A | |
// given item is hidden if any of its requirements are not met. | |
let items = this.doc.querySelectorAll("#sp-gist-label #sp-gist-post #sp-gist-fork #sp-gist-history-button #sp-gist-refresh #sp-gist-file #sp-gist-menu menuitem, #sp-gist-menu menuseparator"); | |
for (let item of items) { | |
if ((item.classList.contains("sp-gist-authed") && !authed) || | |
(item.classList.contains("sp-gist-attached") && !attached) || | |
(item.classList.contains("sp-gist-detached") && attached) || | |
(item.classList.contains("sp-gist-owned") && !own) || | |
(item.classList.contains("sp-gist-other") && own) || | |
(item.classList.contains("sp-gist-multifile") && !multifile)) | |
item.setAttribute("hidden", "true"); | |
else | |
item.removeAttribute("hidden"); | |
} | |
if (attached) { | |
// Update the toolbar and label | |
this.toolbarLink.setAttribute("href", this.attachedGist.html_url); | |
this.toolbarLink.setAttribute("value", this.attachedGist.html_url); | |
// Update the history popup from the attached gist... | |
this.clear(this.historyPopup); | |
this.attachedGist.history.forEach(function(item) { | |
let menuitem = this.addChild(this.historyPopup, "menuitem", { | |
label: item.version.substr(0, 6) + " " + item.user.login, | |
}); | |
menuitem.addEventListener("command", function() { | |
this.refresh(item.version); | |
}.bind(this), true); | |
}.bind(this)); | |
this.fileButton.setAttribute("label", this.attachedFilename); | |
// Update the file popup. | |
this.clear(this.filesPopup); | |
Object.getOwnPropertyNames(this.attachedGist.files).forEach(function(name) { | |
let item = this.attachedGist.files[name]; | |
let menuitem = this.addChild(this.filesPopup, "menuitem", { | |
label: item.filename, | |
}); | |
menuitem.addEventListener("command", function() { | |
this.fileButton.setAttribute("label", name); | |
this.attachedFilename = name; | |
this.loadFile(this.attachedGist, item); | |
}.bind(this)); | |
}.bind(this)); | |
} | |
}, | |
/** | |
* Issue a github API request. | |
*/ | |
request: function(options) { | |
let xhr = new this.win.XMLHttpRequest(); | |
xhr.mozBackgroundRequest = true; | |
xhr.onreadystatechange = () => { | |
if (xhr.readyState != 4) | |
return; | |
if (xhr.status >= 200 && xhr.status < 400) { | |
if (options.success) options.success(JSON.parse(xhr.responseText)); | |
} else { | |
if (typeof(options.err) == "function") { | |
options.err(xhr); | |
} else { | |
let label; | |
try { | |
let response = JSON.parse(xhr.responseText); | |
let prefix = typeof(options.err) == "string" ? options.err : "The request returned an error: "; | |
label = prefix + response.message + "."; | |
} catch(ex) { | |
label = "Request could not be completed."; | |
} | |
this.notify(this.nbox.PRIORITY_CRITICAL_HIGH, label); | |
} | |
} | |
}; | |
xhr.open(options.method || "GET", | |
"https://api.github.com" + options.path, | |
true, null, null); | |
xhr.setRequestHeader("Authorization", options.auth || "token " + this.authtoken); | |
xhr.send(options.args ? JSON.stringify(options.args) : ""); | |
}, | |
/** | |
* Notify the user with the notification box. | |
*/ | |
notify: function(priority, label, buttons) { | |
let notification = this.nbox.getNotificationWithValue("gist-notification"); | |
if (notification) { | |
this.nbox.removeNotification(notification); | |
} | |
this.nbox.appendNotification( | |
label, "gist-notification", null, priority, buttons, null | |
); | |
}, | |
/** | |
* Show the signin dialog and start the authentication process. | |
*/ | |
signIn: function() { | |
let username = {value:null}; | |
let password = {value:null}; | |
let check = {value:false}; | |
Services.prompt.promptUsernameAndPassword(null, "GitHub Credentials", "Enter your github username and password.", | |
username, password, "", check); | |
let auth = "Basic " + this.win.btoa(username.value + ":" + password.value); | |
this.findAuth(auth); | |
}, | |
/** | |
* Try to find an existing authorization for this application. If one is | |
* not found, this method will call createAuth() to create one. | |
*/ | |
findAuth: function(auth) { | |
// Find an existing Scratchpad Gist authorization. | |
this.request({ | |
method: "GET", | |
path: "/authorizations", | |
err: "Couldn't log in: ", | |
auth: auth, | |
success: (response) => { | |
for (let authorization of response) { | |
if (authorization.app.name == kAuthNote + " (API)") { | |
this.authorized(authorization); | |
return; | |
} | |
} | |
this.createAuth(auth); | |
} | |
}); | |
}, | |
/** | |
* Create a gist authorization for this application. | |
*/ | |
createAuth: function(auth) { | |
this.request({ | |
method: "POST", | |
path: "/authorizations", | |
auth: auth, | |
error: "Couldn't log in: ", | |
args: { | |
scopes: ["gist"], | |
note: kAuthNote, | |
note_url: null | |
}, | |
success: (response) => { | |
this.authorized(response); | |
} | |
}); | |
}, | |
/** | |
* Now that we've gotten an authorization token, request user | |
* information so we know who we are. | |
*/ | |
authorized: function(authorization) { | |
// Fetch information about the authorized user. | |
this.request({ | |
path: "/user", | |
auth: "token " + authorization.token, | |
success: (response) => { | |
// Done logging in, set the auth preferences. | |
Services.prefs.setCharPref(kAuthTokenPref, authorization.token); | |
Services.prefs.setCharPref(kUserPref, response.id); | |
this.notify(this.nbox.PRIORITY_INFO_HIGH, "Logged in as " + response.name + "."); | |
// Notify other scratchpad windows that we're logged in. This will | |
// refresh the UI. | |
Services.obs.notifyObservers(null, "sp-gist-auth", ""); | |
} | |
}); | |
}, | |
/** | |
* Forget the currently logged-in user. | |
*/ | |
signOut: function() { | |
Services.prefs.clearUserPref(kAuthTokenPref); | |
// Notify other scratchpad windows that we're logged in. This will | |
// refresh the UI. | |
Services.obs.notifyObservers(null, "sp-gist-auth", ""); | |
}, | |
/** | |
* Prompt the user to attach to a gist. | |
*/ | |
attach: function() { | |
let entry = {value: null}; | |
let check = {value:false}; | |
Services.prompt.prompt( | |
this.win, "Attach to Gist", "Enter the Gist ID", entry, "", check | |
); | |
let id = entry.value; | |
if (id === null) | |
return; | |
if (id.contains("gist.github.com")) { | |
// parse URL | |
let gistRE = new RegExp('gist.github.com/(.*/)(.*)'); | |
let matches = id.match(gistRE); | |
if (matches) { | |
id = matches[2]; | |
} | |
} | |
this.request({ | |
path: "/gists/" + id, | |
err: "Could not attach to the Gist: ", | |
success: function(response) { | |
this.attached(response); | |
}.bind(this), | |
error: "Couldn't find gist." | |
}); | |
}, | |
/** | |
* Fork the currently-attached gist. | |
*/ | |
fork: function() { | |
this.request({ | |
method: "POST", | |
path: "/gists/" + this.attachedGist.id + "/fork", | |
success: function(response) { | |
this.attached(response); | |
}.bind(this), | |
}); | |
}, | |
/** | |
* Fetch the currently-attached gist from the server and load | |
* it in to the scratchpad. | |
* | |
* @param string version optional | |
* Optionally specifies the specific version to fetch. | |
*/ | |
refresh: function(version) { | |
let path = "/gists/" + this.attachedGist.id; | |
if (version) { | |
path += "/" + version; | |
} | |
this.request({ | |
method: "GET", | |
path: path, | |
success: function(response) { | |
this.load(response); | |
}.bind(this) | |
}); | |
}, | |
/** | |
* Post the current contents of the scratchpad as a new gist. | |
* Attaches to the new gist. | |
* | |
* @param boolean pub | |
* True to create a public gist. | |
*/ | |
create: function(pub) { | |
this.request({ | |
method: "POST", | |
path: "/gists", | |
args: { | |
description: null, | |
public: pub, | |
files: this.getFile() | |
}, | |
success: function(response) { | |
this.attached(response); | |
}.bind(this) | |
}); | |
}, | |
/** | |
* Upload the contents of the scratchpad to the currently-attached gist. | |
*/ | |
update: function() { | |
this.request({ | |
method: "PATCH", | |
path: "/gists/" + this.attachedGist.id, | |
args: { | |
description: null, | |
files: this.getFile(), | |
}, | |
success: function(response) { | |
this.attached(response); | |
}.bind(this) | |
}); | |
}, | |
/** | |
* Return a files object for the current object, as needed by | |
* gist API requests. | |
*/ | |
getFile: function() { | |
let files = {}; | |
let filename = "scratchpad.js"; | |
if (this.attachedFilename) { | |
filename = this.attachedFilename; | |
} else { | |
let scratchpad = this.win.Scratchpad; | |
if (scratchpad.filename) { | |
filename = scratchpad.filename; | |
let lastSep = Math.max(filename.lastIndexOf("/"), filename.lastIndexOf("\\")); | |
if (lastSep > -1) { | |
filename = filename.substring(lastSep + 1); | |
} | |
} | |
} | |
files[filename] = { | |
content: this.win.Scratchpad.getText() | |
}; | |
return files; | |
}, | |
/** | |
* Called when we've attached to a gist. | |
*/ | |
attached: function(gist) { | |
this.attachedGist = gist; | |
if (!this.attachedFile) { | |
this.attachedFile = Object.getOwnPropertyNames(gist.files)[0]; | |
} | |
this.updateUI(); | |
}, | |
/** | |
* Load the contents of the given gist into the scratchpad. | |
* | |
* @param object gist | |
* The gist as returned by an API request. | |
*/ | |
load: function(gist) { | |
// Try to find the currently-selected subfile. | |
for (let i in gist.files) { | |
if (gist.files[i].filename == this.attachedFilename) { | |
this.loadFile(gist, gist.files[i]); | |
return; | |
} | |
} | |
// The attached filename was either empty or is now missing. | |
// Attach to the first one. | |
this.attachedFilename = Object.getOwnPropertyNames(gist.files)[0]; | |
this.loadFile(gist, gist.files[this.attachedFilename]); | |
}, | |
/** | |
* Load a specific file's contents from the gist. | |
*/ | |
loadFile: function(gist, file) { | |
this.win.Scratchpad.setText(file.content); | |
this.win.Scratchpad.setFilename(file.filename); | |
}, | |
}; | |
function attachWindow(win) { | |
if (win.Scratchpad && win.document.getElementById("scratchpad-notificationbox")) { | |
win.ScratchpadGist = new ScratchpadGist(win); | |
} | |
} | |
var WindowListener = { | |
onOpenWindow: function(win) { | |
// Wait for the window to finish loading | |
// XXX redefining "win" I don't even... | |
let win = win.QueryInterface(Ci.nsIInterfaceRequestor) | |
.getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow); | |
win.addEventListener("load", function onLoad() { | |
win.removeEventListener("load", onLoad, false); | |
attachWindow(win); | |
}, false); | |
}, | |
onCloseWindow: function(win) { }, | |
onWindowTitleChange: function(win, title) { } | |
}; | |
function startup(data, reason) | |
{ | |
// Should set a type in scratchpad. | |
let e = Services.wm.getEnumerator("devtools:scratchpad"); | |
while (e.hasMoreElements()) { | |
attachWindow(e.getNext()); | |
} | |
Services.wm.addListener(WindowListener); | |
} | |
function shutdown(data, reason) | |
{ | |
// Should set a type in scratchpad. | |
let e = Services.wm.getEnumerator("devtools:scratchpad"); | |
while (e.hasMoreElements()) { | |
let win = e.getNext(); | |
if (win.ScratchpadGist) { | |
win.ScratchpadGist.destroy(); | |
delete win.ScratchpadGist; | |
} | |
let menu = win.document.getElementById("sp-gist-menu"); | |
if (menu) { | |
menu.remove(); | |
} | |
} | |
if (WindowListener) { | |
Services.wm.removeListener(WindowListener); | |
} | |
} | |
function install(data, reason) { } | |
function uninstall(data, reason) { } | |
// If running in the scratchpad, run startup manually. | |
if (__SCRATCHPAD__) { | |
startup(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment