--- /dev/null
+/*
+ * It's All Text - Easy external editing of web forms.
+ * Copyright 2006 Christian Höltje
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License or
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+// @todo [9] IDEA: dropdown list for charsets (utf-8, western-iso, default)?
+// @todo [3] Have a menu/context menu item for turning on monitoring/watch.
+// @todo [9] Menu item to pick the file to load into a textarea.
+// @todo [9] Hot-keys for editing or opening the context menu.
+
+var ItsAllText = function() {
+ /**
+ * This data is all private, which prevents security problems and it
+ * prevents clutter and collection.
+ * @type Object
+ */
+ var that = this;
+
+ /**
+ * Used for tracking all the all the textareas that we are watching.
+ * @type Hash
+ */
+ that.tracker = {};
+
+ /**
+ * Keeps track of all the refreshes we are running.
+ * @type Array
+ */
+ var cron = [null]; // Eat the 0th position
+
+ /**
+ * A constant, a string used for things like the preferences.
+ * @type String
+ */
+ that.MYSTRING = 'itsalltext';
+
+ /**
+ * A constant, the version number. Set by the Makefile.
+ * @type String
+ */
+ that.VERSION = '0.7.3';
+
+ /**
+ * A constant, the url to the readme.
+ * @type String
+ */
+ that.README = 'chrome://itsalltext/locale/readme.xhtml';
+
+ /* The XHTML Namespace */
+ that.XHTMLNS = "http://www.w3.org/1999/xhtml";
+
+ /* The XUL Namespace */
+ that.XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+
+ var string_bundle = Components.classes["@mozilla.org/intl/stringbundle;1"].
+ getService(Components.interfaces.nsIStringBundleService);
+ /**
+ * A localization bundle. Use it like so:
+ * ItsAllText.locale.getStringFromName('blah');
+ */
+ that.locale = string_bundle.createBundle("chrome://itsalltext/locale/itsalltext.properties");
+ /**
+ * Formats a locale string, replacing $N with the arguments in arr.
+ * @param {String} name Locale property name
+ * @param {Array} arr Array of strings to replace in the string.
+ * @returns String
+ */
+ that.localeFormat = function(name, arr) {
+ return this.locale.formatStringFromName(name, arr, arr.length);
+ };
+ /**
+ * Returns the locale string matching name.
+ * @param {String} name Locale property name
+ * @returns String
+ */
+ that.localeString = function(name) {
+ return this.locale.GetStringFromName(name);
+ };
+
+ /**
+ * Create an error message from given arguments.
+ * @param {Object} message One or more objects to be made into strings...
+ */
+ that.logString = function() {
+ var args = Array.prototype.slice.apply(arguments,[0]);
+ for (var i=0; i<args.length; i++) {
+ try {
+ args[i] = args[i].toString();
+ } catch(e) {
+ Components.utils.reportError(e);
+ args[i] = 'toStringFailed';
+ }
+ }
+ args.unshift(that.MYSTRING+':');
+ return args.join(' ');
+ };
+
+ /**
+ * This is a handy debug message. I'll remove it or disable it when
+ * I release this.
+ * @param {Object} message One or more objects can be passed in to display.
+ */
+ that.log = function() {
+ var message = that.logString.apply(that, arguments);
+ var consoleService, e;
+ try {
+ // idiom: Convert arguments to an array for easy handling.
+ consoleService = Components.
+ classes["@mozilla.org/consoleservice;1"].
+ getService(Components.interfaces.nsIConsoleService);
+ consoleService.logStringMessage(message);
+ } catch(e) {
+ Components.utils.reportError(message);
+ }
+ };
+
+ /**
+ * Uses log iff debugging is turned on. Used for messages that need to
+ * globally logged (firebug only logs locally).
+ * @param {Object} message One or more objects can be passed in to display.
+ */
+ that.debuglog = function() {
+ if (that.preferences.debug) {
+ that.log.apply(that,arguments);
+ }
+ };
+
+ /**
+ * Displays debug information, if debugging is turned on.
+ * Requires Firebug.
+ * @param {Object} message One or more objects can be passed in to display.
+ */
+ that.debug = function() {
+ if (that.preferences.debug) {
+ try { Firebug.Console.logFormatted(arguments); }
+ catch(e) {
+ that.log.apply(that,arguments);
+ }
+ }
+ };
+
+ /**
+ * A factory method to make an nsILocalFile object.
+ * @param {String} path A path to initialize the object with (optional).
+ * @returns {nsILocalFile}
+ */
+ that.factoryFile = function(path) {
+ var file = Components.
+ classes["@mozilla.org/file/local;1"].
+ createInstance(Components.interfaces.nsILocalFile);
+ if (typeof(path) == 'string' && path !== '') {
+ file.initWithPath(path);
+ }
+ return file;
+ };
+
+ /**
+ * Returns the directory where we put files to edit.
+ * @returns nsILocalFile The location where we should write editable files.
+ */
+ that.getEditDir = function() {
+ /* Where is the directory that we use. */
+ var fobj = Components.classes["@mozilla.org/file/directory_service;1"].
+ getService(Components.interfaces.nsIProperties).
+ get("ProfD", Components.interfaces.nsIFile);
+ fobj.append(that.MYSTRING);
+ if (!fobj.exists()) {
+ fobj.create(Components.interfaces.nsIFile.DIRECTORY_TYPE,
+ parseInt('0700',8));
+ }
+ if (!fobj.isDirectory()) {
+ that.error(that.localeFormat('problem_making_directory', [fobj.path]));
+ }
+ return fobj;
+ };
+
+ /**
+ * Cleans out the edit directory, deleting all old files.
+ */
+ that.cleanEditDir = function(force) {
+ force = (force && typeof(force) != 'undefined');
+ var last_week = Date.now() - (1000*60*60*24*7);
+ var fobj = that.getEditDir();
+ var entries = fobj.directoryEntries;
+ var entry;
+ while (entries.hasMoreElements()) {
+ entry = entries.getNext();
+ entry.QueryInterface(Components.interfaces.nsIFile);
+ if(force || !entry.exists() || entry.lastModifiedTime < last_week){
+ try{
+ entry.remove(false);
+ } catch(e) {
+ that.debug('unable to remove',entry,'because:',e);
+ }
+ }
+ }
+ };
+
+ /* Clean the edit directory whenever we create a new window. */
+ that.cleanEditDir();
+
+ /* Load the various bits needed to make this work. */
+ var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"].getService(Components.interfaces.mozIJSSubScriptLoader);
+ loader.loadSubScript('chrome://itsalltext/content/Color.js', that);
+ loader.loadSubScript('chrome://itsalltext/content/cacheobj.js', that);
+
+ /**
+ * Dictionary for storing the preferences in.
+ * @type Hash
+ */
+ that.preferences = {
+ debug: true,
+
+ /**
+ * Fetches the current value of the preference.
+ * @private
+ * @param {String} aData The name of the pref to fetch.
+ * @returns {Object} The value of the preference.
+ */
+ _get: function(aData) {
+ var po = that.preference_observer;
+ return po._branch['get'+(po.types[aData])+'Pref'](aData);
+ },
+
+ /**
+ * Sets the current preference.
+ * @param {String} aData The name of the pref to change.
+ * @param {Object} value The value to set.
+ */
+ _set: function(aData, value) {
+ var po = that.preference_observer;
+ return po._branch['set'+(po.types[aData])+'Pref'](aData, value);
+ }
+ };
+
+ /**
+ * A Preference Observer.
+ */
+ that.preference_observer = {
+ /**
+ * Dictionary of types (well, really the method needed to get/set the
+ * type.
+ * @type Hash
+ */
+ types: {
+ charset: 'Char',
+ editor: 'Char',
+ refresh: 'Int',
+ debug: 'Bool',
+ disable_gumdrops: 'Bool',
+ extensions: 'Char'
+ },
+
+ /**
+ * Register the observer.
+ */
+ register: function() {
+ var prefService = Components.
+ classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefService);
+ this._branch = prefService.getBranch("extensions."+that.MYSTRING+".");
+ this._branch.QueryInterface(Components.interfaces.nsIPrefBranch2);
+ this._branch.addObserver("", this, false);
+ /* setup the preferences */
+ for(var type in this.types) {
+ that.preferences[type] = that.preferences._get(type);
+ }
+ },
+
+ /**
+ * Unregister the observer. Not currently used, but may be
+ * useful in the future.
+ */
+ unregister: function() {
+ if (!this._branch) {return;}
+ this._branch.removeObserver("", this);
+ },
+
+ /**
+ * Observation callback.
+ * @param {String} aSubject The nsIPrefBranch we're observing (after appropriate QI)e
+ * @param {String} aData The name of the pref that's been changed (relative to the aSubject).
+ * @param {String} aTopic The string defined by NS_PREFBRANCH_PREFCHANGE_TOPIC_ID
+ */
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != "nsPref:changed") {return;}
+ if (that.preferences) {
+ that.preferences[aData] = that.preferences._get(aData);
+ if (aData == 'refresh') {
+ that.monitor.restart();
+ }
+ }
+ }
+ };
+
+ /**
+ * A Preference Option: What character set should the file use?
+ * @returns {String} the charset to be used.
+ */
+ that.getCharset = function() {
+ return that.preferences.charset;
+ };
+
+ /**
+ * A Preference Option: How often should we search for new content?
+ * @returns {int} The number of seconds between checking for new content.
+ */
+ that.getRefresh = function() {
+ var refresh = that.preferences.refresh;
+ if (!refresh || refresh < 1) {
+ that.debug('Invalid refresh gotten:',refresh);
+ refresh = 1;
+ }
+ var retval = 1000*refresh;
+ return retval;
+
+ };
+
+ /**
+ * Returns true if the system is running Mac OS X.
+ * @returns {boolean} Is this a Mac OS X system?
+ */
+ that.isDarwin = function() {
+ /* more help:
+ http://developer.mozilla.org/en/docs/Code_snippets:Miscellaneous#Operating_system_detection
+ */
+
+ var is_darwin = that._is_darwin;
+ if (typeof(is_darwin) == 'undefined') {
+ is_darwin = /^Darwin/i.test(Components.classes["@mozilla.org/xre/app-info;1"].getService(Components.interfaces.nsIXULRuntime).OS);
+ that._is_darwin = is_darwin;
+ }
+ return is_darwin;
+ };
+
+ /**
+ * A Preference Option: What editor should we use?
+ *
+ * Note: On some platforms, this can return an
+ * NS_ERROR_FILE_INVALID_PATH exception and possibly others.
+ *
+ * For a complete list of exceptions, see:
+ * http://lxr.mozilla.org/seamonkey/source/xpcom/base/nsError.h#262
+ * @returns {nsILocalFile} A file object of the editor.
+ */
+ that.getEditor = function() {
+ var editor = that.preferences.editor;
+ var retval = null;
+
+ if (editor === '' && that.isDarwin()) {
+ editor = '/usr/bin/open';
+ that.preferences._set('editor', editor);
+ }
+
+ if (editor !== '') {
+ retval = that.factoryFile(editor);
+ }
+ return retval;
+ };
+
+ /**
+ * A Preference Option: should we display debugging info?
+ * @returns {bool}
+ */
+ that.getDebug = function() {
+ return that.preferences.debug;
+ };
+
+ /**
+ * A Preference Option: Are the edit gumdrops disabled?
+ * @returns {bool}
+ */
+ that.getDisableGumdrops = function() {
+ return that.preferences.disable_gumdrops;
+ };
+
+ /**
+ * A Preference Option: The list of extensions
+ * @returns Array
+ */
+ that.getExtensions = function() {
+ var string = that.preferences.extensions.replace(/[\n\t ]+/g,'');
+ var extensions = string.split(',');
+ if (extensions.length === 0) {
+ return ['.txt'];
+ } else {
+ return extensions;
+ }
+ };
+
+ /**
+ * Open the preferences dialog box.
+ * @param{boolean} wait The function won't return until the preference is set.
+ * @private
+ * Borrowed from http://wiki.mozilla.org/XUL:Windows
+ * and utilityOverlay.js's openPreferences()
+ */
+ that.openPreferences = function (wait) {
+ wait = typeof(wait)=='boolean'?wait:false;
+ var paneID = that.MYSTRING + '_preferences';
+ var instantApply = getBoolPref("browser.preferences.instantApply", false) && !wait;
+ var features = "chrome,titlebar,toolbar,centerscreen" + (instantApply ? ",dialog=no" : ",modal");
+
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(Components.interfaces.nsIWindowMediator);
+ var win = wm.getMostRecentWindow("Browser:Preferences");
+ var pane;
+ if (win) {
+ win.focus();
+ if (paneID) {
+ pane = win.document.getElementById(paneID);
+ win.document.documentElement.showPane(pane);
+ }
+ } else {
+ openDialog('chrome://itsalltext/chrome/preferences.xul',
+ "", features, paneID);
+ }
+ };
+
+ /**
+ * A Preference Option: Append an extension
+ * @returns Array
+ */
+ that.appendExtensions = function(ext) {
+ ext = ext.replace(/[\n\t ]+/g,'');
+ var current = that.getExtensions();
+ for(var i=0; i<current.length; i++) {
+ if(ext == current[i]) {
+ return; // Don't add a duplicate.
+ }
+ }
+
+ var value = that.preferences.extensions;
+ if(value.replace(/[\t\n ]+/g) === '') {
+ value = ext;
+ } else {
+ value = [value,',',ext].join('');
+ }
+ that.preferences._set('extensions', value);
+ };
+
+ // @todo [3] Profiling and optimization.
+
+ /**
+ * Returns a cache object
+ * Note: These UIDs are only unique for Its All Text.
+ * @param {Object} node A dom object node or ID to one.
+ * @returns {String} the UID or null.
+ */
+ that.getCacheObj = function(node) {
+ var cobj = null;
+ var str = that.MYSTRING+"_UID";
+ if (typeof(node) == 'string') {
+ cobj = that.tracker[node];
+ } else {
+ if (node && node.hasAttribute(str)) {
+ cobj = that.tracker[node.getAttribute(str)];
+ }
+ if (!cobj) {
+ cobj = new ItsAllText.CacheObj(node);
+ }
+ }
+ return cobj;
+ };
+
+ /**
+ * Cleans out all old cache objects.
+ */
+ that.cleanCacheObjs = function() {
+ var count = 0;
+ var cobj, id;
+ for(id in that.tracker) {
+ cobj = that.tracker[id];
+ if (cobj.node.ownerDocument.location === null) {
+ that.debug('cleaning %s', id);
+ delete cobj.node;
+ delete cobj.button;
+ delete that.tracker[id];
+ } else {
+ count += 1;
+ }
+ }
+ that.debuglog('tracker count:', count);
+ };
+
+ /**
+ * Refresh Textarea.
+ * @param {Object} node A specific textarea dom object to update.
+ */
+ that.refreshTextarea = function(node, is_chrome) {
+ var cobj = ItsAllText.getCacheObj(node);
+ if(!cobj) { return; }
+
+ cobj.update();
+ if (!is_chrome) { cobj.addGumDrop(); }
+ };
+
+ // @todo [5] Refresh textarea on editor quit.
+ // @todo [9] IDEA: support for input elements as well?
+
+ /**
+ * Refresh Document.
+ * @param {Object} doc The document to refresh.
+ */
+ that.refreshDocument = function(doc) {
+ if(!doc.location) { return; } // it's being cached, but not shown.
+ var is_chrome = (doc.location.protocol == 'chrome:' &&
+ doc.location.href != that.README);
+ var nodes = doc.getElementsByTagName('textarea');
+ var i;
+ for(i=0; i < nodes.length; i++) {
+ that.refreshTextarea(nodes[i], is_chrome);
+ }
+ nodes = doc.getElementsByTagName('textbox');
+ for(i=0; i < nodes.length; i++) {
+ that.refreshTextarea(nodes[i], is_chrome);
+ }
+ };
+
+ /**
+ * Returns the offset from the containing block.
+ * @param {Object} node A DOM element.
+ * @param {Object} container If unset, then this will use the offsetParent of node. Pass in null to go all the way to the root.
+ * @return {Array} The X & Y page offsets
+ */
+ that.getContainingBlockOffset = function(node, container) {
+ if(typeof(container) == 'undefined') {
+ container = node.offsetParent;
+ }
+ var pos = [node.offsetLeft, node.offsetTop];
+ var pnode = node.offsetParent;
+ while(pnode && (container === null || pnode != container)) {
+ pos[0] += pnode.offsetLeft || 0;
+ pos[1] += pnode.offsetTop || 0;
+ pos[0] -= pnode.scrollLeft || 0;
+ pos[1] -= pnode.scrollTop || 0;
+ pnode = pnode.offsetParent;
+ }
+ return pos;
+ };
+
+
+ /**
+ * This function is called regularly to watch changes to web documents.
+ */
+ that.monitor = {
+ id: null,
+ last_now:0,
+ documents: [],
+ /**
+ * Starts or restarts the document monitor.
+ */
+ restart: function() {
+ var rate = that.getRefresh();
+ var id = that.monitor.id;
+ if (id) {
+ clearInterval(id);
+ }
+ that.monitor.id = setInterval(that.monitor.watcher, rate);
+ },
+ /**
+ * watches the document 'doc'.
+ * @param {Object} doc The document to watch.
+ */
+ watch: function(doc, force) {
+ var contentType, location, is_html, is_usable, is_my_readme;
+ if (!force) {
+ /* Check that this is a document we want to play with. */
+ contentType = doc.contentType;
+ location = doc.location;
+ is_html = (contentType=='text/html' ||
+ contentType=='text/xhtml' ||
+ contentType=='application/xhtml+xml');
+ //var is_xul=(contentType=='application/vnd.mozilla.xul+xml');
+ is_usable = (is_html) &&
+ location.protocol != 'about:' &&
+ location.protocol != 'chrome:';
+ is_my_readme = location.href == that.README;
+ if (!(is_usable || is_my_readme)) {
+ that.debuglog('watch(): ignoring -- ',
+ location, contentType);
+ return;
+ }
+ }
+
+ that.refreshDocument(doc);
+ that.monitor.documents.push(doc);
+ },
+ /**
+ * Callback to be used by restart()
+ * @private
+ */
+ watcher: function(offset) {
+ var monitor = that.monitor;
+ var rate = that.getRefresh();
+
+ var now = Date.now();
+ if (now - monitor.last_now < Math.round(rate * 0.9)) {
+ that.debuglog('monitor.watcher(',offset,') -- skipping catchup refresh');
+ return;
+ }
+ monitor.last_now = now;
+
+ /* Walk the documents looking for changes */
+ var documents = monitor.documents;
+ that.debuglog('monitor.watcher(',offset,'): ', documents.length);
+ var i, doc;
+ var did_delete = false;
+ for(i in documents) {
+ doc = documents[i];
+ if (doc.location) {
+ that.debuglog('refreshing', doc.location);
+ that.refreshDocument(doc);
+ }
+ }
+ },
+ /**
+ * Stops watching doc.
+ * @param {Object} doc The document to watch.
+ */
+ unwatch: function(doc) {
+ var documents = that.monitor.documents;
+ var i;
+ for(i in documents) {
+ if (documents[i] === doc) {
+ that.debug('unwatching', doc);
+ delete documents[i];
+ }
+ }
+ that.cleanCacheObjs();
+ for(i=documents.length - 1; i >= 0; i--) {
+ if(typeof(documents[i]) == 'undefined') {
+ documents.splice(i,1);
+ }
+ }
+ }
+ };
+
+ /**
+ * Callback whenever the DOM content in a window or tab is loaded.
+ * @param {Object} event An event passed in.
+ */
+ that.onDOMContentLoad = function(event) {
+ if (event.originalTarget.nodeName != "#document") { return; }
+ var doc = event.originalTarget || document;
+ that.monitor.watch(doc);
+ return;
+ };
+
+ /**
+ * Open the editor for a selected node.
+ * @param {Object} node The textarea to get.
+ */
+ that.onEditNode = function(node) {
+ var cobj = that.getCacheObj(node);
+ if(cobj) {
+ cobj.edit();
+ }
+ return;
+ };
+
+ /**
+ * Triggered when the context menu is shown.
+ * @param {Object} event The event passed in by the event handler.
+ */
+ that.onContextMenu = function(event) {
+ var tid, node, tag, is_disabled, cobj, menu;
+ if(event.target) {
+ tid = event.target.id;
+ if (tid == "itsalltext-context-popup" ||
+ tid == "contentAreaContextMenu") {
+ node = document.popupNode;
+ tag = node.nodeName.toLowerCase();
+ is_disabled = (!(tag == 'textarea' ||
+ tag == 'textbox') ||
+ node.style.display == 'none' ||
+ node.getAttribute('readonly') ||
+ node.getAttribute('disabled')
+ );
+ if (tid == "itsalltext-context-popup") {
+ cobj = that.getCacheObj(node);
+ that.rebuildMenu(cobj.uid,
+ 'itsalltext-context-popup',
+ is_disabled);
+ } else {
+ // tid == "contentAreaContextMenu"
+ menu = document.getElementById("itsalltext-contextmenu");
+ menu.setAttribute('hidden', is_disabled);
+ }
+
+ }
+ }
+ return true;
+ };
+
+ that.openReadme = function() {
+ browser = getBrowser();
+ browser.selectedTab = browser.addTab(that.README, null);
+ };
+
+ /**
+ * Initialize the module. Should be called once, when a window is loaded.
+ * @private
+ */
+ var windowload = function(event) {
+ that.debug("startup(): It's All Text! is watching this window...");
+
+ // Start watching the preferences.
+ that.preference_observer.register();
+
+ // Start the monitor
+ that.monitor.restart();
+
+ var appcontent = document.getElementById("appcontent"); // The Browser
+ if (appcontent) {
+ // Normal web-page.
+ appcontent.addEventListener("DOMContentLoaded", that.onDOMContentLoad,
+ true);
+ } else {
+ that.onDOMContentLoad(event);
+ }
+ // Attach the context menu, if we can.
+ var contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
+ if (contentAreaContextMenu) {
+ contentAreaContextMenu.addEventListener("popupshowing",
+ that.onContextMenu, false);
+ }
+ };
+
+ // Do the startup when things are loaded.
+ window.addEventListener("load", windowload, true);
+ // Do the startup when things are unloaded.
+ window.addEventListener("unload", function(event){that.monitor.unwatch(event.originalTarget||document); that.preference_observer.unregister();}, true);
+
+};
+
+/**
+ * The command that is called when picking a new extension.
+ * @param {Event} event
+ */
+ItsAllText.prototype.menuNewExtEdit = function(event) {
+ var that = this;
+ var uid = this._current_uid;
+ var cobj = that.getCacheObj(uid);
+
+ var params = {out:null};
+ window.openDialog("chrome://itsalltext/chrome/newextension.xul", "",
+ "chrome, dialog, modal, resizable=yes", params).focus();
+ var ext;
+ if (params.out) {
+ ext = params.out.extension.replace(/[\n\t ]+/g,'');
+ if(params.out.do_save) {
+ that.appendExtensions(ext);
+ }
+ cobj.edit(ext);
+ }
+};
+
+/**
+ * The command that is called when selecting an existing extension.
+ * @param {Event} event
+ */
+ItsAllText.prototype.menuExtEdit = function(event) {
+ var that = this;
+ var uid = that._current_uid;
+ var ext = event.target.getAttribute('label');
+ var cobj = that.getCacheObj(uid);
+ cobj.edit(ext);
+};
+
+/**
+ * Rebuilds the option menu, to reflect the current list of extensions.
+ * @private
+ * @param {String} uid The UID to show in the option menu.
+ */
+ItsAllText.prototype.rebuildMenu = function(uid, menu_id, is_disabled) {
+ menu_id = typeof(menu_id) == 'string'?menu_id:'itsalltext-optionmenu';
+ is_disabled = (typeof(is_disabled) == 'undefined'||!is_disabled)?false:(is_disabled&&true);
+ var i;
+ var that = this;
+ var exts = that.getExtensions();
+ var menu = document.getElementById(menu_id);
+ var items = menu.childNodes;
+ var items_length = items.length - 1; /* We ignore the preferences item */
+ var node;
+ that._current_uid = uid;
+ var magic_stop_node = null;
+ var magic_start = null;
+ var magic_stop = null;
+
+ // Find the beginning and end of the magic replacement parts.
+ for(i=0; i<items_length; i++) {
+ node = items[i];
+ if (node.nodeName.toLowerCase() == 'menuseparator') {
+ if(magic_start === null) {
+ magic_start = i;
+ } else if (magic_stop === null) {
+ magic_stop = i;
+ magic_stop_node = node;
+ }
+ } else if (node.nodeName.toLowerCase() == 'menuitem') {
+ node.setAttribute('disabled', is_disabled?'true':'false');
+ }
+ }
+
+ // Remove old magic bits
+ for(i = magic_stop - 1; i > magic_start; i--) {
+ menu.removeChild(items[i]);
+ }
+
+ // Insert the new magic bits
+ for(i=0; i<exts.length; i++) {
+ node = document.createElementNS(that.XULNS, 'menuitem');
+ node.setAttribute('label', exts[i]);
+ node.addEventListener('command', function(event){return that.menuExtEdit(event);}, false);
+ node.setAttribute('disabled', is_disabled?'true':'false');
+ menu.insertBefore(node, magic_stop_node);
+
+ }
+ return menu;
+};
+
+ItsAllText = new ItsAllText();
+