// mimulus.js - XHTML Editor
// For all your immersive hypertext editing needs.
// $Id: mimulus.js 332 2006-10-27 18:27:51Z sbp $
//
// Author: Sean B. Palmer, inamidst.com
// Author: Christopher Schmidt, crschmidt.net
// Tested on Firefox 1.5 under OS X and Windows
//
// Documentation: http://inamidst.com/mimulus/
// Install: http://inamidst.com/mimulus/mimulus.xpi
//
// Logos designed by Cody Woodard, d8uv.org
// As noted inline, portions were derived from:
// * Bitflux Editor, http://bitfluxeditor.org/
// * TiddlyWiki, http://www.tiddlywiki.com/
//
// License: http://inamidst.com/mimulus/license
// (BSD style, sans infamous advertising clause)
var Mimulus = {
// A singleton class. Avoids littering the global namespace.
/* @@ Todo:
* Allow plain text editing? (R2)
* Get the browser to scroll to follow the caret (B1)
* Classes for tabs where mimulus is on (R4)
* Using Ctrl+K on any element (R3)
* Prevent caret flash using removeAllRanges (or somesuch)
* Don't prompt when navigating away from a saved page?
* Does this still happen? "If you've got an inline element at the end of
a paragraph then you'll probably have to remove it to enable you to
add other stuff after it."
*/
miniListener: function(e) {
// When Mimulus first loads, the only thing that it listens for is
// Ctrl+I, which toggles Mimulus editing mode on and off.
if (e.ctrlKey) {
switch(String.fromCharCode(e.charCode)) {
case 'i': // for "insert"
Mimulus.modcall(Mimulus.toggle, e);
break;
}
}
},
mainListener: function(e) {
// When Mimulus is turned on, it listens for various key combinations,
// mapping those to editing functions. @@ Caret browsing should only be
// enabled when Mimulus is, but that's not consistent at the moment.
Mimulus.selection = Mimulus.win.getSelection();
try { Mimulus.range = Mimulus.selection.getRangeAt(0); }
catch(e) { return false; }
if (e.metaKey) {
// The command key on OS X
return false;
} else if (e.ctrlKey) {
// @@ Enable use of Alt on Windows?
switch(String.fromCharCode(e.charCode)) {
// Forbidden Donut:
// Ctrl+A (Select All)
// Ctrl+C (Copy)
// Ctrl+X (Cut)
// Ctrl+V (Paste)
// Ctrl+B (Bookmarks)
// Ctrl+Q (Quit)
// Ctrl+W (Close Tab)
// Ctrl+F (Find)
// Ctrl+T (New Tab)
// Editor Functions
case 'i': // for "insert"
Mimulus.modcall(Mimulus.toggle, e);
Mimulus.setModified(false);
break;
case 's': // for "save"
Mimulus.modcall(Mimulus.save, e);
Mimulus.setModified(false);
break;
case 'v': // for "vpaste" :-)
Mimulus.modcall(Mimulus.paste, e);
Mimulus.setModified(true);
break;
case 'h': // for "HTML edit"
Mimulus.modcall(Mimulus.editSourceToggle, e);
Mimulus.setModified(false);
break;
// Inline-Level Functions
case 'e': // for "emphasize"
Mimulus.modcall(Mimulus.emphasis, e);
Mimulus.setModified(true);
break;
case 'r': // for "really emphasize"
Mimulus.modcall(Mimulus.strong, e);
Mimulus.setModified(true);
break;
case 'l': // for "link"
Mimulus.modcall(Mimulus.anchor, e);
Mimulus.setModified(true);
break;
case 'y': // for "yank inline"
Mimulus.modcall(Mimulus.removeInline, e);
Mimulus.setModified(true);
break;
// Block-level Functions
case 'n': // for "new paragraph"
Mimulus.modcall(Mimulus.paragraph, e);
Mimulus.setModified(true);
break;
case 'u': // for "unordered list"
Mimulus.modcall(Mimulus.unorderedList, e);
Mimulus.setModified(true);
break;
case 'p': // for "preformatted"
Mimulus.modcall(Mimulus.preformatted, e);
Mimulus.setModified(true);
break;
case '1': // for "1st level heading
Mimulus.heading(1);
e.preventDefault();
e.stopPropagation();
Mimulus.setModified(true);
break;
case '2': // for "2nd level heading"
Mimulus.heading(2);
e.preventDefault();
e.stopPropagation();
Mimulus.setModified(true);
break;
case '3': // for "3rd level heading"
Mimulus.heading(3);
e.preventDefault();
e.stopPropagation();
Mimulus.setModified(true);
break;
case 'j': // for "joggle"
Mimulus.modcall(Mimulus.togglePreformatting, e);
Mimulus.setModified(true);
break;
case 'k': // for "kpreformat" :-)
Mimulus.modcall(Mimulus.editElement, e);
Mimulus.setModified(true);
break;
// @@ Ctrl+O or something for secondary commands
case 'm':
Mimulus.modcall(Mimulus.scrollDown, e);
break;
}
} else {
// Regular, non-modifier, keypress
switch (e.keyCode) {
case e.DOM_VK_RETURN:
Mimulus.keycall(Mimulus.doReturn, e);
Mimulus.setModified(true);
break;
case e.DOM_VK_BACK_SPACE:
Mimulus.keycall(Mimulus.doBackspace, e);
Mimulus.setModified(true);
break;
case e.DOM_VK_DELETE:
Mimulus.keycall(Mimulus.doDelete, e);
Mimulus.setModified(true);
break;
default:
if (e.which === 0) { break; }
var editpre = Mimulus.doc.mimulusEditElement;
if (!editpre || Mimulus.getAnyParent()) {
var text = String.fromCharCode(e.charCode);
Mimulus.insertString(text);
e.preventDefault();
e.stopPropagation();
}
var node = Mimulus.selection.focusNode;
var offset = Mimulus.selection.focusOffset;
Mimulus.selection.collapse(node, offset);
Mimulus.setModified(true);
}
}
},
sourceListener: function(e) {
// On Ctrl+H, this will become The Listener, enabling people to
// edit the source of their documents.
Mimulus.selection = Mimulus.win.getSelection();
try { Mimulus.range = Mimulus.selection.getRangeAt(0); }
catch(e) { return false; }
if (e.metaKey) {
// The command key on OS X
return false;
} else if (e.ctrlKey) {
switch(String.fromCharCode(e.charCode)) {
case 's': // for "save"
Mimulus.saveSource(false); // Manual save
e.preventDefault();
e.stopPropagation();
Mimulus.setModified(false);
break;
case 'h': // for "HTML edit"
Mimulus.editSourceToggle();
e.preventDefault();
e.stopPropagation();
Mimulus.setModified(true);
break;
}
}
},
keycall: function(method, e) {
// Call the appropriate method for the normal keypress event
var editpre = Mimulus.doc.mimulusEditElement;
if (editpre && !Mimulus.getAnyParent()) {
return false; // We're editing a pre-textarea
}
method();
e.preventDefault();
e.stopPropagation();
return true;
},
modcall: function(method, e) {
// Call the appropriate method for the modifier keypress event
method();
e.preventDefault();
e.stopPropagation();
return true;
},
scrollDown: function() {
// Components.interfaces.nsIEditor
// var comp = Components.classes['@mozilla.org/editor/texteditor;1'];
// var e = comp.createInstance(Components.interfaces.nsIEditor);
// @@.scrollSelectionIntoView(Mimulus.selection, Mimulus.range, true);
},
setModified: function(bool) {
// Set whether the document has unsaved modifications
Mimulus.doc.mimulusModified = bool;
},
validSelection: function() {
// @@ This isn't actually used anywhere!
Mimulus.selection = Mimulus.win.getSelection();
try { Mimulus.range = Mimulus.selection.getRangeAt(0); }
catch(e) { return false; }
return true;
},
selectEnd: function(node) {
var child = node.lastChild;
Mimulus.selection.collapse(child, child.length);
},
somethingSelected: function() {
// Return true if something is selected, false otherwise
Mimulus.selection = Mimulus.win.getSelection();
try { Mimulus.range = Mimulus.selection.getRangeAt(0); }
catch(e) {}
if (Mimulus.range.toString().length > 0) {
return true;
} else { return false; }
// if ((Mimulus.range.startContainer != Mimulus.range.endContainer) ||
// (Mimulus.range.startOffset != Mimulus.range.endOffset)) {
// return false;
// } else { return true; }
},
startOfElement: function() {
// Return true if the caret is at the start of the parent element
var here = Mimulus.range.startContainer;
var parent = here.parentNode;
var pos = Mimulus.range.startOffset;
if (here == parent.firstChild && pos == 0) {
return true;
} else { return false; }
},
endOfElement: function() {
// Return true if the caret is at the end of the parent element
var here = Mimulus.range.startContainer;
var parent = here.parentNode;
var pos = Mimulus.range.startOffset;
if (here == parent.lastChild && pos == here.length) {
return true;
} else { return false; }
},
paste: function() {
// Cf. http://www.mozilla.org/xpfe/xptoolkit/clipboard.html
var clip = Components.classes['@mozilla.org/widget/clipboard;1'].
getService(Components.interfaces.nsIClipboard);
var trans = Components.classes['@mozilla.org/widget/transferable;1'].
getService(Components.interfaces.nsITransferable);
if (clip && trans) {
// This only needs to be set once, and will error if reset
// We just catch the error
try { trans.addDataFlavor('text/unicode'); }
catch(e) {}
clip.getData(trans, clip.kGlobalClipboard);
var transData = new Object();
var length = new Object();
trans.getTransferData('text/unicode', transData, length);
if (transData) {
var value = transData.value.
QueryInterface(Components.interfaces.nsISupportsString);
} else { return; }
// The transData value is stored as UTF-16, and the length given
// is the length of internal bytes, so the actual length of the
// data (which we extract below) is half that figure.
//
if (value && value.data) {
var data = value.data.substring(0, length.value/2);
Mimulus.insertString(data);
}
}
},
insertString: function(s) {
// Inserts character data into the document, escaping <, >, and &
var parent = Mimulus.getAnyParent();
// The space/ hack for empty block elements
if (parent.innerHTML == ' ') {
// The HTML needs to be escaped when using innerHTML...
s = s.replace(/&/g, '&');
s = s.replace(//g, '>'); // just for consistency
if (s.substr(s.length - 1) == ' ') {
s = s.substr(0, s.length - 1) + ' ';
}
parent.innerHTML = s;
Mimulus.selectEnd(parent);
return;
}
// The trailing space/ hack for block elements
if (Mimulus.endOfElement()) {
// If we're at the end of the current container, then...
var html = parent.innerHTML;
// The following must really be "== null". Doing a boolean test
// won't work because '' counts.
if (html == null) { return false; } // @@ in non-editable contexts
// If the element ends in an , replace it with a space
if (html.substr(html.length - 6) == ' ' && html.length > 6) {
parent.innerHTML = html.substr(0, html.length - 6) + ' ';
Mimulus.selectEnd(parent);
Mimulus.range = Mimulus.selection.getRangeAt(0);
}
if (s.substr(s.length - 1) == ' ') {
parent.innerHTML = html + s.substr(0, s.length - 1) + ' ';
Mimulus.selectEnd(parent);
return true;
}
}
try { Mimulus.range.deleteContents(); }
catch(e) { return; }
// We'll be checking that insertData didn't advance the caret
var startOffset = Mimulus.range.startOffset;
if (Mimulus.range.startContainer.nodeType ==
Mimulus.range.startContainer.TEXT_NODE) {
Mimulus.range.startContainer.insertData(startOffset, s);
// The following basically advances the caret
// @@ When selecting across elements, Mimulus.selects everything
if (startOffset == Mimulus.range.startOffset) {
Mimulus.selection.collapse(Mimulus.range.startContainer,
Mimulus.range.startOffset + s.length);
}
}
// As Firebug puts it: "Mimulus.relection has no properties"
// In other words, what was the following code actually for?
// Mimulus.selection.addRange(Mimulus.range);
return s;
},
selectNode: function(node) {
// @@ This is only used a few times. Refactor it?
// http://www.opendarwin.org/pipermail/webkit-changes/2006-January.txt
if (Mimulus.selection.selectAllChildren) {
Mimulus.selection.selectAllChildren(node);
}
},
findParent: function(tagnames) {
// Get parent of node. E.g. Mimulus.findParent(['p', 'pre']);
var node = Mimulus.range.startContainer;
var matches = Object();
for (var i in tagnames) {
matches[tagnames[i]] = 1;
}
while (true) {
if (node.nodeType == node.TEXT_NODE) {
node = node.parentNode;
continue;
}
else if (node == Mimulus.doc.documentElement) {
return false;
}
var name = node.tagName.toLowerCase();
if (name in matches) { break; }
else { node = node.parentNode; }
}
return node;
},
findAnyParent: function() {
// Get parent of node
var node = Mimulus.range.startContainer;
while (true) {
if (node.nodeType == node.TEXT_NODE) {
node = node.parentNode;
continue;
} else if (node == Mimulus.doc.documentElement) {
return false;
}
return node.tagName.toLowerCase();
}
},
getMainParent: function() {
// Get parents we can put siblings after
return Mimulus.findParent(['p', 'pre', 'ul', 'h1', 'h2', 'h3']);
},
getAnyParent: function() {
// This list of elements is not exhaustive, apparently. Chris added
// them, and I'm not sure what metric for inclusion he used!
var tags = ['a', 'address', 'blockquote', 'center', 'dd', 'div', 'dir',
'dl', 'dt', 'em', 'form', 'h1', 'h2', 'h3', 'h4', 'h5',
'h6', 'li', 'p', 'pre', 'span', 'strong', 'table', 'td'];
return Mimulus.findParent(tags);
},
newParagraph: function() {
var p = Mimulus.doc.createElement('p');
p.innerHTML = ' ';
var parent = Mimulus.getMainParent();
var grandparent = parent.parentNode;
if (parent.nextSibling) {
grandparent.insertBefore(p, parent.nextSibling);
} else { grandparent.appendChild(p); }
Mimulus.selection.collapse(p.firstChild, 0);
Mimulus.autosave();
},
newParagraphBefore: function() {
var p = Mimulus.doc.createElement('p');
p.innerHTML = ' ';
var parent = Mimulus.getMainParent();
var grandparent = parent.parentNode;
grandparent.insertBefore(p, parent);
Mimulus.selection.collapse(p.firstChild, 0);
Mimulus.autosave();
},
paragraph: function() {
// Create a new paragraph, if the conditions are right
var tags = ['h1', 'h2', 'h3', 'li', 'p', 'pre'];
var parent = Mimulus.findParent(tags);
if (!parent) { return false; } // @@ Show error?
var name = parent.tagName.toLowerCase();
if (name == 'pre') {
Mimulus.newParagraph();
} else if (Mimulus.endOfElement()) {
// @@ Doesn't work in an paragraph
// Possibly for the best, but...
Mimulus.newParagraph();
}
},
newListItem: function() {
var li = Mimulus.doc.createElement('li');
li.innerHTML = ' ';
var parent = Mimulus.findParent(['li']);
var grandparent = parent.parentNode;
if (parent.nextSibling) {
grandparent.insertBefore(li, parent.nextSibling);
} else { grandparent.appendChild(li); }
Mimulus.selection.collapse(li.firstChild, 0);
Mimulus.autosave();
},
newListItemBefore: function() {
var li = Mimulus.doc.createElement('li');
li.innerHTML = ' ';
var parent = Mimulus.findParent(['li']);
var grandparent = parent.parentNode;
grandparent.insertBefore(li, parent);
Mimulus.selection.collapse(li.firstChild, 0);
Mimulus.autosave();
},
newUnorderedList: function() {
var ul = Mimulus.doc.createElement('ul');
var li = Mimulus.doc.createElement('li');
li.innerHTML = ' ';
ul.appendChild(li);
var parent = Mimulus.getMainParent();
var grandparent = parent.parentNode;
if (parent.nextSibling) {
grandparent.insertBefore(ul, parent.nextSibling);
} else { grandparent.appendChild(ul); }
Mimulus.selection.collapse(li.firstChild, 0);
Mimulus.autosave();
},
unorderedList: function() {
// Make a new unordered list, if the conditions are right
var tags = ['h1', 'h2', 'h3', 'p', 'pre'];
var parent = Mimulus.findParent(tags);
if (!parent) { return false; } // @@ Show error?
var name = parent.tagName.toLowerCase();
if (name == 'pre') {
Mimulus.newUnorderedList();
// } else if (name == 'li') {
// // @@! Should sequential ULs be allowed? How about inline ULs?
// mimulusNewInlineUnorderedList();
} else if (Mimulus.endOfElement()) {
Mimulus.newUnorderedList();
}
},
newPreformatted: function() {
var pre = Mimulus.doc.createElement('pre');
pre.innerHTML = '\n';
var parent = Mimulus.getMainParent();
var grandparent = parent.parentNode;
if (parent.nextSibling) {
grandparent.insertBefore(pre, parent.nextSibling);
} else { grandparent.appendChild(pre); }
Mimulus.selection.collapse(pre.firstChild, 0);
Mimulus.autosave();
},
preformatted: function() {
// Create a new preformatted section, if the conditions are right
var parent = Mimulus.findParent(['h1', 'h2', 'h3', 'li', 'p']);
if (!parent) { return false; } // @@ Show error?
var name = parent.tagName.toLowerCase();
// No pre-special-case needed here!
if (Mimulus.endOfElement()) {
Mimulus.newPreformatted();
}
},
newHeading: function(level) {
// We only allow the first three levels of headers
if (!(level == 1 || level == 2 || level == 3)) { return false; }
var h = Mimulus.doc.createElement('h' + level);
h.innerHTML = ' ';
var parent = Mimulus.getMainParent();
var grandparent = parent.parentNode;
if (parent.nextSibling) {
grandparent.insertBefore(h, parent.nextSibling);
} else { grandparent.appendChild(h); }
Mimulus.selection.collapse(h.firstChild, 0);
Mimulus.autosave();
},
heading: function(level) {
// We only allow the first three levels of headers
if (!(level == 1 || level == 2 || level == 3)) { return false; }
var parent = Mimulus.findParent(['h1', 'h2', 'h3', 'li', 'p', 'pre']);
if (!parent) { return false; } // @@ Show error?
var name = parent.tagName.toLowerCase();
if (name == 'pre') {
Mimulus.newHeading(level);
} else if (Mimulus.endOfElement()) {
Mimulus.newHeading(level);
}
},
newInline: function(tag) {
// Create a new inline element of name tag.
// This might turn existing text content into an inline element.
// If we're already in this kind of inline element, exit
var parent = Mimulus.getAnyParent();
if (parent.tagName.toLowerCase() == tag) {
Mimulus.exitElement(parent);
return false;
}
// If something was selected, use that as the new content
// Warning! If you globally scope the content variable, really bad
// things happen, like... crashing and stuff. So don't do that.
if (Mimulus.somethingSelected()) {
var selection = Mimulus.range.toString();
// @@ if (selection.length > 0) {
var content = Mimulus.doc.createTextNode(selection);
try { Mimulus.range.deleteContents(); }
catch(e) {}
} else { var content = null; }
var container = Mimulus.range.startContainer;
var offset = Mimulus.range.startOffset;
// We can only make inline elements inside text nodes
if (container.nodeType == container.TEXT_NODE) {
// Trailing hack
if (offset == container.length) {
if (container.data.substr(container.length - 1) == '\u00A0') {
container.replaceData(container.length - 1, 1, ' ');
}
}
container.splitText(offset);
var inline = Mimulus.doc.createElement(tag);
if (!content) {
inline.innerHTML = ' ';
} else { inline.appendChild(content); }
var parent = container.parentNode;
var next = container.nextSibling;
parent.insertBefore(inline, next);
if (!content) {
Mimulus.selection.collapse(inline.firstChild, 0);
} else {
Mimulus.selection.collapse(content, content.length);
// Mimulus.selection.addRange(Mimulus.range);
}
return inline;
} else {
alert('Unable to make an inline element here');
return false;
}
},
emphasis: function() {
Mimulus.newInline('em');
},
strong: function() {
Mimulus.newInline('strong');
},
anchor: function() {
var uri = window.prompt('URI', 'http://');
var a = Mimulus.newInline('a');
a.setAttribute('href', uri);
},
togglePreformatting: function() {
var parent = Mimulus.findParent(['p', 'pre']);
if (!parent) { return false; } // @@ show error?
var name = parent.tagName.toLowerCase();
if (name == 'p') {
Mimulus.transformElement(parent, 'pre');
} else { Mimulus.transformElement(parent, 'p'); }
},
transformElement: function(parent, tag) {
var grandparent = parent.parentNode;
var node = Mimulus.doc.createElement(tag);
for (var i=0; child = parent.childNodes.item(i); i++) {
node.appendChild(child.cloneNode(true));
}
grandparent.replaceChild(node, parent);
// @@ selectEnd(node), selectBeginning(node)
if (node.innerHTML == ' ') { // @@ Only from p to pre, no?
node.innerHTML = '\n';
Mimulus.selection.collapse(node.firstChild, 0);
} else {
Mimulus.selection.collapse(node.lastChild, node.lastChild.length);
}
Mimulus.updateStatus();
},
// Edit individual block level elements
editElement: function() {
// Do funky stuff to get around a Firefox caret browsing bug.
if (Mimulus.doc.mimulusEditElement) {
Mimulus.editElementOff(true);
} else { Mimulus.editElementOn(); }
},
editElementOn: function() {
// @@ Preserve attributes!
var e = Mimulus.findParent(['p', 'pre', 'ul']);
// Unique ID that nobody else in the world will use
var uid = 'uidemarsdendownloads';
var textarea = Mimulus.doc.getElementById('mimulus-' + uid);
if (!e) {
Mimulus.notice('This only works in
,
, and
elements.');
return false;
} else if (textarea) {
Mimulus.notice("You are already editing an element's source.");
return false;
}
var name = e.tagName.toLowerCase();
var text = e.innerHTML.toString();
text = text.replace(/^\n- /, '
- ');
var rows = Math.max(5, text.split('\n').length);
var node = Mimulus.doc.createElement('textarea');
node.setAttribute('id', 'mimulus-' + uid);
// Border colour reflects element type being edited...
if (name == 'p') {
var border = '2px solid #e9a;'; // red
} else if (name == 'pre') {
var border = '2px solid #e9d;'; // purple
} else if (name == 'ul') {
var border = '2px solid #9ca;'; // green
}
node.setAttribute('style', 'border:' + border +' padding:2px;');
node.setAttribute('rows', rows);
node.setAttribute('cols', '79');
node.value = text;
e.parentNode.replaceChild(node, e);
node.focus(); // @@ Focus on the beginning of the text, not the end
Mimulus.doc.mimulusEditElementName = name;
Mimulus.doc.mimulusEditElement = true;
return true;
},
editElementOff: function(select) {
// Unique ID that nobody else in the world will use
var uid = 'uidemarsdendownloads';
var textarea = Mimulus.doc.getElementById('mimulus-' + uid);
if (!textarea) {
Mimulus.notice("You are not editing an element's source.");
return false;
} else if (Mimulus.findParent(['p', 'pre', 'ul'])) {
var restart = true;
} else if (Mimulus.getAnyParent()) {
Mimulus.notice("Can't do anything with that here."); // @@
return false;
}
var name = Mimulus.doc.mimulusEditElementName;
var node = Mimulus.doc.createElement(name);
node.innerHTML = textarea.value;
textarea.parentNode.replaceChild(node, textarea);
Mimulus.doc.mimulusEditElementName = null;
Mimulus.doc.mimulusEditElement = false;
// @@ About what this means
if (select && !restart) {
Mimulus.selection.collapse(node.firstChild, 0);
} else if (restart && select) { Mimulus.editElementOn(); }
},
exitElement: function(current) {
// Get the caret to exit the current inline element
if (current.nextSibling) {
Mimulus.selectNode(current.nextSibling);
}
},
removeInline: function() {
// Remove inline formatting, keeping the contents
var inline = Mimulus.findParent(['em', 'strong', 'a']);
if (!inline) { return false; } // @@ warn?
var grandparent = inline.parentNode;
// Clone the contents of the inline element, and insert it to before
// where the current inline element is.
for (var i=0; i -1; i--) {
var child = element.childNodes.item(i);
if (child.previousSibling) {
var before = child.previousSibling;
if (child.nodeType == child.TEXT_NODE &&
before.nodeType == before.TEXT_NODE) {
if (child != Mimulus.range.startContainer) {
before.insertData(before.length, child.data);
element.removeChild(child);
} else {
var pos = before.length + Mimulus.range.startOffset;
before.insertData(before.length, child.data);
element.removeChild(child);
Mimulus.selection.collapse(before, pos);
}
}
}
}
},
fiddlePre: function(pre) {
Mimulus.selection = Mimulus.win.getSelection();
try { Mimulus.range = Mimulus.selection.getRangeAt(0); }
catch(e) { return false; }
var parent = pre.parentNode;
var clone = pre.cloneNode(true);
var pos = Mimulus.range.startOffset;
Mimulus.selection.removeAllRanges();
parent.replaceChild(clone, pre);
Mimulus.selection.collapse(clone.firstChild, pos);
},
notice: function(msg) {
alert('Mimulus Notice\n' + msg);
},
// Edit Keys - Return, Backspace, Delete
doReturn: function() {
// Return was pressed, act appropriately
if (!Mimulus.somethingSelected()) {
// Nothing was selected
var tags = ['a', 'em', 'strong', 'h1', 'h2', 'h3', 'li', 'p', 'pre'];
var parent = Mimulus.findParent(tags);
var name = parent.tagName.toLowerCase();
if (parent) {
if (name == 'pre') {
// @@ Do a special redraw! It works with textarea, so
// surely it can be simulated too!
Mimulus.insertString('\n');
Mimulus.fiddlePre(parent);
} else if (Mimulus.endOfElement()) {
switch (name) {
case 'p':
case 'h1':
case 'h2':
case 'h3':
Mimulus.newParagraph();
break;
case 'em':
case 'strong':
case 'a':
Mimulus.exitElement(parent);
break;
case 'li':
Mimulus.newListItem();
break;
}
} else if (Mimulus.startOfElement()) {
switch (name) {
case 'p':
Mimulus.newParagraphBefore();
break;
case 'li':
Mimulus.newListItemBefore();
break;
}
} else if (name == 'li' || name == 'p') {
Mimulus.splitElement();
}
}
} else { Mimulus.doDelete(); }
},
doBackspace: function() {
// Backspace key pressed in some element
// This is fairly ecumenical, except for joining li and p
var here = Mimulus.range.startContainer;
var pos = Mimulus.range.startOffset;
if (!Mimulus.somethingSelected() && here.nodeType == here.TEXT_NODE) {
// Nothing is selected, so just a regular delete
// We can only delete if we're inside a text node
if (here.parentNode.innerHTML == ' ') {
// Delete an empty element
Mimulus.deleteElement('previous');
} else if (pos > 0) {
// Regular text deletion
here.deleteData(pos - 1, 1);
Mimulus.checkTrailingWhitespace();
} else if (Mimulus.startOfElement()) {
// Join this element with the previous one, if an li or p
var joined = Mimulus.joinPrevious();
// Otherwise, delete across inline element boundaries
if (!joined) {
var prev = here.parentNode.previousSibling;
if (prev.nodeType == prev.TEXT_NODE) {
Mimulus.selection.collapse(prev, prev.length);
Mimulus.selection = Mimulus.win.getSelection();
try { Mimulus.range = Mimulus.selection.getRangeAt(0); }
catch(e) { return false; } // updateSelection? updateRange?
Mimulus.doBackspace(); // Hmm, small recursion potential...
}
}
} else if (here.previousSibling) {
// Delete across the boundaries of inline elements
var t = here.previousSibling.lastChild;
if (t && t.nodeType == t.TEXT_NODE && t.length > 0) {
t.deleteData(t.length -1, 1);
Mimulus.selection.collapse(t, t.length);
// @@ Need a preceding hack...
}
}
} else {
// Delete some selected amount of text
try { Mimulus.range.deleteContents(); }
catch(e) { return; }
Mimulus.selection.removeAllRanges();
Mimulus.selection.collapse(here, pos);
Mimulus.checkTrailingWhitespace();
}
},
doDelete: function() {
// Delete key pressed in some element
// This is fairly ecumenical, except for joining li and p
var here = Mimulus.range.startContainer;
var pos = Mimulus.range.startOffset;
if (!Mimulus.somethingSelected() && here.nodeType == here.TEXT_NODE) {
// Nothing is selected, so just a regular delete
// We can only delete if we're inside a text node
if (here.parentNode.innerHTML == ' ') {
// Delete an empty element
Mimulus.deleteElement('following');
} else if (pos < here.length) {
// Regular text deletion
here.deleteData(pos, 1);
Mimulus.checkTrailingWhitespace();
} else if (Mimulus.endOfElement()) {
// Join this element with the next one, if an li or p
var joined = Mimulus.joinFollowing();
// Otherwise, delete across inline element boundaries
if (!joined) {
var next = here.parentNode.nextSibling;
if (next.nodeType == next.TEXT_NODE) {
Mimulus.selection.collapse(next, 0);
Mimulus.selection = Mimulus.win.getSelection();
try { Mimulus.range = Mimulus.selection.getRangeAt(0); }
catch(e) { return false; } // updateSelection? updateRange?
Mimulus.doDelete(); // Hmm, very small recursion potential...
}
}
} else if (here.nextSibling) {
// Delete across the boundaries of inline elements
var t = here.nextSibling.firstChild;
if (t && t.nodeType == t.TEXT_NODE && t.length > 0) {
t.deleteData(0, 1);
Mimulus.selection.collapse(t, 0);
// @@ Need a preceding hack...
}
}
} else {
// Delete some selected amount of text
try { Mimulus.range.deleteContents(); }
catch(e) { return; }
Mimulus.selection.removeAllRanges();
Mimulus.selection.collapse(here, pos);
Mimulus.checkTrailingWhitespace();
}
},
checkTrailingWhitespace: function() {
// Now time for more hacking!
//
// This arises because if a paragraph contains "pqr s" and the end s is
// deleted, then the trailing space won't show, which is confusing
// because then the next time backspace is hit, nothing visibly happens
// even though that space is deleted.
Mimulus.selection = Mimulus.win.getSelection();
try { Mimulus.range = Mimulus.selection.getRangeAt(0); }
catch(e) { return false; }
// If we've deleted in p, li, h[1-3] and we end with a space, nbspise it
var p = Mimulus.findParent(['p', 'li', 'h1', 'h2', 'h3']);
if (p && Mimulus.endOfElement()) {
var html = p.innerHTML;
if (html.substr(html.length - 1) == ' ' && html.length > 1) {
p.innerHTML = html.substr(0, html.length - 1) + ' ';
Mimulus.selection.collapse(p.lastChild, p.lastChild.length);
} else if (!html) {
p.innerHTML = ' ';
Mimulus.selection.collapse(p.firstChild, 0);
}
}
},
splitElement: function() {
// Split the current paragraph into two
// @@ Won't work inside inline elements, only text
// @@ Handle leading and trailing spaces
var parent = Mimulus.getAnyParent();
var name = parent.tagName.toLowerCase();
if (name != 'li' && name != 'p') { return false; }
var first = Mimulus.doc.createElement(name);
var second = null;
var current = Mimulus.range.endContainer;
var offset = Mimulus.range.endOffset;
for (var i=0; i\n<');
text = text.replace(/<\/(h1|h2|h3|li)>\n<');
text = text.replace(/<\/(p|pre|ul)>\n\n<');
var html = text.replace(/&/g, '&').replace(/\n\n\n';
}
var editor =
'
\n' +
(dochead? form('Head', head) : '') + form('Body', body) +
'
Ctrl+S to save, Ctrl+H to quit Source Edit Mode.
\n' +
'
\n';
Mimulus.doc.mimulusOldTitle = Mimulus.doc.title;
Mimulus.doc.title = 'Mimulus: Source Edit Mode';
Mimulus.doc.body.innerHTML = editor;
Mimulus.doc.mimulusSourceEditing = true;
},
editSourceOff: function() {
// Turn source editing off
Mimulus.saveSource(true); // Automatic save
Mimulus.unlisten('keypress', Mimulus.sourceListener);
Mimulus.listen('keypress', Mimulus.mainListener);
// Restore the document!
Mimulus.doc.title = Mimulus.doc.mimulusOldTitle;
Mimulus.doc.mimulusOldTitle = null;
var head = Mimulus.doc.getElementById('mimulusHead');
if (head) {
dochead = Mimulus.doc.getElementsByTagName('head')[0];
if (dochead) { dochead.innerHTML = head.value; }
// @@ grep the title out
// @@ may need to refresh other head changes
}
var body = Mimulus.doc.getElementById('mimulusBody');
Mimulus.doc.body.innerHTML = body.value;
Mimulus.setCaret(true);
Mimulus.caretMode = Mimulus.getCaret();
Mimulus.doc.mimulusSourceEditing = false;
// @@ Select the first editable bit of content
},
saveSource: function(auto) {
// @@ Rats, the following doesn't work!
// var documentCopy = Mimulus.doc.cloneNode(true);
// @@ This'll mean editing GRDDL will suck in source mode...
var mimulusHead = Mimulus.doc.getElementById('mimulusHead');
if (mimulusHead) { var head = mimulusHead.value; }
else { var head = Mimulus.doc.head.innerHTML; }
var mimulusBody = Mimulus.doc.getElementById('mimulusBody');
var body = mimulusBody.value;
var content =
'\n' +
'\n' + head + '\n' +
'\n' + body + '\n' +
'';
var fileuri = Mimulus.doc.mimulusFileURI;
return Mimulus.saveToFile(fileuri, content, auto);
},
// I/O Functions
saveToFile: function(fileuri, content, auto) {
// Save content to fileuri, prompting per auto
// (This was derived from TiddlyWiki)
try {
var component = '@mozilla.org/network/protocol;1?name=file';
var comp = Components.classes[component];
var handler = Components.interfaces.nsIFileProtocolHandler;
var f = comp.createInstance(handler);
// Ignore filename, use our own thing
var file = f.getFileFromURLSpec(fileuri);
if (!file.exists()) { file.create(0, 0644); } // @@
var component = '@mozilla.org/network/file-output-stream;1';
var instance = Components.interfaces.nsIFileOutputStream;
var out = Components.classes[component].createInstance(instance);
out.init(file, 0x20 | 0x02, 00004, null);
out.write(content, content.length);
out.flush();
out.close();
// Display an appropriate save message in the status bar
var verb = (auto? 'Autosaved' : 'Saved');
var msg = verb + ' ' + content.length + ' bytes';
if (Mimulus.doc.mimulusFileURI != Mimulus.doc.location) {
// @@ location.toString()
Mimulus.message = msg + ' to ' + file.path;
} else { Mimulus.message = msg + '.'; }
// @@ Some way to copy the current filename
return true;
} catch(e) {
alert(e);
return false;
}
},
pickFileURI: function() {
// Open up a file picker for the user to enter a save filename
// Actually returns a file: URI. @@ Mimulus.pickFileURI?
// Cf. http://kb.mozillazine.org/File_IO
// Cf. http://developer.mozilla.org/en/docs/nsIFilePicker
const nsIFilePicker = Components.interfaces.nsIFilePicker;
var comp = Components.classes['@mozilla.org/filepicker;1'];
var picker = comp.createInstance(nsIFilePicker);
var msg = 'Mimulus - Save this document to...';
picker.init(window, msg, nsIFilePicker.modeSave);
picker.appendFilters(nsIFilePicker.filterAll|nsIFilePicker.filterHTML);
// @@ picker.defaultString = something
// @@ displayDirectory, too? homedir?
var status = picker.show();
if (status == nsIFilePicker.returnOK) {
return Mimulus.fileToURI(picker.file);
} else { return false; }
},
fileToURI: function(file) {
// Return the file: URI denoting this file
var component = '@mozilla.org/network/protocol;1?name=file';
var comp = Components.classes[component];
var handler = Components.interfaces.nsIFileProtocolHandler;
var f = comp.createInstance(handler);
return f.getURLSpecFromFile(file);
},
saveDocument: function(auto) {
// Save the current document
// The following is rather important unless you like losing data
if (Mimulus.doc.mimulusEditElement) {
Mimulus.editElementOff(false);
}
var content =
'\n' +
Mimulus.doc.documentElement.innerHTML + '\n' +
'';
var fileuri = Mimulus.doc.mimulusFileURI;
return Mimulus.saveToFile(fileuri, content, auto);
},
save: function() {
// Save the current document manually
Mimulus.saveDocument(false);
},
autosave: function() {
// Save the current document automatically
Mimulus.saveDocument(true);
},
// Editor State Functions
toggle: function() {
// Toggle Mimulus on or off appropriately
// @@ Mimulus.getWindowDocument();
if (!Mimulus.doc.mimulusRunning) {
Mimulus.turnOn();
} else { Mimulus.turnOff(); }
Mimulus.normaliseState();
},
turnOn: function() {
// Turn Mimulus on in this tab
Mimulus.doc.mimulusRunning = true;
Mimulus.doc.mimulusListening = false;
Mimulus.unlisten('keypress', Mimulus.miniListener);
Mimulus.listen('keypress', Mimulus.mainListener);
Mimulus.listen('keyup', Mimulus.updateStatus);
Mimulus.listen('click', Mimulus.updateStatus);
window.onbeforeunload = Mimulus.safeExit;
Mimulus.setCaret(true);
Mimulus.caretMode = Mimulus.getCaret();
Mimulus.message = null;
if (!Mimulus.doc.mimulusFileURI) {
Mimulus.doc.mimulusFileURI = Mimulus.doc.location.toString();
// @@ Allow changing of the save path, to save to new place?
if (Mimulus.doc.mimulusFileURI.substr(0, 5) != 'file:') {
// Bring up a decent Save As dialogue
// Note that the value is saved across Mimulus restarts
Mimulus.doc.mimulusFileURI = Mimulus.pickFileURI();
if (!Mimulus.doc.mimulusFileURI) {
alert('Unable to save to that location. Turning Mimulus off.');
Mimulus.turnOff();
return false;
}
}
}
Mimulus.tabStyleOn();
Mimulus.updateStatus();
},
turnOff: function() {
// Turn Mimulus off in this tab
// @@ Remove the current message, if there is one
if (Mimulus.doc.mimulusModified) {
Mimulus.autosave();
alert('Autosaved your changes!');
Mimulus.setModified(false);
}
Mimulus.doc.mimulusRunning = false;
Mimulus.doc.mimulusListening = true;
Mimulus.listen('keypress', Mimulus.miniListener);
Mimulus.unlisten('keypress', Mimulus.mainListener);
Mimulus.unlisten('keyup', Mimulus.updateStatus);
Mimulus.unlisten('click', Mimulus.updateStatus);
window.onbeforeunload = null;
// @@ Experimental
// Cf. http://www.mozilla.org/support/firefox/edit#css
try {
var tab = gBrowser.mCurrentTab;
tab.setAttribute('mimulus', 'off');
tab.setAttribute('image', Mimulus.doc.mimulusSavedImage);
} catch(e) {}
// @@ Reset the status?
Mimulus.tabStyleOff();
Mimulus.setCaret(false);
},
tabStyleOn: function() {
var tab = gBrowser.mCurrentTab;
if (tab) {
// Cf. http://www.mozilla.org/support/firefox/edit#css
// tab[mimulus="on"]:not([selected="true"]) {
// background-color: #cdc !important;
// }
tab.setAttribute('mimulus', 'on');
if (Mimulus.getPreference('extensions.mimulus.tabicon')) {
Mimulus.doc.mimulusTabiconURI = tab.getAttribute('image');
tab.setAttribute('image', 'chrome://mimulus/skin/tabicon.png');
}
}
},
tabStyleOff: function() {
var tab = gBrowser.mCurrentTab;
if (tab) {
tab.setAttribute('mimulus', 'off');
if (Mimulus.getPreference('extensions.mimulus.tabicon')) {
tab.setAttribute('image', Mimulus.doc.mimulusTabiconURI);
} else if (Mimulus.doc.mimulusTabiconURI) {
tab.setAttribute('image', Mimulus.doc.mimulusTabiconURI);
Mimulus.doc.mimulusTabiconURI = null;
}
}
},
getPreference: function(name) {
try {
var mozpref = '@mozilla.org/preferences;1';
var pref = new Components.Constructor(mozpref, 'nsIPref')();
return pref.GetBoolPref(name);
} catch(e) { return false; }
},
updateStatus: function() {
// Show appropriate Mimulus messages in the status bar
// Example: "Mimulus is on. Editing: p. Saved 905 bytes"
if (!Mimulus.win || !Mimulus.doc || !Mimulus.doc.mimulusRunning) {
// Don't mess with the status bar if Mimulus isn't running
return false;
} else if (Mimulus.doc.mimulusSourceEditing) {
// Caret mode will be off here
var msg = 'Mimulus source editing. Ctrl+H to exit.';
if (Mimulus.message) {
msg = msg + ' ' + Mimulus.message;
Mimulus.message = null;
}
Mimulus.win.status = msg;
return true;
} else if (!Mimulus.caretMode) {
// @@ Warning! State is presumably messed up somewhere
return false;
}
// @@ validSelection
Mimulus.selection = Mimulus.win.getSelection();
try { Mimulus.range = Mimulus.selection.getRangeAt(0); }
catch(e) {
Mimulus.win.status = 'Mimulus is on, but nothing is selected.';
return true;
}
try {
// @@ Always append Mimulus.message?
var parent = Mimulus.getAnyParent();
if (parent) {
var name = parent.tagName.toLowerCase();
var msg = 'Mimulus is on. Editing <' + name + '>.';
if (Mimulus.message) {
msg = msg + ' ' + Mimulus.message;
Mimulus.message = null;
}
Mimulus.win.status = msg;
} else {
var msg = 'Mimulus is on, but uneditable content is selected.';
Mimulus.win.status = msg;
}
} catch(e) {}
},
listen: function(name, method) {
// An addEventListener wrapper
Mimulus.doc.addEventListener(name, method, false);
},
unlisten: function(name, method) {
// A removeEventListener wrapper
Mimulus.doc.removeEventListener(name, method, false);
},
// Editor Metastate Functions
disableMimulus: function() {
// Don't panic! Try to return the browser to a sane state.
// (This disables the extension for the rest of the session.)
// Warning: Only call this if something is majorly screwed up.
if (!Mimulus.getCaret() && !window.onbeforeunload) {
// Signs that we've probably already done this
return false;
}
alert('Error! Turning Mimulus off for safety');
window.onbeforeunload = null;
Mimulus.setCaret(false);
Mimulus.selectTab = function() { };
// @@ Accesskeys, when implemented
Mimulus.changeIcon('off');
},
safeExit: function() {
// Called on the beforeunload event, to prevent losing work
// @@ Command+Q and then Enter does bad things. Firefox bug?
return 'Any unsaved work in Mimulus will be lost.';
},
getCaret: function() {
// Return boolean indicating whether caret browsing is on
try {
var mozpref = '@mozilla.org/preferences;1';
var pref = new Components.Constructor(mozpref, 'nsIPref')();
return pref.GetBoolPref('accessibility.browsewithcaret');
} catch(e) { return false; } // @@
},
setCaret: function(status) {
// Note that F7 will turn caret browsing on, but not Mimulus.
// This sets caret browsing off/on independently of F7. See also:
// http://www.mail-archive.com/dev-tech-xpcom@lists.mozilla.org/msg00531
try {
// Cf. http://xul.andreashalter.ch/
var mozpref = '@mozilla.org/preferences;1';
var pref = new Components.Constructor(mozpref, 'nsIPref')();
pref.SetBoolPref('accessibility.browsewithcaret', status);
Mimulus.caretMode = status;
} catch(e) { alert(e); }
Mimulus.updateStatus();
},
changeIcon: function(state) {
// Set the Mimulus icon state to be either off or on
if (state != 'off' && state != 'on') { return false; }
var status = document.getElementById('mimulus-status');
var uri = 'chrome://mimulus/skin/mimulus-' + state + '.png';
status.setAttribute('src', uri);
return true;
},
// Browser State Functions
getWindowDocument: function() {
// Set the current window and document for easy access. Note that
// this should be called every time the window changes.
// According to http://developer.mozilla.org/en/docs/DOM:window the
// underscore variant, window._content, is deprecated.
Mimulus.win = window.content;
if (Mimulus.win && Mimulus.win.document) {
Mimulus.doc = Mimulus.win.document;
} else { Mimulus.disableMimulus(); }
},
bindMiniListener: function() {
// Set Ctrl+I to listen for turning Mimulus on
Mimulus.listen('keypress', Mimulus.miniListener);
Mimulus.doc.mimulusListening = true;
Mimulus.doc.mimulusRunning = false;
},
normaliseState: function() {
// Make sure that Mimulus is only on per-tab, not cross-browser
// Set window status, caret browsing, unloading, icon, and @@ accesskeys
// This is called a) when a tab is selected, and b) when Mimulus is
// turned off or on.
//
// @@ We should let the user set the default caret browsing state
// in preferences, but that isn't implemented yet.
if (!Mimulus.win || !Mimulus.doc || !Mimulus.doc.mimulusRunning) {
// If Mimulus is not running, be minimally invasive
Mimulus.win.status = null;
window.onbeforeunload = null;
if (Mimulus.getCaret()) { Mimulus.setCaret(false); }
Mimulus.changeIcon('off');
// @@ Accesskeys
} else {
// Otherwise, make sure caret browsing is on, etc.
window.onbeforeunload = Mimulus.safeExit;
if (!Mimulus.getCaret()) { Mimulus.setCaret(true); }
Mimulus.changeIcon('on');
// @@ Accesskeys
}
},
// Main Functions: Get Mimulus running
selectTab: function() {
// A tab has been selected or loaded. If Mimulus is not running here,
// set Ctrl+I to listen to turn it on. Whether it's running here or
// not, set Mimulus to match the current tab's settings.
Mimulus.getWindowDocument();
if (!Mimulus.win) { return false; }
// If Mimulus isn't listening or running, set Ctrl+I to make it run
if (!Mimulus.doc.mimulusListening && !Mimulus.doc.mimulusRunning) {
Mimulus.bindMiniListener();
}
// This isn't a consistency check, but a consistency enforcer
Mimulus.normaliseState();
},
main: function() {
// Go-go gadget Mimulus!
var browser = document.getElementById('content');
if (!browser) { browser = document; }
// Basically, Mimulus.selectTab does the main dispatching
browser.addEventListener('select', Mimulus.selectTab, true);
browser.addEventListener('load', Mimulus.selectTab, true);
}
}
Mimulus.main();
// [EOF]