-/*
- * 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();
-