Permalink
Browse files

initial commit

  • Loading branch information...
0 parents commit 957fba3c74a26b09febbe748c7b98dce1419937a dietrich ayala committed Feb 20, 2011
Showing with 388 additions and 0 deletions.
  1. +5 −0 README.md
  2. +29 −0 data/commands-autocomplete.html
  3. +46 −0 data/commands-autocomplete.js
  4. +2 −0 docs/main.md
  5. +132 −0 lib/commands.js
  6. +133 −0 lib/main.js
  7. +9 −0 package.json
  8. +32 −0 tests/test-main.js
@@ -0,0 +1,5 @@
+This is the cmd add-on. It contains:
+
+* A program (lib/main.js).
+* A few tests.
+* Some meager documentation.
@@ -0,0 +1,29 @@
+<html>
+ <head>
+ <style type="text/css">
+ body {
+ margin: 0;
+ padding: 0;
+ background-color: silver;
+ }
+ #cmdContainer {
+ padding: 0.5em;
+ }
+ #cmd {
+ color: #fff;
+ font-size: 2em;
+ }
+ .typed {
+ text-decoration: underline;
+ }
+ .completed {
+ }
+ </style>
+ </head>
+ <body>
+ <div id="cmdContainer">
+ <div id="cmd">
+ </div>
+ </div>
+ </body>
+</html>
@@ -0,0 +1,46 @@
+window.addEventListener("DOMContentLoaded", function() {
+ window.removeEventListener("DOMContentLoaded", arguments.callee, false);
+ var cmd = document.getElementById("cmd");
+
+ // handle messages from jetpack code
+ on("message", function(msg) {
+ msg = JSON.parse(msg);
+ if (msg.completed) {
+ if (msg.typed) {
+ cmd.innerHTML = generateUnderlined(msg.completed, msg.typed);
+ }
+ else {
+ cmd.innerHTML = msg.completed;
+ }
+ cmd.style.color = "white";
+ }
+ else if (msg.typed) {
+ cmd.innerHTML = msg.typed;
+ cmd.style.color = "red";
+ }
+ });
+
+ // sent init message to jetpack code
+ //postMessage(JSON.stringify({init: true}));
+}, false);
+
+function generateUnderlined(wholeString, subString) {
+ let startIndex = wholeString.toLowerCase().indexOf(subString.toLowerCase());
+ let endIndex = startIndex + subString.length;
+ // occurs at beginning
+ if (startIndex === 0) {
+ return "<span class='typed'>" + subString + "</span>" +
+ "<span>" + wholeString.substring(subString.length) + "</span>";
+ }
+ // occurs in middle
+ else if (endIndex != wholeString.length) {
+ return "<span>" + wholeString.substring(0, startIndex) + "</span>" +
+ "<span class='typed'>" + wholeString.substring(startIndex, startIndex + subString.length) + "</span>" +
+ "<span>" + wholeString.substring(endIndex) + "</span>";
+ }
+ // occurs at the end
+ else {
+ return "<span>" + wholeString.substring(0, startIndex - 1) + "</span>" +
+ "<span class='typed'>" + wholeString.substring(startIndex) + "</span>";
+ }
+}
@@ -0,0 +1,2 @@
+The main module is a program that creates a widget. When a user clicks on
+the widget, the program loads the mozilla.org website in a new tab.
@@ -0,0 +1,132 @@
+/*
+
+Module for searching available commands in the browser.
+
+Allows searching and execution of commands in Firefox, such as
+'Back', 'Zoom In', 'Reload'.
+
+Example that adds buttons to the add-on bar for every command,
+which trigger the command when clicked:
+
+const widget = require("widget");
+const commands = require("commands");
+commands.search("", function(command) {
+ widget.Widget({
+ label: command.alias,
+ content: command.alias,
+ onClick: command.execute
+ });
+});
+
+API
+
+The 'search' method takes two parameters. the first is a string of text
+to match against command names. For example, "zo" will match the commands
+"Zoom In" and "Zoom Out". The second parameter is a callback that'll be
+passed the command objects for all commands matching the search. The command
+object has an 'alias' property, which contains a label used in the application
+to refer to the command. The command object has an 'execute' method which
+will trigger the command.
+
+Implementation
+
+The module enumerates all <command> elements found in the active browser
+window, and then searches for any element in the browser that has a
+command attribute with that command's id, storing the item's label
+attribute value. The DOM traveral is only done once, on the first invocation
+of the 'search' method.
+
+TODO:
+* find all elements with @oncommand, and log their string and id as a command
+* for above, find keys, and walk back to consumers with strings, and log the
+ string + key id as a command
+
+*/
+
+let commands = {};
+let aliases = {};
+let aliasArray = [];
+let cached = false;
+
+function cacheCommands() {
+ let window = require("window-utils").activeBrowserWindow;
+ let document = window.document;
+ // harvest all basic commands
+ let els = document.querySelectorAll("command");
+ let len = els.length;
+ for (let i = 0; i < len; i++) {
+ let el = els[i];
+ let labels = [];
+ let subEls = document.querySelectorAll("*[command=\"" + el.id + "\"]");
+ if (subEls) {
+ for (let i = 0; i < subEls.length; i++) {
+ if (subEls[i].label) {
+ labels.push(subEls[i].label);
+ }
+ }
+ }
+ if (labels.length) {
+ commands[el.id] = el.id;
+ labels.forEach(function(label) {
+ aliases[label] = el.id;
+ aliasArray.push(label);
+ });
+ }
+ }
+
+ // harvest all elements with an oncommand attribute
+ els = document.querySelectorAll("*[oncommand]");
+ len = els.length;
+ for (let i = 0; i < len; i++) {
+ let el = els[i];
+ if (el.label && !aliases[el.label]) {
+ aliases[el.label] = el.id;
+ commands[el.id] = el.id;
+ aliasArray.push(el.label);
+ }
+ // ignore keys for now
+ /*
+ else {
+ let labels = [];
+ let subEls = document.querySelectorAll("*[command=\"" + el.id + "\"]");
+ if (subEls) {
+ for (let i = 0; i < subEls.length; i++) {
+ if (subEls[i].label) {
+ labels.push(subEls[i].label);
+ }
+ }
+ }
+ if (labels.length) {
+ commands[commandEl.id] = commandEl.id;
+ labels.forEach(function(label) aliases[label] = commandEl.id);
+ }
+ }
+ */
+ }
+
+ aliasArray.sort(function(a, b) a - b);
+}
+
+function command(id, alias) {
+ this.alias = alias;
+ this.execute = function() {
+ if (commands[id]) {
+ let document = require("window-utils").activeBrowserWindow.document
+ let el = document.getElementById(id);
+ if (el)
+ el.doCommand();
+ }
+ };
+}
+
+exports.search = function(text, callback) {
+ if (!cached) {
+ cacheCommands();
+ cached = true;
+ }
+
+ for (var i in aliasArray) {
+ if (!text.length || (new RegExp(text, "i")).test(aliasArray[i]))
+ callback(new command(aliases[aliasArray[i]], aliasArray[i]));
+ }
+};
@@ -0,0 +1,133 @@
+
+// TODO: support remembering last-executed command across restarts
+// TODO: support selection feedback ranking
+// TODO: fix shift-until-no-match
+
+// populate command list
+let cmds = {};
+let cmdAliases = [];
+require("commands").search("", function(command) {
+ cmds[command.alias] = command;
+ cmdAliases.push(command.alias);
+});
+
+let active = false;
+let typed = "";
+let matches = [];
+let matchIndex = 0;
+let lastExecuted = "";
+
+const panels = require("panel");
+let cmdWidget = panels.Panel({
+ label: "Cmds",
+ width: 350,
+ height: 54,
+ contentURL: require("self").data.url("commands-autocomplete.html"),
+ contentScriptFile: require("self").data.url("commands-autocomplete.js"),
+ contentScriptWhen: "ready",
+ onShow: function() {
+ active = true;
+ },
+ onHide: function() {
+ active = false;
+ },
+});
+
+function update(typed, completed) {
+ cmdWidget.postMessage(JSON.stringify({typed: typed, completed: completed}));
+}
+
+function hasModifier(e) e.altKey || e.ctrlKey || e.shiftKey || e.metaKey
+function isModifier(e) [e.altKey, e.ctrlKey, e.shiftKey, e.metaKey].indexOf(e.which) != -1
+function isIgnorable(e) {
+ switch(e.which) {
+ case 38: //up arrow
+ case 40: //down arrow
+ case 37: //left arrow
+ case 39: //right arrow
+ case 33: //page up
+ case 34: //page down
+ case 36: //home
+ case 35: //end
+ case 13: //enter
+ case 9: //tab
+ case 27: //esc
+ case 16: //shift
+ case 17: //ctrl
+ case 18: //alt
+ case 20: //caps lock
+ // we handle this for editing
+ //case 8: //backspace
+ // need to handle for editing also?
+ case 46: //delete
+ case 0:
+ return true;
+ break;
+ default:
+ return false;
+ }
+}
+
+function onKeyPress(e) {
+ // show ui on cmd+shift+;
+ if (!active && e.metaKey && e.which == 58 /*colon*/) {
+ update("", lastExecuted || "Type a command...");
+ cmdWidget.show();
+ }
+ // if user pressed return, attempt to execute command
+ else if (active && e.which == e.DOM_VK_RETURN && !hasModifier(e)) {
+ if (cmds[ matches[matchIndex] ]) {
+ cmds[matches[matchIndex]].execute();
+ lastExecuted = matches[matchIndex];
+ typed = "";
+ cmdWidget.hide();
+ }
+ }
+ // attempt to complete typed command
+ else if (active && !hasModifier(e) && !isModifier(e) && !isIgnorable(e)) {
+ // correct on backspace
+ if (e.which == 8)
+ typed = typed.substring(0, typed.length - 1);
+ // otherwise add typed character to buffer
+ else
+ typed += String.fromCharCode(e.which);
+
+ // search, and update UI
+ matches = findMatchingCommands(typed);
+ if (matches.length) {
+ update(typed, matches[0]);
+ matchIndex = 0;
+ }
+ else {
+ update(typed);
+ }
+ }
+ // tab -> shift to next result
+ // shift + tab -> shift to next result
+ else if (active && e.keyCode == e.DOM_VK_TAB) {
+ if (e.shiftKey && matchIndex)
+ update(typed, matches[--matchIndex]);
+ else if (matches.length > matchIndex)
+ update(typed, matches[++matchIndex]);
+ }
+}
+
+function findMatchingCommands(text) {
+ let match = null, count = cmdAliases.length, matches = [];
+ for (var i = 0; i < count; i++) {
+ if (cmdAliases[i].toLowerCase().indexOf(typed.toLowerCase()) != -1)
+ matches.push(cmdAliases[i]);
+ }
+ return matches;
+}
+
+// add shortcut listener to all current and future windows
+const wu = require("window-utils");
+new wu.WindowTracker({
+ onTrack: function (window) {
+ window.document.addEventListener("keypress", onKeyPress, false);
+ },
+ onUntrack: function (window) {
+ window.document.removeEventListener("keypress", onKeyPress, false);
+ }
+});
@@ -0,0 +1,9 @@
+{
+ "name": "cmd",
+ "license": "MPL 1.1/GPL 2.0/LGPL 2.1",
+ "author": "",
+ "version": "0.1",
+ "fullName": "cmd",
+ "id": "jid0-IwGwzPi3AqlZLfHV1nfcQbYQj6E",
+ "description": "a basic add-on"
+}
@@ -0,0 +1,32 @@
+const main = require("main");
+
+exports.test_test_run = function(test) {
+ test.pass("Unit test running!");
+};
+
+exports.test_id = function(test) {
+ test.assert(require("self").id.length > 0);
+};
+
+exports.test_url = function(test) {
+ require("request").Request({
+ url: "http://www.mozilla.org/",
+ onComplete: function(response) {
+ test.assertEqual(response.statusText, "OK");
+ test.done();
+ }
+ }).get();
+ test.waitUntilDone(20000);
+};
+
+exports.test_open_tab = function(test) {
+ const tabs = require("tabs");
+ tabs.open({
+ url: "http://www.mozilla.org/",
+ onReady: function(tab) {
+ test.assertEqual(tab.url, "http://www.mozilla.org/");
+ test.done();
+ }
+ });
+ test.waitUntilDone(20000);
+};

0 comments on commit 957fba3

Please sign in to comment.