blob: d6a9695e42dfa55a5299cba60cf07de13720fd36 [file] [log] [blame]
/*------------------------------------------*\
InsertNote plugin for Xinha
___________________________
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 3 of the License, or (at
your option) 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
Lesser General Public License (at http://www.gnu.org/licenses/lgpl.html)
for more details.
InsertNote
----------
Allows for the insertion of footnotes/endnotes. Supports:
* Automatic numbering
* Automatic linking to anchors
* Links back to each specific citation
* Saves previously referenced notes for easy reuse
* Cleanup of unreferenced notes (or references to non-existent notes)
\*------------------------------------------*/
function InsertNote(editor)
{
this.editor = editor;
var cfg = editor.config;
var self = this;
cfg.registerButton({
id : "insert-note",
tooltip : this._lc("Insert footnote"),
image : editor.imgURL("insert-note.gif", "InsertNote"),
textMode : false,
action : function() {
self.show();
}
});
cfg.addToolbarElement("insert-note", "createlink", 1);
};
InsertNote._pluginInfo = {
name : "InsertNote",
version : "0.4",
developer : "Nicholas Bergson-Shilcock",
developer_url : "http://www.nicholasbs.com",
c_owner : "Nicholas Bergson-Shilcock",
sponsor : "The Open Planning Project",
sponsor_url : "http://topp.openplans.org",
license : "LGPL"
};
InsertNote.prototype.onGenerateOnce = function ()
{
this.notes = {};
this.noteNums = {};
this.ID_PREFIX = "InsertNoteID_"; // This should suffice to be unique...
this.MARKER_SUFFIX = "_marker";
this.MARKER_CLASS = "InsertNoteMarker";
this.LINK_BACK_SUFFIX = "_LinkBacks";
this.NOTE_LIST_ID = "InsertNote_NoteList";
this._prepareDialog();
};
InsertNote.prototype._prepareDialog = function()
{
var self = this;
var editor = this.editor;
if (!this.html)
{
Xinha._getback(Xinha.getPluginDir("InsertNote") + "/dialog.html",
function(getback) {
self.html = getback;
self._prepareDialog();
});
return;
}
this.dialog = new Xinha.Dialog(editor, this.html, "InsertNote",
{width: 400,
closeOnEscape: true,
resizable: false,
centered: true,
modal: true
});
this.dialog.getElementById("ok").onclick = function() {self.apply();};
this.dialog.getElementById("cancel").onclick = function() {self.dialog.hide();};
this.dialog.getElementById("noteMenu").onchange = function() {
var noteId = this.options[this.selectedIndex].value;
var text = self.notes[noteId] || "";
var textArea = self.dialog.getElementById("noteContent")
textArea.value = text;
if (text) {
textArea.disabled = true;
} else {
textArea.disabled = false;
textArea.focus();
}
};
this.ready = true;
};
InsertNote.prototype.show = function()
{
if (!this.ready)
{
var self = this;
window.setTimeout(function() {self.show();}, 80);
return;
}
var editor = this.editor;
// All of the Xinha dialogs are part of the main document, not the editor
// document, so we have to use the correct document reference to modify them.
var doc = document;
this.repairNotes();
// Can't insert footnotes inside of other footnotes...
if (this.cursorInFootnote())
{
alert("Footnotes cannot be inserted inside of other footnotes.");
return
}
this.dialog.show();
var popupMenu = this.dialog.getElementById("noteMenu");
while (popupMenu.lastChild.value != "new")
{
// Remove all menu options except "New note"
popupMenu.removeChild(popupMenu.lastChild);
}
var temp, displayName, n;
var orderedIds = this._getOrderedNoteIds();
for (var i=0; i<orderedIds.length; i++)
{
n = orderedIds[i];
temp = doc.createElement("option");
temp.setAttribute("value", n);
displayName = this.notes[n];
displayName = displayName.replace(/<[^>]*>/gi, ""); // XXX TODO: Strip HTML less naively.
if (displayName.length > 50) // Truncate preview text to 50 chars
{
displayName = displayName.substr(0,50) + "...";
}
displayName = this._getNumFromNoteId(n) + ". " + displayName;
temp.appendChild(doc.createTextNode(displayName));
popupMenu.appendChild(temp);
}
var textArea = this.dialog.getElementById("noteContent");
textArea.disabled = false;
textArea.focus();
textArea.value = "";
};
// Checks to see whether or not the cursor is placed inside of a footnote
InsertNote.prototype.cursorInFootnote = function(ignoreMarkers)
{
var ancestors = this.editor.getAllAncestors();
for (var i=0; i<ancestors.length; i++)
{
if (ancestors[i].id == this.NOTE_LIST_ID)
return true;
if (ancestors[i].className == this.MARKER_CLASS && !ignoreMarkers)
return true;
}
return false;
}
/* This function makes sure all of the notes are consistent.
Specifically:
* If a note exists but is never referred to, that note is removed
* Conversely, if a note is referred to but does not exist, the
references are removed
* If the markup for a note marker exists but contains no visible
text, the markup is removed
* All numbering is updated as follows:
- the first note is numbered 1
- each note thereafter is numbered sequentially, unless it has
already been referenced, in which case it's given the same
number as it was the first time it was referenced.
* The note text stored in the this.notes hash is updated to reflect
any changes made by the user (so if the user changes the
citation body in Xinha we don't overwrite these changes next
time we update things)
* The noteNums array is regenerated so as to be up to date
*/
InsertNote.prototype.repairNotes = function()
{
var self = this;
var note;
var markers;
var marker;
var isEmpty;
var temp;
var doc = this.editor._doc;
this.repairScheduled = false;
// First, we remove any markers if they reference a note that doesn't exist.
// If the note does exist, we make sure we have a reference to it by
// calling this._saveNote()
markers = this._getMarkers();
for (var i=0; i<markers.length; i++)
{
marker = markers[i];
// Remove empty markers
isEmpty = false;
if (marker) // check if anchor is empty
isEmpty = marker.innerHTML.match(/^(<span[^>]*>)?(<sup>)?(<a[^>]*>)?[\s]*(<\/a>)?(<\/sup>)?(<\/span>)?$/m);
if (isEmpty)
marker.parentNode.removeChild(marker);
var noteId = this._getIdFromMarkerId(marker.id);
if (!this.notes[noteId])
{
// We don't have this note stored, let's see if
// it actually exists...
note = doc.getElementById(noteId);
if (note)
{
this._saveNote(note);
} else
{
if (marker)
marker.parentNode.removeChild(marker);
}
}
}
// Now we iterate through all the note ids we have stored...
for (var id in this.notes)
{
// Get reference to note
note = doc.getElementById(id);
markers = this._getMarkers(id);
if (note)
{
if (markers.length == 0) // Remove note if it's not referenced anywhere
{
// remove the note
note.parentNode.removeChild(note);
delete this.notes[id];
} else
{
// The note exists and *is* referenced, so we save its contents
this._saveNote(note);
}
} else
{
// Note no longer exists. Remove any references to it.
for (var i=0; i<markers.length; i++)
{
markers[i].parentNode.removeChild(markers[i]);
}
delete this.notes[id];
}
}
// Correct marker numbering
this._updateRefNums();
// At this point we now have all of the note markers correctly
// numbered, an up-to-date hash (this.noteNums) of note numbers
// keyed to note ids, and no stray markers or unreferenced notes.
// Final step: Correct the numbering/order of the actual notes
var noteList = doc.getElementById(this.NOTE_LIST_ID);
var newNoteList = this._createNoteList();
if (newNoteList)
{
noteList.parentNode.replaceChild(newNoteList, noteList);
} else
{
if (noteList)
noteList.parentNode.removeChild(noteList);
}
};
InsertNote.prototype._saveNote = function(note)
{
var doc = this.editor._doc;
// Before saving a note's contents, we do two things:
// 1) we remove the markup we inserted so we don't end up with duplicate links
// 2) check to see if the note is empty, and if so, we remove it
var temp = doc.getElementById(this._getLinkBackSpanId(note.id));
if (temp)
temp.parentNode.removeChild(temp);
if (note.innerHTML)
{
this.notes[note.id] = note.innerHTML;
} else
{
// If we're here then the note is empty,
// so we delete it and any markers for it.
delete this.notes[note.id];
markers = this._getMarkers(note.id);
for (var i=0; i<markers.length; i++)
markers[i].parentNode.removeChild(markers[i]);
}
};
InsertNote.prototype._updateRefNums = function()
{
var runningCount = 1; // first reference is 1
var num;
var marker;
var noteId;
// Reset all note numbering...
this.noteNums = {};
var markers = this._getMarkers();
for (var i=0; i<markers.length; i++)
{
marker = markers[i];
noteId = this._getIdFromMarkerId(marker.id);
if (this.noteNums[noteId])
{
num = this.noteNums[noteId];
} else
{
this.noteNums[noteId] = runningCount;
num = runningCount++;
}
// span -> sup -> anchor
marker.firstChild.firstChild.innerHTML = num;
// XXX TODO: don't use firstChild.firstChild?
}
};
InsertNote.prototype.apply = function()
{
var editor = this.editor;
var param = this.dialog.hide();
var noteId = param['noteMenu'].value;
var newNote = (noteId == "new");
var noteContent = param['noteContent'];
var doc = editor._doc;
if (newNote) // Inserting a new note
noteId = this._getNextId(this.ID_PREFIX);
this.notes[noteId] = noteContent;
var markerTemplate = this._createNoteMarker(noteId)
editor.insertNodeAtSelection(markerTemplate);
var marker = doc.getElementById(markerTemplate.id);
var currentNoteList = doc.getElementById(this.NOTE_LIST_ID);
var newNoteList = this._createNoteList();
var body = doc.body;
if (currentNoteList)
{
body.replaceChild(newNoteList, currentNoteList);
} else
{
body.appendChild(newNoteList);
}
var newel = doc.createTextNode('\u00a0');
if (marker.nextSibling)
{
newel = marker.parentNode.insertBefore(newel, marker.nextSibling);
} else
{
newel = marker.parentNode.appendChild(newel);
}
this.repairNotes();
editor.selectNodeContents(newel,false);
};
InsertNote.prototype._createNoteList = function()
{
var doc = this.editor._doc;
var noteList = doc.createElement("ol");
noteList.id = this.NOTE_LIST_ID;
var orderedIds = this._getOrderedNoteIds();
if (orderedIds.length == 0)
return null;
var note, id;
for (var i=0; i<orderedIds.length; i++)
{
id = orderedIds[i];
note = this._createNoteNode(id, this.notes[id]);
noteList.appendChild(note);
}
return noteList;
};
InsertNote.prototype._createNoteMarker = function(noteId)
{
// Create the note marker, i.e., the # superscript
var doc = this.editor._doc;
var noteNum = this._getNumFromNoteId(noteId);
var link = doc.createElement("a");
link.href = "#" + noteId;
link.innerHTML = noteNum;
var superscript = doc.createElement("sup");
superscript.appendChild(link);
var span = doc.createElement("span");
span.id = this._getNextMarkerId(noteId);
span.className = this.MARKER_CLASS;
span.appendChild(superscript);
return span
};
InsertNote.prototype._createNoteNode = function(noteId, noteContent)
{
var doc = this.editor._doc;
var anchor;
var superscript = doc.createElement("sup");
var markers = this._getMarkers(noteId);
var temp;
for (var i=0; i<markers.length; i++)
{
anchor = doc.createElement("a");
if (markers.length == 1)
{
anchor.innerHTML = "^";
}
else
{
anchor.innerHTML = this._letterNum(i);
}
anchor.href = "#" + markers[i].id;
superscript.appendChild(anchor);
if (i < markers.length-1)
{
temp = doc.createTextNode(", ");
superscript.appendChild(temp);
}
}
var span = doc.createElement("span");
span.id = this._getLinkBackSpanId(noteId);
span.appendChild(superscript);
var li = doc.createElement("li");
li.id = noteId;
li.innerHTML = noteContent;
li.appendChild(span);
return li;
};
InsertNote.prototype._getNextId = function(prefix)
{
// We can't just use Xinha.uniq here because it doesn't
// work across editing sessions. E.g., it will break if
// the user copies the html from one editor to another.
var id = Xinha.uniq(prefix);
while (this.editor._doc.getElementById(id))
id = Xinha.uniq(prefix);
return id;
};
InsertNote.prototype._getOrderedNoteIds = function()
{
var self = this;
var orderedIds = new Array();
for (var id in this.notes)
orderedIds.push(id);
orderedIds.sort(function(a,b) {
var n1 = self._getNumFromNoteId(a);
var n2 = self._getNumFromNoteId(b);
return (n1 - n2);
});
return orderedIds;
};
InsertNote.prototype._getNumFromNoteId = function(noteId)
{
if (!this.noteNums[noteId])
return 0;
return this.noteNums[noteId];
};
// Return array of all markers that reference the specified note.
// If noteId is not supplied, *all* markers are returned.
InsertNote.prototype._getMarkers = function(noteId)
{
var doc = this.editor._doc;
var markers = Xinha.getElementsByClassName(doc, this.MARKER_CLASS);
if (!noteId)
return markers;
var els = new Array();
for (var i=0; i<markers.length; i++)
{
if (this._getIdFromMarkerId(markers[i].id) == noteId)
{
els.push(markers[i]);
}
}
return els;
};
InsertNote.prototype._getNextMarkerId = function(noteId)
{
return this._getNextId(noteId + this.MARKER_SUFFIX);
};
InsertNote.prototype._getIdFromMarkerId = function(markerId)
{
return markerId.substr(0, markerId.search(this.MARKER_SUFFIX));
};
InsertNote.prototype._getLinkBackSpanId = function(noteId)
{
return noteId + this.LINK_BACK_SUFFIX;
};
InsertNote.prototype._lc = function(string)
{
return Xinha._lc(string, "InsertNote");
};
// Probably overkill, but it does the job
InsertNote.prototype._letterNum = function(num)
{
var letters = "abcdefghijklmnopqrstuvwxyz";
var len = Math.floor(num/letters.length) + 1;
var s = "";
for (var i=0; i<len; i++)
s += letters.substr(num % letters.length,1);
return s;
};
InsertNote.prototype.inwardHtml = function(html)
{
return html;
};
InsertNote.prototype.outwardHtml = function(html)
{
this.repairNotes();
return this.editor.getHTML();
};
InsertNote.prototype.onKeyPress = function (event)
{
// This seems a bit hacky, but I don't presently see
// a better way.
//@NOTE: 8 = Backspace, 46 = Delete; this undocumented
// function apears to be for handling delete note references
// to have the note automatically deleted also
if (event.keyCode == 8 || event.keyCode == 46)
{
var self = this;
if (!this.repairScheduled && !this.cursorInFootnote(true)) // ignore if in a footnote
{
this.repairScheduled = true;
window.setTimeout(function() { self.repairNotes(); }, 1000);
}
}
return false;
};
/**
*
* @param {String} mode either 'textmode' or 'wysiwyg'
*/
InsertNote.prototype.onMode = function (mode)
{
return false;
};
/**
*
* @param {String} mode either 'textmode' or 'wysiwyg'
*/
InsertNote.prototype.onBeforeMode = function (mode)
{
return false;
};