// Copyright (c) 2011, 2012 Anton Karl Ingason, Aaron Ecay, Jana Beck// This file is part of the Annotald program for annotating// phrase-structure treebanks in the Penn Treebank style.// This file is distributed under the terms of the GNU 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 for more details.// You should have received a copy of the GNU Lesser General Public// License along with this program. If not, see// <http://www.gnu.org/licenses/>.// Global TODOs:// - (AWE) make the dash-tags modular, so that ctrl x -> set XXX, w ->// set NP-SBJ doesn't blow away the XXX// - (AWE) what happens when you delete e.g. an NP node w metadata?// Does the metadata get blown away? pro/demoted? Does deletion fail, or// raise a prompt?// - strict mode// - modularize doc -- namespaces?// - make key commands for case available// TODO: for unsaved ch warning: use tolabeledbrax, not html...html is// sensitive to search highlight, selection, etc// Table of contents:// * Initialization// * User configuration// ** CSS styles// ** Key bindings// * UI functions// ** Event handlers// ** Context Menu// ** Messages// ** Dialog boxes// ** Selection// ** Metadata editor// ** Splitting words// ** Editing parts of the tree// ** Search// *** HTML strings and other globals// *** Event handlers// *** Helper functions// *** Search interpretation function// *** The core search function// * Tree manipulations// ** Movement// ** Creation// ** Deletion// ** Label manipulation// ** Coindexation// * Server-side operations// ** Saving// *** Save helper function// ** Validating// ** Advancing through the file// ** Idle/resume// ** Quitting// * Undo/redo// * Misc// * Misc (candidates to move to utils)// End TOC// ===== Initialization/**
* This variable holds the selected node, or "start" node if multiple
* selection is in effect. Otherwise undefined.
*
* @type DOM Node
*/var startnode = null;
/**
* This variable holds the "end" node if multiple selection is in effect.
* Otherwise undefined.
*
* @type DOM Node
*/var endnode = null;
var ctrlKeyMap = {};
var shiftKeyMap = {};
var regularKeyMap = {};
var startuphooks = [];
var last_event_was_mouse = false;
var lastsavedstate = "";
var globalStyle = $('<style type="text/css"></style>');
var lemmataStyleNode, lemmataHidden = true;
(function () {
lemmataStyleNode = document.createElement("style");
lemmataStyleNode.setAttribute("type", "text/css");
document.getElementsByTagName("head")[0].appendChild(lemmataStyleNode);
lemmataStyleNode.innerHTML = ".lemma { display: none; }";
})();
var currentIndex = 1; // TODO: move to where it goesif (typeof String.prototype.startsWith !== 'function') {
String.prototype.startsWith = function(str) {
return (this.substr(0, str.length) === str);
};
}
if (typeof String.prototype.endsWith !== 'function') {
String.prototype.endsWith = function(str) {
return (this.substr(this.length - str.length) === str);
};
}
function navigationWarning() {
if ($("#editpane").html() != lastsavedstate) {
return"Unsaved changes exist, are you sure you want to leave the page?";
}
return undefined;
}
function logUnload() {
logEvent("page-unload");
}
addStartupHook(function() {
logEvent("page-load");
});
function assignEvents() {
// load custom commands from user settings file customCommands();
document.body.onkeydown = handleKeyDown;
$("#sn0").mousedown(handleNodeClick);
document.body.onmouseup = killTextSelection;
$("#butsave").mousedown(save);
$("#butundo").mousedown(undo);
$("#butredo").mousedown(redo);
$("#butidle").mousedown(idle);
$("#butexit").unbind("click").click(quitServer);
$("#butvalidate").unbind("click").click(validateTrees);
$("#butnexterr").unbind("click").click(nextValidationError);
$("#butnexttree").unbind("click").click(nextTree);
$("#butprevtree").unbind("click").click(prevTree);
$("#butgototree").unbind("click").click(goToTree);
$("#editpane").mousedown(clearSelection);
$("#conMenu").mousedown(hideContextMenu);
$(document).mousewheel(handleMouseWheel);
window.onbeforeunload = navigationWarning;
window.onunload = logUnload;
}
function styleIpNodes() {
for (var i = 0; i < ipnodes.length; i++) {
styleTag(ipnodes[i], "border-top: 1px solid black;" +
"border-bottom: 1px solid black;" +
"background-color: #C5908E;");
}
}
function addStartupHook(fn) {
startuphooks.push(fn);
}
function documentReadyHandler() {
// TODO: something is very slow here; profile// TODO: move some of this into hooks assignEvents();
styleIpNodes();
setupCommentTypes();
globalStyle.appendTo("head");
_.each(startuphooks, function (hook) {
hook();
});
lastsavedstate = $("#editpane").html();
}
$(document).ready(function () {
documentReadyHandler();
});
// ===== User configuration// ========== CSS stylesfunction addStyle(string) {
var style = globalStyle.text() + "\n" + string;
globalStyle.text(style);
}
/**
* Add a css style for a certain tag.
*
* @param {String} tagName The tag which to style. Will match instances of
* the given tag with additional trailing dash tags.
* @param {String} css The css style declarations to associate with the tag.
*/function styleTag(tagName, css) {
addStyle('*[class*=" ' + tagName + '-"],*[class*=" ' + tagName +
' "],*[class$=" ' + tagName + '"],[class*=" ' + tagName +
'="] { ' + css + ' }');
}
/**
* Add a css style for a certain dash tag.
*
* @param {String} tagName The tag which to style. Will match any node with
* this dash tag. Should not itself have leading or trailing dashes.
* @param {String} css The css style declarations to associate with the tag.
*/function styleDashTag(tagName, css) {
addStyle('*[class*="-' + tagName + '-"],*[class*="-' + tagName +
' "],*[class$="-' + tagName + '"],[class*="-' + tagName +
'="] { ' + css + ' }');
}
/**
* A convenience function to wrap {@link styleTag}.
*
* @param {Array} tagNames Tags to style.
* @param {String} css The css style declarations to associate with the tags.
*/function styleTags(tagNames, css) {
for (var i = 0; i < tagNames.length; i++) {
styleTag(tagNames[i], css);
}
}
// ========== Key bindings/**
* Add a keybinding command.
*
* Calls to this function should be in the `settings.js` file, grouped in a
* function called `customCommands`
*
* @param {Object} dict a mapping of properties of the keybinding. Can
* contain:
*
* - `keycode`: the numeric keycode for the binding (mandatory)
* - `shift`: true if this is a binding with shift pressed (optional)
* - `ctrl`: true if this is a binding with control pressed (optional)
*
* @param {Function} fn the function to associate with the keybinding. Any
* further arguments to the `addCommand` function are passed to `fn` on each
* invocation.
*/function addCommand(dict, fn) {
var commandMap;
if (dict.ctrl) {
commandMap = ctrlKeyMap;
} elseif (dict.shift) {
commandMap = shiftKeyMap;
} else {
commandMap = regularKeyMap;
}
commandMap[dict.keycode] = {
func: fn,
args: Array.prototype.slice.call(arguments, 2)
};
}
// ===== UI functions// ========== Event handlersfunction killTextSelection() {
if (dialogShowing) return;
var sel = window.getSelection();
sel.removeAllRanges();
}
function handleMouseWheel(e, delta) {
if (e.shiftKey && startnode) {
var nextNode;
if (delta < 0) { // negative means scroll down, counterintuitively nextNode = $(startnode).next().get(0);
} else {
nextNode = $(startnode).prev().get(0);
}
if (nextNode) {
selectNode(nextNode);
scrollToShowSel();
}
}
}
var keyDownHooks = [];
function addKeyDownHook(fn) {
keyDownHooks.push(fn);
}
function handleKeyDown(e) {
if ((e.ctrlKey && e.shiftKey) || e.metaKey || e.altKey) {
// unsupported modifier combinationsreturntrue;
}
if (e.keyCode == 16 || e.keyCode == 17 || e.keyCode == 18) {
// Don't handle shift, ctrl, and meta pressesreturntrue;
}
// Becasuse of bug #75, we don't want to count keys used for scrolling as// keypresses that interrupt a chain of mouse clicks.if (! _.contains([33, //page up34, // page down37,38,39,40// arrow keys ], e.keyCode)) {
last_event_was_mouse = false;
}
var commandMap;
if (e.ctrlKey) {
commandMap = ctrlKeyMap;
} elseif (e.shiftKey) {
commandMap = shiftKeyMap;
} else {
commandMap = regularKeyMap;
}
if (!commandMap[e.keyCode]) {
returntrue;
}
e.preventDefault();
var theFn = commandMap[e.keyCode].func;
var theArgs = commandMap[e.keyCode].args;
_.each(keyDownHooks, function (fn) {
fn({
keyCode: e.keyCode,
shift: e.shiftKey,
ctrl: e.ctrlKey
},
theFn,
theArgs);
});
theFn.apply(undefined, theArgs);
if (!theFn.async) {
undoBarrier();
}
returnfalse;
}
var clickHooks = [];
function addClickHook(fn) {
clickHooks.push(fn);
}
function handleNodeClick(e) {
e = e || window.event;
var element = (e.target || e.srcElement);
saveMetadata();
if (e.button == 2) {
// rightclickif (startnode && !endnode) {
if (startnode != element) {
e.stopPropagation();
moveNode(element);
} else {
showContextMenu(e);
}
} elseif (startnode && endnode) {
e.stopPropagation();
moveNodes(element);
} else {
showContextMenu(e);
}
} else {
// leftclick hideContextMenu();
if (e.shiftKey && startnode) {
selectNode(element, true);
e.preventDefault(); // Otherwise, this sets the text// selection in the browser... } else {
selectNode(element);
if (e.ctrlKey) {
makeNode("XP");
}
}
}
_.each(clickHooks, function (fn) {
fn(e.button);
});
e.stopPropagation();
last_event_was_mouse = true;
undoBarrier();
}
// ========== Context Menufunction showContextMenu(e) {
var element = e.target || e.srcElement;
if (element == document.getElementById("sn0")) {
clearSelection();
return;
}
var left = e.pageX;
var top = e.pageY;
left = left + "px";
top = top + "px";
var conl = $("#conLeft"),
conr = $("#conRight"),
conrr = $("#conRightest"),
conm = $("#conMenu");
conl.empty();
loadContextMenu(element);
// Make the columns equally high conl.height("auto");
conr.height("auto");
conrr.height("auto");
var h = _.max([conl,conr,conrr], function (x) { return x.height(); });
conl.height(h);
conr.height(h);
conrr.height(h);
conm.css("left",left);
conm.css("top",top);
conm.css("visibility","visible");
}
function hideContextMenu() {
$("#conMenu").css("visibility","hidden");
}
// ========== Messages/**
* Show the message history.
*/function showMessageHistory() {
showDialogBox("Messages", "<textarea readonly='readonly' " +
"style='width:100%;height:100%;'>" +
messageHistory + "</textarea>");
}
addStartupHook(function () {
$("#messagesTitle").click(showMessageHistory);
});
// ========== Dialog boxesvar dialogShowing = false;
/**
* Show a dialog box.
*
* This function creates keybindings for the escape (to close dialog box) and
* return (caller-specified behavior) keys.
*
* @param {String} title the title of the dialog box
* @param {String} html the html to display in the dialog box
* @param {Function} returnFn a function to call when return is pressed
* @param {Function} hideHook a function to run when hiding the dialog box
*/function showDialogBox(title, html, returnFn, hideHook) {
document.body.onkeydown = function (e) {
if (e.keyCode == 27) { // escapeif (hideHook) {
hideHook();
}
hideDialogBox();
} elseif (e.keyCode == 13 && returnFn) {
returnFn();
}
};
html = '<div class="menuTitle">' + title + '</div>' +
'<div id="dialogContent">' + html + '</div>';
$("#dialogBox").html(html).get(0).style.visibility = "visible";
$("#dialogBackground").get(0).style.visibility = "visible";
dialogShowing = true;
}
/**
* Hide the displayed dialog box.
*/function hideDialogBox() {
$("#dialogBox").get(0).style.visibility = "hidden";
$("#dialogBackground").get(0).style.visibility = "hidden";
document.body.onkeydown = handleKeyDown;
dialogShowing = false;
}
/**
* Set a handler for the enter key in a text box.
* @private */function setInputFieldEnter(field, fn) {
field.keydown(function (e) {
if (e.keyCode == 13) {
fn();
returnfalse;
} else {
returntrue;
}
});
}
// ========== Selection/**
* Select a node, and update the GUI to reflect that.
*
* @param {DOM Node} node the node to be selected
* @param {Boolean} force if true, force this node to be a secondary
* selection, even if it wouldn't otherwise be.
*/function selectNode(node, force) {
if (node) {
if (!(node instanceof Node)) {
try {
throw Error("foo");
} catch (e) {
console.log("selecting a non-node: " + e.stack);
}
}
if (node == document.getElementById("sn0")) {
clearSelection();
return;
}
while (!$(node).hasClass("snode") && node != document) {
node = node.parentNode;
}
if (node == startnode) {
startnode = null;
if (endnode) {
startnode = endnode;
endnode = null;
}
} elseif (startnode === null) {
startnode = node;
} else {
if (startnode && (last_event_was_mouse || force)) {
if (node == endnode) {
endnode = null;
} else {
endnode = node;
}
} else {
endnode = null;
startnode = node;
}
}
updateSelection();
} else {
try {
throw Error("foo");
} catch (e) {
console.log("tried to select something falsey: " + e.stack);
}
}
}
/**
* Remove any selection of nodes.
*/function clearSelection() {
saveMetadata();
window.event.preventDefault();
startnode = endnode = null;
updateSelection();
hideContextMenu();
}
function updateSelection() {
// update selection display $('.snodesel').removeClass('snodesel');
if (startnode) {
$(startnode).addClass('snodesel');
}
if (endnode) {
$(endnode).addClass('snodesel');
}
updateMetadataEditor();
updateUrtext();
}
function updateUrtext() {
if (startnode) {
var tr = getTokenRoot($(startnode));
var str = currentTextPretty($(tr), " ");
var md = JSON.parse(tr.getAttribute("data-metadata"));
var id = (md || {ID: ""}).ID;
if (id !== "") {
id = "<b>" + id + "</b>: ";
}
$("#urtext").html(id + escapeHtml(str)).show();
} else {
$("#urtext").hide();
}
}
addStartupHook(function () {
$("#urtext").hide();
});
/**
* Scroll the page so that the first selected node is visible.
*/function scrollToShowSel() {
function isTopVisible(elem) {
var docViewTop = $(window).scrollTop();
var docViewBottom = docViewTop + $(window).height();
var elemTop = $(elem).offset().top;
return ((elemTop <= docViewBottom) && (elemTop >= docViewTop));
}
if (!isTopVisible(startnode)) {
window.scroll(0, $(startnode).offset().top - $(window).height() * 0.25);
}
}
// ========== Metadata editorfunction saveMetadata() {
if ($("#metadata").html() !== "") {
$(startnode).attr("data-metadata",
JSON.stringify(formToDictionary($("#metadata"))));
}
}
function updateMetadataEditor() {
if (!startnode || endnode) {
$("#metadata").html("");
return;
}
var addButtonHtml = '<input type="button" id="addMetadataButton" ' +
'value="Add" />';
$("#metadata").html(dictionaryToForm(getMetadata($(startnode))) +
addButtonHtml);
$("#metadata").find(".metadataField").change(saveMetadata).
focusout(saveMetadata).keydown(function (e) {
if (e.keyCode == 13) {
$(e.target).blur();
}
e.stopPropagation();
returntrue;
});
$("#metadata").find(".key").click(metadataKeyClick);
$("#addMetadataButton").click(addMetadataDialog);
}
function metadataKeyClick(e) {
var keyNode = e.target;
var html = 'Name: <input type="text" ' +
'id="metadataNewName" value="' + $(keyNode).text() +
'" /><div id="dialogButtons"><input type="button" value="Save" ' +
'id="metadataKeySave" /><input type="button" value="Delete" ' +
'id="metadataKeyDelete" /></div>';
showDialogBox("Edit Metadata", html);
// TODO: make focus go to end, or select whole thing? $("#metadataNewName").focus();
function saveMetadataInner() {
$(keyNode).text($("#metadataNewName").val());
hideDialogBox();
saveMetadata();
}
function deleteMetadata() {
$(keyNode).parent().remove();
hideDialogBox();
saveMetadata();
}
$("#metadataKeySave").click(saveMetadataInner);
setInputFieldEnter($("#metadataNewName"), saveMetadataInner);
$("#metadataKeyDelete").click(deleteMetadata);
}
function addMetadataDialog() {
// TODO: allow specifying value too in initial dialog?var html = 'New Name: <input type="text" id="metadataNewName" value="NEW" />' +
'<div id="dialogButtons"><input type="button" id="addMetadata" ' +
'value="Add" /></div>';
showDialogBox("Add Metatata", html);
function addMetadata() {
var oldMetadata = formToDictionary($("#metadata"));
oldMetadata[$("#metadataNewName").val()] = "NEW";
$(startnode).attr("data-metadata", JSON.stringify(oldMetadata));
updateMetadataEditor();
hideDialogBox();
}
$("#addMetadata").click(addMetadata);
setInputFieldEnter($("#metadataNewName"), addMetadata);
}
// ========== Splitting wordsfunction splitWord() {
if (!startnode || endnode) return;
if (!isLeafNode($(startnode)) || isEmpty(wnodeString($(startnode)))) return;
undoBeginTransaction();
touchTree($(startnode));
var wordSplit = wnodeString($(startnode)).split("-");
var origWord = wordSplit[0];
var startsWithAt = false, endsWithAt = false;
if (origWord[0] == "@") {
startsWithAt = true;
origWord = origWord.substr(1);
}
if (origWord.substr(origWord.length - 1, 1) == "@") {
endsWithAt = true;
origWord = origWord.substr(0, origWord.length - 1);
}
var origLemma = "XXX";
if (wordSplit.length == 2) {
origLemma = "@" + wordSplit[1] + "@";
}
var origLabel = getLabel($(startnode));
function doSplit() {
var words = $("#splitWordInput").val().split("@");
if (words.join("") != origWord) {
displayWarning("The two new words don't match the original. Aborting");
undoAbortTransaction();
return;
}
if (words.length < 0) {
displayWarning("You have not specified where to split the word.");
undoAbortTransaction();
return;
}
if (words.length > 2) {
displayWarning("You can only split in one place at a time.");
undoAbortTransaction();
return;
}
var labelSplit = origLabel.split("+");
var secondLabel = "X";
if (labelSplit.length == 2) {
setLeafLabel($(startnode), labelSplit[0]);
secondLabel = labelSplit[1];
}
setLeafLabel($(startnode), (startsWithAt ? "@" : "") + words[0] + "@");
var hasLemma = $(startnode).find(".lemma").size() > 0;
makeLeaf(false, secondLabel, "@" + words[1] + (endsWithAt ? "@" : ""));
if (hasLemma) {
// TODO: move to something like foo@1 and foo@2 for the two pieces// of the lemmata addLemma(origLemma);
}
hideDialogBox();
undoEndTransaction();
undoBarrier();
}
var html = "Enter an at-sign at the place to split the word: \
<input type='text' id='splitWordInput' value='" + origWord +
"' /><div id='dialogButtons'><input type='button' id='splitWordButton'\
value='Split' /></div>";
showDialogBox("Split word", html, doSplit);
$("#splitWordButton").click(doSplit);
$("#splitWordInput").focus();
}
splitWord.async = true;
// ========== Editing parts of the tree// TODO: document entry points better// DONE(?): split these fns up...they are monsters. (or split to sep. file?)/**
* Perform an appropriate editing operation on the selected node.
*/function editNode() {
if (getLabel($(startnode)) == "CODE" &&
_.contains(commentTypes,
// strip leading { and the : and everything after wnodeString($(startnode)).substr(1).split(":")[0])
) {
editComment();
} else {
displayRename();
}
}
var commentTypeCheckboxes = "Type of comment: ";
function setupCommentTypes() {
for (var i = 0; i < commentTypes.length; i++) {
commentTypeCheckboxes +=
'<input type="radio" name="commentType" value="' +
commentTypes[i] + '" id="commentType' + commentTypes[i] +
'" /> ' + commentTypes[i];
}
}
function editComment() {
if (!startnode || endnode) return;
touchTree($(startnode));
var commentRaw = $.trim(wnodeString($(startnode)));
var commentType = commentRaw.split(":")[0];
// remove the { commentType = commentType.substring(1);
var commentText = commentRaw.split(":")[1];
commentText = commentText.substring(0, commentText.length - 1);
// regex because string does not give global search. commentText = commentText.replace(/_/g, " ");
showDialogBox("Edit Comment",
'<textarea id="commentEditBox">' +
commentText + '</textarea><div id="commentTypes">' +
commentTypeCheckboxes + '</div><div id="dialogButtons">' +
'<input type="button"' +
'id="commentEditButton" value="Save" /></div>');
$("input:radio[name=commentType]").val([commentType]);
$("#commentEditBox").focus().get(0).setSelectionRange(commentText.length,
commentText.length);
function editCommentDone (change) {
if (change) {
var newText = $.trim($("#commentEditBox").val());
if (/_|\n|:|\}|\{|\(|\)/.test(newText)) {
// TODO(AWE): slicker way of indicating errors... alert("illegal characters in comment: illegal characters are" +
" _, :, {}, (), and newline");
// hideDialogBox(); $("#commentEditBox").val(newText);
return;
}
newText = newText.replace(/ /g, "_");
commentType = $("input:radio[name=commentType]:checked").val();
setLabelLL($(startnode).children(".wnode"),
"{" + commentType + ":" + newText + "}");
}
hideDialogBox();
}
$("#commentEditButton").click(editCommentDone);
$("#commentEditBox").keydown(function (e) {
if (e.keyCode == 13) {
// return editCommentDone(true);
returnfalse;
} elseif (e.keyCode == 27) {
editCommentDone(false);
returnfalse;
} else {
returntrue;
}
});
}
/**
* Return the JQuery object with the editor for a leaf node.
* @private */function leafEditorHtml(label, word, lemma) {
// Single quotes mess up the HTML code.if (lemma) lemma = lemma.replace(/'/g, "'");
word = word.replace(/'/g, "'");
label = label.replace(/'/g, "'");
var editorHtml = "<div id='leafeditor' class='snode'>" +
"<input id='leafphrasebox' class='labeledit' type='text' value='" +
label +
"' /><input id='leaftextbox' class='labeledit' type='text' value='" +
word +
"' " + (!isEmpty(word) ? "disabled='disabled'" : "") + " />";
if (lemma) {
editorHtml += "<input id='leaflemmabox' class='labeledit' " +
"type='text' value='" + lemma + "' />";
}
editorHtml += "</div>";
return $(editorHtml);
}
/**
* Return the JQuery object with the replacement after editing a leaf node.
* @private
*/
function leafEditorReplacement(label, word, lemma) {
if (lemma) {
lemma = lemma.replace(/</g,"<");
lemma = lemma.replace(/>/g,">");
lemma = lemma.replace(/'/g,"'");
}
word = word.replace(/</g,"<");
word = word.replace(/>/g,">");
word = word.replace(/'/g,"'");
// TODO: test for illegal chars in label
label = label.toUpperCase();
var replText = "<div class='snode'>" + label +
" <span class='wnode'>" + word;
if (lemma) {
replText += "<span class='lemma'>-" +
lemma + "</span>";
}
replText += "</span></div>";
return $(replText);
}
/**
* Edit the selected node
*
* If the selected node is a terminal, edit its label, and lemma. The text is
* available for editing if it is an empty node (trace, comment, etc.). If a
* non-terminal, edit the node label.
*/
function displayRename() {
// Inner functions
function space(event) {
var element = (event.target || event.srcElement);
$(element).val($(element).val());
event.preventDefault();
}
function postChange(newNode) {
if (newNode) {
updateCssClass(newNode, oldClass);
startnode = endnode = null;
updateSelection();
document.body.onkeydown = handleKeyDown;
$("#sn0").mousedown(handleNodeClick);
$("#editpane").mousedown(clearSelection);
$("#butundo").prop("disabled", false);
$("#butredo").prop("disabled", false);
$("#butsave").prop("disabled", false);
}
}
// Begin code
if (!startnode || endnode) {
return;
}
undoBeginTransaction();
touchTree($(startnode));
document.body.onkeydown = null;
$("#sn0").unbind('mousedown');
$("#editpane").unbind('mousedown');
$("#butundo").prop("disabled", true);
$("#butredo").prop("disabled", true);
$("#butsave").prop("disabled", true);
var label = getLabel($(startnode));
var oldClass = parseLabel(label);
if ($(startnode).children(".wnode").size() > 0) {
// this is a terminal
var word, lemma;
// is this right? we still want to allow editing of index, maybe?
var isLeaf = isLeafNode($(startnode));
if ($(startnode).children(".wnode").children(".lemma").size() > 0) {
var preword = $.trim($(startnode).children().first().text());
preword = preword.split("-");
lemma = preword.pop();
word = preword.join("-");
} else {
word = $.trim($(startnode).children().first().text());
}
$(startnode).replaceWith(leafEditorHtml(label, word, lemma));
$("#leafphrasebox,#leaftextbox,#leaflemmabox").keydown(
function(event) {
var replText, replNode;
if (event.keyCode == 32) {
space(event);
}
if (event.keyCode == 27) {
replNode = leafEditorReplacement(label, word, lemma);
$("#leafeditor").replaceWith(replNode);
postChange(replNode);
undoAbortTransaction();
}
if (event.keyCode == 13) {
var newlabel = $("#leafphrasebox").val().toUpperCase();
var newword = $("#leaftextbox").val();
var newlemma;
if (lemma) {
newlemma = $('#leaflemmabox').val(); }
if (isLeafNode) {
if (typeof testValidLeafLabel !== "undefined") {
if (!testValidLeafLabel(newlabel)) {
displayWarning("Not a valid leaf label: '" +
newlabel + "'.");
return;
}
}
} else {
if (typeof testValidPhraseLabel !== "undefined") {
if (!testValidPhraseLabel(newlabel)) {
displayWarning("Not a valid phrase label: '" +
newlabel + "'.");
return;
}
}
}
if (newword + newlemma === "") {
displayWarning("Cannot create an empty leaf.");
return;
}
replNode = leafEditorReplacement(newlabel, newword,
newlemma);
$("#leafeditor").replaceWith(replNode);
postChange(replNode);
undoEndTransaction();
undoBarrier();
}
if (event.keyCode == 9) {
var element = (event.target || event.srcElement);
if ($("#leafphrasebox").is(element)) {
if (!$("#leaftextbox").attr("disabled")) {
$("#leaftextbox").focus();
} elseif ($("#leaflemmabox").length == 1) {
$("#leaflemmabox").focus();
}
} elseif ($("#leaftextbox").is(element)) {
if ($("#leaflemmabox").length == 1) {
$("#leaflemmabox").focus();
} else {
$("#leafphrasebox").focus();
}
} elseif ($("#leaflemmabox").is(element)) {
$("#leafphrasebox").focus();
}
event.preventDefault();
}
}).mouseup(function editLeafClick(e) {
e.stopPropagation();
});
setTimeout(function(){ $("#leafphrasebox").focus(); }, 10);
} else {
// this is not a terminalvar editor = $("<input id='labelbox' class='labeledit' " +
"type='text' value='" + label + "' />");
var origNode = $(startnode);
var isWordLevelConj =
origNode.children(".snode").children(".snode").size() === 0 &&
// TODO: make configurable origNode.children(".CONJ") .size() > 0;
textNode(origNode).replaceWith(editor);
$("#labelbox").keydown(
function(event) {
if (event.keyCode == 9) {
event.preventDefault();
}
if (event.keyCode == 32) {
space(event);
}
if (event.keyCode == 27) {
$("#labelbox").replaceWith(label + " ");
postChange(origNode);
undoAbortTransaction();
}
if (event.keyCode == 13) {
var newphrase = $("#labelbox").val().toUpperCase();
if (typeof testValidPhraseLabel !== "undefined") {
if (!(testValidPhraseLabel(newphrase) ||
(typeof testValidLeafLabel !== "undefined" &&
isWordLevelConj &&
testValidLeafLabel(newphrase)))) {
displayWarning("Not a valid phrase label: '" +
newphrase + "'.");
return;
}
}
$("#labelbox").replaceWith(newphrase + " ");
postChange(origNode);
undoEndTransaction();
undoBarrier();
}
}).mouseup(function editNonLeafClick(e) {
e.stopPropagation();
});
setTimeout(function(){ $("#labelbox").focus(); }, 10);
}
}
displayRename.async = true;
/**
* Edit the lemma of a terminal node.
*/function editLemma() {
// Inner functionsfunction space(event) {
var element = (event.target || event.srcElement);
$(element).val($(element).val());
event.preventDefault();
}
function postChange() {
startnode = null; endnode = null;
updateSelection();
document.body.onkeydown = handleKeyDown;
$("#sn0").mousedown(handleNodeClick);
$("#undo").attr("disabled", false);
$("#redo").attr("disabled", false);
$("#save").attr("disabled", false);
undoBarrier();
}
// Begin codevar childLemmata = $(startnode).children(".wnode").children(".lemma");
if (!startnode || endnode || childLemmata.size() != 1) {
return;
}
document.body.onkeydown = null;
$("#sn0").unbind('mousedown');
undoBeginTransaction();
touchTree($(startnode));
$("#undo").attr("disabled", true);
$("#redo").attr("disabled", true);
$("#save").attr("disabled", true);
var lemma = $(startnode).children(".wnode").children(".lemma").text();
lemma = lemma.substring(1);
var editor=$("<span id='leafeditor' class='wnode'><input " +
"id='leaflemmabox' class='labeledit' type='text' value='" +
lemma + "' /></span>");
$(startnode).children(".wnode").children(".lemma").replaceWith(editor);
$("#leaflemmabox").keydown(
function(event) {
if (event.keyCode == 9) {
event.preventDefault();
}
if (event.keyCode == 32) {
space(event);
}
if (event.keyCode == 13) {
var newlemma = $('#leaflemmabox').val();
newlemma = newlemma.replace("<","<");
newlemma = newlemma.replace(">",">");
newlemma = newlemma.replace(/'/g,"'");
$("#leafeditor").replaceWith("<span class='lemma'>-" +
newlemma + "</span>");
postChange();
}
// TODO: escape
});
setTimeout(function(){ $("#leaflemmabox").focus(); }, 10);
}
editLemma.async = true;
// ========== Search
// TODO: anchor right end of string, so that NP does not match NPR, only NP or NP-X (???)
// TODO: profile this and optimize like crazy.
// =============== HTML strings and other globals
/**
* The HTML code for a regular search node
* @private
* @constant
*/
// TODO: make the presence of a lemma search option contingent on the presence
// of lemmata in the corpus
var searchnodehtml = "<div class='searchnode'>" +
"<div class='searchadddelbuttons'>" +
"<input type='button' class='searchornodebut' " +
"value='|' />" +
"<input type='button' class='searchdeepnodebut' " +
"value='D' />" +
"<input type='button' class='searchprecnodebut' " +
"value='>' />" +
"<input type='button' class='searchdelnodebut' " +
"value='-' />" +
"<input type='button' class='searchnewnodebut' " +
"value='+' />" +
"</div>" +
"<select class='searchtype'><option>Label</option>" +
"<option>Text</option><option>Lemma</option></select>: " +
"<input type='text' class='searchtext' />" +
"</div>";
/**
* The HTML code for an "or" search node
* @private
* @constant
*/
var searchornodehtml = "<div class='searchnode searchornode'>" +
"<div class='searchadddelbuttons'>" +
"<input type='button' class='searchdelnodebut' value='-' />" +
"</div>" +
"<input type='hidden' class='searchtype' value='Or' />OR<br />" +
searchnodehtml + "</div>";
/**
* The HTML code for a "deep" search node
* @private
* @constant
*/
var searchdeepnodehtml = "<div class='searchnode searchdeepnode'>" +
"<div class='searchadddelbuttons'>" +
"<input type='button' class='searchdelnodebut' value='-' />" +
"</div>" +
"<input type='hidden' class='searchtype' value='Deep' />...<br />" +
searchnodehtml + "</div>";
/**
* The HTML code for a "precedes" search node
* @private
* @constant
*/
var searchprecnodehtml = "<div class='searchnode searchprecnode'>" +
"<div class='searchadddelbuttons'>" +
"<input type='button' class='searchdelnodebut' value='-' />" +
"</div>" +
"<input type='hidden' class='searchtype' value='Prec' />><br />" +
searchnodehtml + "</div>";
/**
* The HTML code for a node to add new search nodes
* @private
* @constant
*/
var addsearchnodehtml = "<div class='newsearchnode'>" +
"<input type='hidden' class='searchtype' value='NewNode' />+" +
"</div>";
/**
* The HTML code for the default starting search node
* @private
* @constant
*/
var searchhtml = "<div id='searchnodes' class='searchnode'><input type='hidden' " +
"class='searchtype' value='Root' />" + searchnodehtml + "</div>";
/**
* The last search
*
* So that it can be restored next time the dialog is opened.
* @private
*/
var savedsearch = $(searchhtml);
addStartupHook(function () {
$("#butsearch").click(search);
$("#butnextmatch").click(nextSearchMatch);
$("#butclearmatch").click(clearSearchMatches);
$("#matchcommands").hide();
});
// =============== Event handlers
/**
* Clear the highlighting from search matches.
*/
function clearSearchMatches() {
$(".searchmatch").removeClass("searchmatch");
$("#matchcommands").hide();
}
/**
* Scroll down to the next node that matched a search.
*/
function nextSearchMatch(e, fromSearch) {
if (!fromSearch) {
if ($("#searchInc").prop('checked')) {
doSearch();
}
}
scrollToNext(".searchmatch");
}
/**
* Add a sibling search node
* @private
*/
function addSearchDaughter(e) {
var node = $(e.target).parents(".searchnode").first();
var newnode = $(searchnodehtml);
node.append(newnode);
searchNodePostAdd(newnode);
}
/**
* Add a sibling search node
* @private
*/
function addSearchSibling(e) {
var node = $(e.target);
var newnode = $(searchnodehtml);
node.before(newnode);
searchNodePostAdd(newnode);
}
/**
* Delete a search node
* @private
*/
function searchDelNode(e) {
var node = $(e.target).parents(".searchnode").first();
var tmp = $("#searchnodes").children(".searchnode:not(.newsearchnode)");
if (tmp.length == 1 && tmp.is(node) &&
node.children(".searchnode").length === 0) {
displayWarning("Cannot remove only search term!");
return;
}
var child = node.children(".searchnode").first();
if (child.length == 1) {
node.contents(":not(.searchnode)").remove();
child.unwrap();
} else {
node.remove();
}
rejiggerSearchSiblingAdd();
}
/**
* Add an "or" search node
* @private
*/
function searchOrNode(e) {
var node = $(e.target).parents(".searchnode").first();
var newnode = $(searchornodehtml);
node.replaceWith(newnode);
newnode.children(".searchnode").replaceWith(node);
searchNodePostAdd(newnode);
}
/**
* Add a "deep" search node
* @private
*/
function searchDeepNode(e) {
var node = $(e.target).parents(".searchnode").first();
var newnode = $(searchdeepnodehtml);
node.append(newnode);
searchNodePostAdd(newnode);
}
/**
* Add a "precedes" search node
* @private
*/
function searchPrecNode(e) {
var node = $(e.target).parents(".searchnode").first();
var newnode = $(searchprecnodehtml);
node.after(newnode);
searchNodePostAdd(newnode);
}
// =============== Helper functions
/**
* Indicate that a node matches a search
*
* @param {DOM node} node the node to flag
*/
function flagSearchMatch(node) {
$(node).addClass("searchmatch");
$("#matchcommands").show();
}
/**
* Hook up event handlers after adding a node to the search dialog
*/
function searchNodePostAdd(node) {
$(".searchnewnodebut").unbind("click").click(addSearchDaughter);
$(".searchdelnodebut").unbind("click").click(searchDelNode);
$(".searchdeepnodebut").unbind("click").click(searchDeepNode);
$(".searchornodebut").unbind("click").click(searchOrNode);
$(".searchprecnodebut").unbind("click").click(searchPrecNode);
rejiggerSearchSiblingAdd();
var nodeToFocus = (node && node.find(".searchtext")) ||
$(".searchtext").first();
nodeToFocus.focus();
}
/**
* Recalculate the position of nodes that add siblings in the search dialog.
* @private
*/
function rejiggerSearchSiblingAdd() {
$(".newsearchnode").remove();
$(".searchnode").map(function() {
$(this).children(".searchnode").last().after(addsearchnodehtml);
});
$(".newsearchnode").click(addSearchSibling);
}
/**
* Remember the currently-entered search, in order to restore it subsequently.
* @private
*/
function saveSearch() {
savedsearch = $("#searchnodes").clone();
var savedselects = savedsearch.find("select");
var origselects = $("#searchnodes").find("select");
savedselects.map(function (i) {
$(this).val(origselects.eq(i).val());
});
}
/**
* Perform the search as entered in the dialog
* @private
*/
function doSearch () {
// TODO: need to save val of incremental across searches
var searchnodes = $("#searchnodes");
saveSearch();
hideDialogBox();
var searchCtx = $(".snode"); // TODO: remove sn0
var incremental = $("#searchInc").prop('checked');
if (incremental && $(".searchmatch").length > 0) {
var lastMatchTop = $(".searchmatch").last().offset().top;
searchCtx = searchCtx.filter(function () {
// TODO: do this with faster document position dom call
return $(this).offset().top > lastMatchTop;
});
}
clearSearchMatches();
for (var i = 0; i < searchCtx.length; i++) {
var res = interpretSearchNode(searchnodes, searchCtx[i]);
if (res) {
flagSearchMatch(res);
if (incremental) {
break;
}
}
}
nextSearchMatch(null, true);
// TODO: when reaching the end of the document in incremental search,
// don't dehighlight the last match, but print a nice message
// TODO: need a way to go back in incremental search}
/**
* Clear any previous search, reverting the dialog back to its default state.
* @private */function clearSearch() {
savedsearch = $(searchhtml);
$("#searchnodes").replaceWith(savedsearch);
searchNodePostAdd();
}
// =============== Search interpretation function/**
* Interpret the DOM nodes comprising the search dialog.
*
* This function is treponsible for transforming the representation of a
* search query as HTML into an executable query, and matching it against a
* node.
* @private *
* @param {DOM node} node the search node to interpret
* @param {DOM node} target the tree node to match it against
* @param {Object} options search options
* @returns {DOM node} `target` if it matched the query, otherwise `undefined`
*/function interpretSearchNode(node, target, options) {
// TODO: optimize to remove jquery calls, only use regex matching if needed// TODO: make case sensitivity an option? options = options || {};
var searchtype = $(node).children(".searchtype").val();
var rx, hasMatch, i, j;
var newTarget = $(target).children();
var childSearches = $(node).children(".searchnode");
if ($(node).parent().is("#searchnodes") &&
!$("#searchnodes").children(".searchnode").first().is(node) &&
!options.norecurse) {
// special case siblings at root level// What an ugly hack, can it be improved? newTarget = $(target).siblings();
for (j = 0; j < newTarget.length; j++) {
if (interpretSearchNode(node, newTarget[j], { norecurse: true })) {
return target;
}
}
}
if (searchtype == "Label") {
rx = RegExp("^" + $(node).children(".searchtext").val(), "i");
hasMatch = $(target).hasClass("snode") && rx.test(getLabel($(target)));
if (!hasMatch) {
return undefined;
}
} elseif (searchtype == "Text") {
rx = RegExp("^" + $(node).children(".searchtext").val(), "i");
hasMatch = $(target).children(".wnode").length == 1 &&
rx.test(wnodeString($(target)));
if (!hasMatch) {
return undefined;
}
} elseif (searchtype == "Lemma") {
rx = RegExp("^" + $(node).children(".searchtext").val(), "i");
hasMatch = hasLemma($(target)) &&
rx.test(getLemma($(target)));
if (!hasMatch) {
return undefined;
}
} elseif (searchtype == "Root") {
newTarget = $(target);
} elseif (searchtype == "Or") {
for (i = 0; i < childSearches.length; i++) {
if (interpretSearchNode(childSearches[i], target)) {
return target;
}
}
return undefined;
} elseif (searchtype == "Deep") {
newTarget = $(target).find(".snode,.wnode");
} elseif (searchtype == "Prec") {
newTarget = $(target).nextAll();
}
for (i = 0; i < childSearches.length; i++) {
var succ = false;
for (j = 0; j < newTarget.length; j++) {
if (interpretSearchNode(childSearches[i], newTarget[j])) {
succ = true;
break;
}
}
if (!succ) {
return undefined;
}
}
return target;
}
// =============== The core search function/**
* Display a search dialog.
*/function search() {
var html = "<div id='searchnodes' />" +
"<div id='dialogButtons'><label for='searchInc'>Incremental: " +
"</label><input id='searchInc' name='searchInc' type='checkbox' />" +
// TODO: it seems that any plausible implementation of search is// going to use rx, so it doesn't make sense to turn it off// "<label for='searchRE'>Regex: </label>" +// "<input id='searchRE' name='searchRE' type='checkbox' />" +"<input id='clearSearch' type='button' value='Clear' />" +
"<input id='doSearch' type='button' value='Search' /></div>";
showDialogBox("Search", html, doSearch, saveSearch);
$("#searchnodes").replaceWith(savedsearch);
$("#doSearch").click(doSearch);
$("#clearSearch").click(clearSearch);
searchNodePostAdd();
}
// ===== Collapsing nodes/**
* Toggle collapsing of a node.
*
* When a node is collapsed, its contents are displayed as continuous text,
* without labels. The node itself still functions normally with respect to
* movement operations etc., but its contents are inaccessible.
*/function toggleCollapsed() {
if (!startnode || endnode) {
returnfalse;
}
$(startnode).toggleClass("collapsed");
returntrue;
}
// ===== Tree manipulations// ========== Movement/**
* Move the selected node(s) to a new position.
*
* The movement operation must not change the text of the token.
*
* Empty categories are not allowed to be moved as a leaf. However, a
* non-terminal containing only empty categories can be moved.
*
* @param {DOM Node} parent the parent node to move selection under
*
* @returns {Boolean} whether the operation was successful
*/function moveNode(parent) {
var parent_ip = $(startnode).parents("#sn0>.snode,#sn0").first();
var other_parent = $(parent).parents("#sn0>.snode,#sn0").first();
if (parent == document.getElementById("sn0") ||
!parent_ip.is(other_parent)) {
parent_ip = $("#sn0");
}
var parent_before;
var textbefore = currentText(parent_ip);
var nodeMoved;
if (!isPossibleTarget(parent) || // can't move under a tag node $(startnode).parent().children().length == 1 || // can't move an only child $(parent).parents().is(startnode) || // can't move under one's own child isEmptyNode(startnode) // can't move an empty leaf node by itself )
{
clearSelection();
returnfalse;
} elseif ($(startnode).parents().is(parent)) {
// move up if moving to a node that is already my parentif ($(startnode).parent().children().first().is(startnode)) {
if ($(startnode).parentsUntil(parent).slice(0,-1).
filter(":not(:first-child)").size() > 0) {
clearSelection();
returnfalse;
}
if (parent == document.getElementById("sn0")) {
touchTree($(startnode));
registerNewRootTree($(startnode));
} else {
touchTree($(startnode));
}
$(startnode).insertBefore($(parent).children().filter(
$(startnode).parents()));
if (currentText(parent_ip) != textbefore) {
alert("failed what should have been a strict test");
}
} elseif ($(startnode).parent().children().last().is(startnode)) {
if ($(startnode).parentsUntil(parent).slice(0,-1).
filter(":not(:last-child)").size() > 0) {
clearSelection();
returnfalse;
}
if (parent == document.getElementById("sn0")) {
touchTree($(startnode));
registerNewRootTree($(startnode));
} else {
touchTree($(startnode));
}
$(startnode).insertAfter($(parent).children().
filter($(startnode).parents()));
if (currentText(parent_ip) != textbefore) {
alert("failed what should have been a strict test");
}
} else {
// cannot move from this position clearSelection();
returnfalse;
}
} else {
// otherwise move under my sistervar tokenMerge = isRootNode( $(startnode) );
var maxindex = maxIndex(getTokenRoot($(parent)));
var movednode = $(startnode);
// NOTE: currently there are no more stringent checks below; if that// changes, we might want to demote this parent_before = parent_ip.clone();
// where a and b are DOM elements (not jquery-wrapped),// a.compareDocumentPosition(b) returns an integer. The first (counting// from 0) bit is set if B precedes A, and the second bit is set if A// precedes B.// TODO: perhaps here and in the immediately following else if it is// possible to simplify and remove the compareDocumentPosition call,// since the jQuery subsumes itif (parent.compareDocumentPosition(startnode) & 0x4) {
// check whether the nodes are adjacent. Ideally, we would like// to say selfAndParentsUntil, but no such jQuery fn exists, thus// necessitating the disjunction.// TODO: too strict// &&// $(startnode).prev().is(// $(parent).parentsUntil(startnode.parentNode).last()) ||// $(startnode).prev().is(parent)// parent precedes startnode undoBeginTransaction();
if (tokenMerge) {
registerDeletedRootTree($(startnode));
touchTree($(parent));
// TODO: this will bomb if we are merging more than 2 tokens// by multiple selection. addToIndices(movednode, maxindex);
} else {
touchTree($(startnode));
touchTree($(parent));
}
movednode.appendTo(parent);
if (currentText(parent_ip) != textbefore) {
undoAbortTransaction();
parent_ip.replaceWith(parent_before);
if (parent_ip.attr("id") == "sn0") {
$("#sn0").mousedown(handleNodeClick);
}
clearSelection();
returnfalse;
} else {
undoEndTransaction();
}
} elseif ((parent.compareDocumentPosition(startnode) & 0x2)) {
// &&// $(startnode).next().is(// $(parent).parentsUntil(startnode.parentNode).last()) ||// $(startnode).next().is(parent)// startnode precedes parent undoBeginTransaction();
if (tokenMerge) {
registerDeletedRootTree($(startnode));
touchTree($(parent));
addToIndices(movednode, maxindex);
} else {
touchTree($(startnode));
touchTree($(parent));
}
movednode.insertBefore($(parent).children().first());
if (currentText(parent_ip) != textbefore) {
undoAbortTransaction();
parent_ip.replaceWith(parent_before);
if (parent_ip == "sn0") {
$("#sn0").mousedown(handleNodeClick);
}
clearSelection();
returnfalse;
} else {
undoEndTransaction();
}
} // TODO: conditional branches not exhaustive }
clearSelection();
returntrue;
}
/**
* Move several nodes.
*
* The two selected nodes must be sisters, and they and all intervening sisters
* will be moved as a unit. Calls {@link moveNode} to do the heavy lifting.
*
* @param {DOM Node} parent the parent to move the selection under
*/function moveNodes(parent) {
if (!startnode || !endnode) {
return;
}
undoBeginTransaction();
touchTree($(startnode));
touchTree($(parent));
if (startnode.compareDocumentPosition(endnode) & 0x2) {
// endnode precedes startnode, reverse themvar temp = startnode;
startnode = endnode;
endnode = temp;
}
if (startnode.parentNode == endnode.parentNode) {
// collect startnode and its sister up until endnode $(startnode).add($(startnode).nextUntil(endnode)).
add(endnode).
wrapAll('<div xxx="newnode" class="snode">XP</div>');
} else {
return; // they are not sisters }
var toselect = $(".snode[xxx=newnode]").first();
toselect = toselect.get(0);
// BUG when making XP and then use context menu: TODO XXX startnode = toselect;
var res = ignoringUndo(function () { moveNode(parent); });
if (res) {
undoEndTransaction();
} else {
undoAbortTransaction();
}
startnode = $(".snode[xxx=newnode]").first().get(0);
endnode = undefined;
pruneNode();
clearSelection();
}
// ========== Creation/**
* Create a leaf node before the selected node.
*
* Uses heuristic to determine whether the new leaf is to be a trace, empty
* subject, etc.
*/function leafBefore() {
makeLeaf(true);
}
/**
* Create a leaf node after the selected node.
*
* Uses heuristic to determine whether the new leaf is to be a trace, empty
* subject, etc.
*/function leafAfter() {
makeLeaf(false);
}
// TODO: the hardcoding of defaults in this function is ugly. We should// supply a default heuristic fn to try to guess these, then allow// settings.js to override it.// TODO: maybe put the heuristic into leafbefore/after, and leave this fn clean?/**
* Create a leaf node adjacent to the selection, or a given target.
*
* @param {Boolean} before whether to create the node before or after selection
* @param {String} label the label to give the new node
* @param {String} word the text to give the new node
* @param {DOM Node} target where to put the new node (default: selected node)
*/function makeLeaf(before, label, word, target) {
if (!(target || startnode)) return;
if (!label) {
if (before) {
label = "NP-SBJ";
} else {
label = "VB";
}
}
if (!word) {
if (before) {
word = "*con*";
} else {
word = "*";
}
}
if (!target) {
target = startnode;
}
undoBeginTransaction();
var isRootLevel = false;
if (isRootNode($(target))) {
isRootLevel = true;
} else {
touchTree($(target));
}
var lemma = false;
var temp = word.split("-");
if (temp.length > 1) {
lemma = temp.pop();
word = temp.join("-");
}
var doCoindex = false;
if (endnode) {
var startRoot = getTokenRoot($(startnode));
var endRoot = getTokenRoot($(endnode));
if (startRoot == endRoot) {
word = "*ICH*";
label = getLabel($(endnode));
if (label.startsWith("W")) {
word = "*T*";
label = label.substr(1).replace(/-[0-9]+$/, "");
} elseif (label.split("-").indexOf("CL") > -1) {
word = "*CL*";
label = getLabel($(endnode)).replace("-CL", "");
if (label.substring(0,3) == "PRO") {
label = "NP";
}
}
doCoindex = true;
} else { // abort if selecting from different tokens undoAbortTransaction();
return;
}
}
var newleaf = "<div class='snode " + label + "'>" + label +
"<span class='wnode'>" + word;
if (lemma) {
newleaf += "<span class='lemma'>-" + lemma +
"</span>";
}
newleaf += "</span></div>\n";
newleaf = $(newleaf);
if (before) {
newleaf.insertBefore(target);
} else {
newleaf.insertAfter(target);
}
if (doCoindex) {
startnode = newleaf.get(0);
coIndex();
}
startnode = null;
endnode = null;
selectNode(newleaf.get(0));
updateSelection();
if (isRootLevel) {
registerNewRootTree(newleaf);
}
undoEndTransaction();
}
/**
* Create a phrasal node.
*
* The node will dominate the selected node or (if two sisters are selected)
* the selection and all intervening sisters.
*
* @param {String} [label] the label to give the new node (default: XP)
*/function makeNode(label) {
// check if something is selectedif (!startnode) {
return;
}
if (!label) {
label = "XP";
}
var rootLevel = isRootNode($(startnode));
undoBeginTransaction();
if (rootLevel) {
registerDeletedRootTree($(startnode));
} else {
touchTree($(startnode));
}
var parent_ip = $(startnode).parents("#sn0>.snode,#sn0").first();
var parent_before = parent_ip.clone();
var newnode = '<div class="snode ' + label + '">' + label + ' </div>\n';
// make end = start if only one node is selectedif (!endnode) {
// if only one node, wrap around that one $(startnode).wrapAll(newnode);
} else {
if (startnode.compareDocumentPosition(endnode) & 0x2) {
// startnode and endnode in wrong order, reverse themvar temp = startnode;
startnode = endnode;
endnode = temp;
}
// check if they are really sisters XXXXXXXXXXXXXXXif ($(startnode).siblings().is(endnode)) {
// then, collect startnode and its sister up until endnodevar oldtext = currentText(parent_ip);
$(startnode).add($(startnode).nextUntil(endnode)).add(
endnode).wrapAll(newnode);
// undo if this messed up the text orderif(currentText(parent_ip) != oldtext) {
// TODO: is this plausible? can we remove the check? parent_ip.replaceWith(parent_before);
undoAbortTransaction();
clearSelection();
return;
}
} else {
return;
}
}
var toselect = $(startnode).parent();
startnode = null;
endnode = null;
if (rootLevel) {
registerNewRootTree(toselect);
}
undoEndTransaction();
selectNode(toselect.get(0));
updateSelection();
}
// ========== Deletion/**
* Delete a node.
*
* The node can only be deleted if doing so does not affect the text, i.e. it
* directly dominates no non-empty terminals.
*/function pruneNode() {
if (startnode && !endnode) {
var deltext = $(startnode).children().first().text();
if (isLeafNode(startnode) && isEmpty(deltext)) {
// it is ok to delete leaf if it is empty/traceif (isRootNode($(startnode))) {
// perversely, it is possible to have a leaf node at the root// of a file. registerDeletedRootTree($(startnode));
} else {
touchTree($(startnode));
}
var idx = getIndex($(startnode));
if (idx > 0) {
var root = $(getTokenRoot($(startnode)));
var sameIdx = root.find('.snode').filter(function () {
return getIndex($(this)) == idx;
}).not(startnode);
if (sameIdx.length == 1) {
var osn = startnode;
startnode = sameIdx.get(0);
coIndex();
startnode = osn;
}
}
$(startnode).remove();
startnode = endnode = null;
updateSelection();
return;
} elseif (isLeafNode(startnode)) {
// but other leaves are not deletedreturn;
} elseif (startnode == document.getElementById("sn0")) {
return;
}
var toselect = $(startnode).children().first();
if (isRootNode($(startnode))) {
// TODO: ugly and expensive. the alternative is adding a fourth// data type to the undo list, I think. registerDeletedRootTree($(startnode));
$(startnode).children().each(function () {
registerNewRootTree($(this));
});
} else {
touchTree($(startnode));
}
$(startnode).replaceWith($(startnode).children());
startnode = endnode = null;
selectNode(toselect.get(0));
updateSelection();
}
}
// ========== Label manipulation/**
* Toggle a dash tag on a node
*
* If the node bears the given dash tag, remove it. If not, add it. This
* function attempts to put multiple dash tags in the proper order, according
* to the configuration in the `leaf_extensions`, `extensions`, and
* `clause_extensions` variables in the `settings.js` file.
*
* @param {String} extension the dash tag to toggle
* @param {Array of String} [extensionList] override the guess as to the
* appropriate ordered list of possible extensions.
*/function toggleExtension(extension, extensionList) {
if (!startnode || endnode) returnfalse;
if (!extensionList) {
if (guessLeafNode(startnode)) {
extensionList = leaf_extensions;
} elseif (getLabel($(startnode)).split("-")[0] == "IP" ||
getLabel($(startnode)).split("-")[0] == "CP") {
// TODO: should FRAG be a clause?// TODO: make configurable extensionList = clause_extensions;
} else {
extensionList = extensions;
}
}
// Tried to toggle an extension on an inapplicable node.if (extensionList.indexOf(extension) < 0) {
returnfalse;
}
touchTree($(startnode));
var textnode = textNode($(startnode));
var oldlabel = $.trim(textnode.text());
// Extension is not de-dashed here. toggleStringExtension handles it.// The new config format however requires a dash-less extension.var newlabel = toggleStringExtension(oldlabel, extension, extensionList);
textnode.replaceWith(newlabel + " ");
updateCssClass($(startnode), oldlabel);
returntrue;
}
/**
* Set the label of a node intelligently
*
* Given a list of labels, this function will attempt to find the node's
* current label in the list. If it is successful, it sets the node's label
* to the next label in the list (or the first, if the node's current label is
* the last in the list). If not, it sets the label to the first label in the
* list.
*
* @param labels a list of labels. This can also be an object -- if so, the
* base label (without any dash tags) of the target node is looked up as a
* key, and its corresponding value is used as the list. If there is no value
* for that key, the first value specified in the object is the default.
*/function setLabel(labels) {
if (!startnode || endnode) {
returnfalse;
}
var textnode = textNode($(startnode));
var oldlabel = $.trim(textnode.text());
var newlabel = lookupNextLabel(oldlabel, labels);
if (guessLeafNode($(startnode))) {
if (typeof testValidLeafLabel !== "undefined") {
if (!testValidLeafLabel(newlabel)) {
returnfalse;
}
}
} else {
if (typeof testValidPhraseLabel !== "undefined") {
if (!testValidPhraseLabel(newlabel)) {
returnfalse;
}
}
}
touchTree($(startnode));
textnode.replaceWith(newlabel + " ");
updateCssClass($(startnode), oldlabel);
returntrue;
}
// ========== Coindexation/**
* Coindex nodes.
*
* Coindex the two selected nodes. If they are already coindexed, toggle
* types of coindexation (normal -> gapping -> backwards gapping -> double
* gapping -> no indices). If only one node is selected, remove its index.
*/function coIndex() {
if (startnode && !endnode) {
if (getIndex($(startnode)) > 0) {
touchTree($(startnode));
removeIndex(startnode);
}
} elseif (startnode && endnode) {
// don't do anything if different token rootsvar startRoot = getTokenRoot($(startnode));
var endRoot = getTokenRoot($(endnode));
if (startRoot != endRoot) {
return;
}
touchTree($(startnode));
// if both nodes already have an indexif (getIndex($(startnode)) > 0 && getIndex($(endnode)) > 0) {
// and if it is the same indexif (getIndex($(startnode)) == getIndex($(endnode))) {
var theIndex = getIndex($(startnode));
var types = "" + getIndexType($(startnode)) +
"" + getIndexType($(endnode));
// remove itif (types == "=-") {
removeIndex(startnode);
removeIndex(endnode);
appendExtension($(startnode), theIndex, "=");
appendExtension($(endnode), theIndex, "=");
} elseif( types == "--" ){
removeIndex(endnode);
appendExtension($(endnode), getIndex($(startnode)),"=");
} elseif (types == "-=") {
removeIndex(startnode);
removeIndex(endnode);
appendExtension($(startnode), theIndex,"=");
appendExtension($(endnode), theIndex,"-");
} elseif (types == "==") {
removeIndex(startnode);
removeIndex(endnode);
}
}
} elseif (getIndex($(startnode)) > 0 && getIndex($(endnode)) == -1) {
appendExtension($(endnode), getIndex($(startnode)));
} elseif (getIndex($(startnode)) == -1 && getIndex($(endnode)) > 0) {
appendExtension($(startnode), getIndex($(endnode)));
} else { // no indices here, so make themvar index = maxIndex(startRoot) + 1;
appendExtension($(startnode), index);
appendExtension($(endnode), index);
}
}
}
// ===== Server-side operations// ========== Saving// =============== Save helper function// TODO: move to utils?// TODO: this is not very general, in fact only works when called with// #editpane as argfunction toLabeledBrackets(node) {
var out = node.clone();
// The ZZZZZ is a placeholder; first we want to clean any// double-linebreaks from the output (which will be spurious), then we// will turn the Z's into double-linebreaks out.find(".snode:not(#sn0)").each(function () {
this.insertBefore(document.createTextNode("("), this.firstChild);
this.appendChild(document.createTextNode(")"));
});
out.find("#sn0>.snode").each(function () {
$(this).append(jsonToTree(this.getAttribute("data-metadata")));
this.insertBefore(document.createTextNode("( "), this.firstChild);
this.appendChild(document.createTextNode(")ZZZZZ"));
});
out.find(".wnode").each(function () {
this.insertBefore(document.createTextNode(" "), this.firstChild);
});
out = out.text();
// Must use rx for string replace bc using a string doesn't get a// global replace. out = out.replace(/\)\(/g, ") (");
out = out.replace(/ +/g, " ");
out = out.replace(/\n\n+/g,"\n");
out = out.replace(/ZZZZZ/g, "\n\n");
// If there is a space after the word but before the closing paren, it// will make CorpusSearch unhappy. out = out.replace(/ +\)/g, ")");
// Ditto for spaces btw. word and lemma, in dash format out = out.replace(/- +/g, "-");
return out;
}
var saveInProgress = false;
function saveHandler (data) {
if (data.result == "success") {
displayInfo("Save success.");
} else {
lastsavedstate = "";
var extraInfo = "";
if (safeGet(data, 'reasonCode', 0) == 1) {
extraInfo = " <a href='#' id='forceSave' " +
"onclick='javascript:startTime=" + data.startTime +
";save(null, {force:true});return false'>Force save</a>";
} elseif (safeGet(data, 'reasonCode', 0) == 2) {
extraInfo = " <a href='#' id='forceSave' " +
"onclick='javascript:startTime=" + data.startTime +
";save(null, {update_md5:true});return false'>Force save</a>";
}
displayError("Save FAILED!!!: " + data.reason + extraInfo);
}
saveInProgress = false;
}
function save(e, extraArgs) {
if (!extraArgs) {
extraArgs = {};
}
if (document.getElementById("leafphrasebox") ||
document.getElementById("labelbox")) {
// It should be impossible to trigger a save in these conditions, but// it causes data corruption if the save happens,, so this functions// as a last-ditch safety. displayError("Cannot save while editing a node label.");
return;
}
if (!saveInProgress) {
displayInfo("Saving...");
saveInProgress = true;
setTimeout(function () {
var tosave = toLabeledBrackets($("#editpane"));
extraArgs.trees = tosave;
extraArgs.startTime = startTime;
$.post("/doSave", extraArgs, saveHandler).error(function () {
lastsavedstate = "";
saveInProgress = false;
displayError("Save failed, could not " +
"communicate with server!");
});
unAutoIdle();
lastsavedstate = $("#editpane").html();
}, 0);
}
}
// ========== Validatingvar validatingCurrently = false;
function validateTrees(e) {
if (!validatingCurrently) {
validatingCurrently = true;
displayInfo("Validating...");
setTimeout(function () {
// TODO: since this is a settimeout, do we need to also make it async? validateTreesSync(true, e.shiftKey);
}, 0);
}
}
function validateTreesSync(async, shift) {
var toValidate = toLabeledBrackets($("#editpane"));
$.ajax("/doValidate",
{ type: 'POST',
url: "/doValidate",
data: { trees: toValidate,
validator: $("#validatorsSelect").val(),
shift: shift
},
success: validateHandler,
async: async,
dataType: "json" });
}
function validateHandler(data) {
if (data.result == "success") {
displayInfo("Validate success.");
$("#editpane").html(data.html);
assignEvents();
prepareUndoIds();
} elseif (data.result == "failure") {
displayWarning("Validate failed: " + data.reason);
}
validatingCurrently = false;
// TODO(AWE): more nuanced distinction between validation found errors and// validation script itself contains errors}
function nextValidationError() {
var node = scrollToNext(".snode[class*=\"FLAG\"],.snode[class$=\"FLAG\"]");
selectNode(node.get(0));
}
// ========== Advancing through the filefunction nextTree(e) {
e = e || {};
var find = undefined;
if (e.shiftKey) find = "-FLAG";
advanceTree(find, false, 1);
}
function prevTree(e) {
e = e || {};
var find = undefined;
if (e.shiftKey) find = "-FLAG";
advanceTree(find, false, -1);
}
function advanceTree(find, async, offset) {
var theTrees = toLabeledBrackets($("#editpane"));
displayInfo("Fetching tree...");
return $.ajax("/advanceTree",
{ async: async,
success: function(res) {
if (res.result == "failure") {
displayWarning("Fetching tree failed: " + res.reason);
} else {
// TODO: what to do about the save warning $("#editpane").html(res.tree);
documentReadyHandler();
nukeUndo();
currentIndex = res['treeIndexStart'] + 1;
displayInfo("Tree " + currentIndex + " fetched.");
displayTreeIndex("Editing tree #" + currentIndex +
" out of " + res['totalTrees']);
}
},
dataType: "json",
type: "POST",
data: { trees: theTrees,
find: find,
offset: offset
}});
}
function displayTreeIndex(text) {
$("#treeIndexDisplay").text(text);
}
// TODO: test post-mergefunction goToTree() {
function goTo() {
var i;
var treeIndex = $("#gotoInput").val();
advanceTree(undefined, false, treeIndex - currentIndex);
hideDialogBox();
}
var html = "Enter the index of the tree you'd like to jump to: \
<input type='text' id='gotoInput' value=' ' /><div id='dialogButtons'><input type='button' id='gotoButton'\
value='GoTo' /></div>";
showDialogBox("GoTo Tree", html, goTo);
$("#gotoButton").click(goTo);
$("#gotoInput").focus();
}
// ========== Event logging and idle// =============== Event logging functionfunction logEvent(type, data) {
data = data || {};
data.type = type;
$.ajax("/doLogEvent",
{
async: true,
dataType: "json",
type: "POST",
data: { eventData: JSON.stringify(data) },
traditional: true });
}
// =============== Idle timeoutvar idleTimeout = false;
var isIdle = false;
function resetIdleTimeout() {
if (idleTimeout) {
clearTimeout(idleTimeout);
}
idleTimeout = setTimeout(autoIdle, 30 * 1000);
}
function autoIdle() {
logEvent("auto-idle");
becomeIdle();
}
addStartupHook(resetIdleTimeout);
addKeyDownHook(function() {
unAutoIdle();
resetIdleTimeout();
});
addClickHook(function() {
unAutoIdle();
resetIdleTimeout();
});
function unAutoIdle() {
if (isIdle) {
logEvent("auto-resume");
becomeEditing();
}
}
// =============== User interfacefunction becomeIdle() {
isIdle = true;
$("#idlestatus").html("<div style='color:#C75C5C'>IDLE.</div>");
$("#butidle").unbind("mousedown").mousedown(resume);
}
function becomeEditing() {
isIdle = false;
$("#idlestatus").html("<div style='color:#64C465'>Editing.</div>");
$("#butidle").unbind("mousedown").mousedown(idle);
}
function idle() {
logEvent("user-idle");
becomeIdle();
}
function resume() {
logEvent("user-resume");
becomeEditing();
}
// =============== Key/click loggingaddStartupHook(function () {
// This must be delayed, because this file is loaded before settings.js isif (logDetail) {
addKeyDownHook(function (keydata, fn, args) {
var key = (keydata.ctrl ? "C-" : "") +
(keydata.shift ? "S-" : "") +
String.fromCharCode(keydata.keyCode),
theFn = fn.name + "(" +
args.map(function (x) { return JSON.stringify(x); }).join(", ") +
")";
logEvent("keypress",
{ key: key,
fn: theFn
});
});
addClickHook(function (button) {
logEvent("mouse-click",
{ button: button
});
});
// TODO: what about mouse movement? }
});
// ========== Quittingfunction quitServer(e, force) {
unAutoIdle();
if (!force && $("#editpane").html() != lastsavedstate) {
displayError("Cannot exit, unsaved changes exist. <a href='#' " +
"onclick='quitServer(null, true);return false;'>Force</a>");
} else {
$.post("/doExit");
window.onbeforeunload = undefined;
setTimeout(function(res) {
// I have no idea why this works, but it does window.open('', '_self', '');
window.close();
}, 100);
}
}
// ===== Undo/redo// TODO: organize this codevar undoMap,
undoNewTrees,
undoDeletedTrees,
undoStack = [],
redoStack = [],
undoTransactionStack = [];
var idNumber = 1;
function prepareUndoIds() {
$("#sn0>.snode").map(function () {
$(this).attr("id", "id" + idNumber);
idNumber++;
});
nukeUndo();
}
addStartupHook(prepareUndoIds);
/**
* Reset the undo system.
*
* This function removes any intermediate state the undo system has stored; it
* does not affect the undo history.
* @private */function resetUndo() {
undoMap = {};
undoNewTrees = [];
undoDeletedTrees = [];
undoTransactionStack = [];
}
/**
* Reset the undo system entirely.
*
* This function zeroes out any undo history.
*/function nukeUndo() {
resetUndo();
undoStack = [];
redoStack = [];
}
/**
* Record an undo step.
* @private */function undoBarrier() {
if (_.size(undoMap) === 0 &&
_.size(undoNewTrees) === 0 &&
_.size(undoDeletedTrees) === 0) {
return;
}
undoStack.push({
map: undoMap,
newTr: undoNewTrees,
delTr: undoDeletedTrees
});
resetUndo();
redoStack = [];
}
/**
* Begin an undo transaction.
*
* This function MUST be matched by a call to either `undoEndTransaction`
* (which keeps all intermediate steps since the start call) or
* `undoAbortTransaction` (which discards said steps).
*/function undoBeginTransaction() {
undoTransactionStack.push({
map: undoMap,
newTr: undoNewTrees,
delTr: undoDeletedTrees
});
}
/**
* End an undo transaction, keeping its changes
*/function undoEndTransaction() {
undoTransactionStack.pop();
}
/**
* End an undo transaction, discarding its changes
*/function undoAbortTransaction() {
var t = undoTransactionStack.pop();
undoMap = t.map;
undoNewTrees = t.newTr;
undoDeletedTrees = t.delTr;
}
/**
* Execute a function, discarding whatever effects it has on the undo system.
*
* @param {Function} fn a function to execute
*
* @returns the result of `fn`
*/function ignoringUndo(fn) {
// a bit of a grim hack, but it works undoBeginTransaction();
var res = fn();
undoAbortTransaction();
return res;
}
/**
* Inform the undo system that changes are being made.
*
* @param {JQuery Node} node the node in which changes are being made
*/function touchTree(node) {
var root = $(getTokenRoot(node));
if (!undoMap[root.attr("id")]) {
undoMap[root.attr("id")] = root.clone();
}
}
/**
* Inform the undo system of the addition of a new tree at the root level.
*
* @param {JQuery Node} tree the tree being added
*/function registerNewRootTree(tree) {
var newid = "id" + idNumber;
idNumber++;
undoNewTrees.push(newid);
tree.attr("id", newid);
}
/**
* Inform the undo system of a tree's removal at the root level
*
* @param {JQuery Node} tree the tree being removed
*/function registerDeletedRootTree(tree) {
var prev = tree.prev();
if (prev.length === 0) {
prev = null;
}
undoDeletedTrees.push({
tree: tree.clone(),
before: prev && prev.attr("id")
});
}
/**
* Perform an undo operation.
*
* This is a worker function, wrapped by `undo` and `redo`.
* @private */function doUndo(undoData) {
var map = {},
newTr = [],
delTr = [];
_.each(undoData.map, function(v, k) {
var theNode = $("#" + k);
map[k] = theNode.clone();
theNode.replaceWith(v);
});
// Add back the deleted trees before removing the new trees, just in case// the insertion point of one of these is going to get zapped. This// shouldn't happen, though. _.each(undoData.delTr, function(v) {
var prev = v.before;
if (prev) {
v.tree.insertAfter($("#" + prev));
} else {
v.tree.prependTo($("#sn0"));
}
newTr.push(v.tree.attr("id"));
});
_.each(undoData.newTr, function(v) {
var theNode = $("#" + v);
var prev = theNode.prev();
if (prev.length === 0) {
prev = null;
}
delTr.push({
tree: theNode.clone(),
before: prev && prev.attr("id")
});
theNode.remove();
});
return {
map: map,
newTr: newTr,
delTr: delTr
};
}
/**
* Perform undo.
*/function undo() {
if (undoStack.length === 0) {
displayWarning("No further undo information");
return;
}
var lastUndo = undoStack.pop();
redoStack.push(doUndo(lastUndo));
startnode = endnode = undefined;
updateSelection();
}
/**
* Perform redo.
*/function redo () {
if (redoStack.length === 0) {
displayWarning("No further redo information");
return;
}
undoStack.push(doUndo(redoStack.pop()));
startnode = endnode = undefined;
updateSelection();
}
// ===== Misc/**
* Toggle display of lemmata.
*/function toggleLemmata() {
if (lemmataHidden) {
lemmataStyleNode.innerHTML = "";
} else {
lemmataStyleNode.innerHTML = ".lemma { display: none; }";
}
lemmataHidden = !lemmataHidden;
}
function fixError() {
if (!startnode || endnode) return;
var sn = $(startnode);
if (hasDashTag(sn, "FLAG")) {
toggleExtension("FLAG", ["FLAG"]);
}
updateSelection();
}
function zeroDashTags() {
if (!startnode || endnode) return;
var label = getLabel($(startnode));
var idx = parseIndex(label),
idxType = parseIndexType(label),
lab = parseLabel(label);
if (idx == -1) {
idx = idxType = "";
}
touchTree($(startnode));
setLabelLL($(startnode), lab.split("-")[0] + idxType + idx);
}
// TODO: should allow numeric indices; documentfunction basesAndDashes(bases, dashes) {
function _basesAndDashes(string) {
var spl = string.split("-");
var b = spl.shift();
return (bases.indexOf(b) > -1) &&
_.all(spl, function (x) { return (dashes.indexOf(x) > -1); });
}
return _basesAndDashes;
}
function addLemma(lemma) {
// TODO: This only makes sense for dash-format corporaif (!startnode || endnode) return;
if (!isLeafNode($(startnode)) || isEmpty(wnodeString($(startnode)))) return;
touchTree($(startnode));
var theLemma = $("<span class='lemma'>-" + lemma +
"</span>");
$(startnode).children(".wnode").append(theLemma);
}
function untilSuccess() {
for (var i = 0; i < arguments.length; i++) {
var fn = arguments[i][0],
args = arguments[i].slice(1);
var res = fn.apply(null, args);
if (res) {
return;
}
}
}
function leafOrNot(leaf, not) {
var fn, args;
if (guessLeafNode($(startnode))) {
fn = arguments[0][0];
args = arguments[0].slice(1);
} else {
fn = arguments[1][0];
args = arguments[1].slice(1);
}
fn.apply(null, args);
}
// ===== Misc (candidates to move to utils)// TODO: move to utils?function setLeafLabel(node, label) {
if (!node.hasClass(".wnode")) {
// why do we do this? We should be less fault-tolerant. node = node.children(".wnode").first();
}
textNode(node).replaceWith($.trim(label));
}
// TODO: need a setLemma function as well// TODO: only called from one place, with indices: possibly specialize name?function appendExtension(node, extension, type) {
if (!type) {
type="-";
}
if (shouldIndexLeaf(node) && !isNaN(extension)) {
// Adding an index to an empty category, and the EC is not an// empty operator. The final proviso is needed because of// things like the empty WADJP in comparatives.var oldLabel = textNode(node.children(".wnode").first()).text();
setLeafLabel(node, oldLabel + type + extension);
} else {
setNodeLabel(node, getLabel(node) + type + extension, true);
}
}
function removeIndex(node) {
node = $(node);
if (getIndex(node) == -1) {
return;
}
var label, setLabelFn;
if (shouldIndexLeaf(node)) {
label = wnodeString(node);
setLabelFn = setLeafLabel;
} else {
label = getLabel(node);
setLabelFn = setNodeLabel;
}
setLabelFn(node,
label.substr(0, Math.max(label.lastIndexOf("-"),
label.lastIndexOf("="))),
true);
}
// A low-level (LL) version of setLabel. It is only responsible for changing// the label; not doing any kind of matching/changing/other crap.function setLabelLL(node, label) {
if (node.hasClass("snode")) {
if (label[label.length - 1] != " ") {
// Some other spots in the code depend on the label ending with a// space... label += " ";
}
} elseif (node.hasClass("wnode")) {
// Words cannot have a trailing space, or CS barfs on save. label = $.trim(label);
} else {
// should never happenreturn;
}
var oldLabel = parseLabel(getLabel(node));
textNode(node).replaceWith(label);
updateCssClass(node, oldLabel);
}
/**
* Update the CSS class of a node to reflect its label.
*
* @param {JQuery Node} node
* @param {String} oldlabel (optional) the former label of this node
*/function updateCssClass(node, oldlabel) {
if (!node.hasClass("snode")) {
return;
}
if (oldlabel) {
// should never be needed, but a bit of defensiveness can't hurt oldlabel = parseLabel($.trim(oldlabel));
} else {
// oldlabel wasn't supplied -- try to guess oldlabel = node.attr("class").split(" ");
oldlabel = _.find(oldlabel, function (s) { return (/[A-Z-]/).match(s); });
}
node.removeClass(oldlabel);
node.addClass(parseLabel(getLabel(node)));
}
//================================================== Obsolete/other/**
* Sets the label of a node
*
* Contains none of the heuristics of {@link setLabel}.
*
* @param {JQuery Node} node the target node
* @param {String} label the new label
* @param {Boolean} noUndo whether to record this operation for later undo
*/function setNodeLabel(node, label, noUndo) {
// TODO: fold this and setLabelLL together... setLabelLL(node, label);
}
// TODO(AWE): I think that updating labels on changing nodes works, but// this fn should be interactively called with debugging arg to test this// supposition. When I am confident of the behavior of the code, this fn will// be removed.function resetLabelClasses(alertOnError) {
var nodes = $(".snode").each(
function() {
var node = $(this);
var label = $.trim(getLabel(node));
if (alertOnError) {
var classes = node.attr("class").split(" ");
// This incantation removes a value from an array. classes.indexOf("snode") >= 0 &&
classes.splice(classes.indexOf("snode"), 1);
classes.indexOf(label) >= 0 &&
classes.splice(classes.indexOf(label), 1);
if (classes.length > 0) {
alert("Spurious classes '" + classes.join() +
"' detected on node id'" + node.attr("id") + "'");
}
}
node.attr("class", "snode " + label);
});
}
// TODO: badly need a DSL for forms// Local Variables:// js2-additional-externs: ("$" "setTimeout" "customCommands\// " "customConLeafBefore" "customConMenuGroups" "extensions" "leaf_extensions\// " "clause_extensions" "JSON" "testValidLeafLabel" "testValidPhraseLabel\// " "_" "startTime" "console" "loadContextMenu" "safeGet\// " "jsonToTree" "objectToTree" "dictionaryToForm" "formToDictionary\// " "displayWarning" "displayInfo" "displayError" "isEmpty" "isPossibleTarget\// " "isRootNode" "isLeafNode" "guessLeafNode" "getTokenRoot" "wnodeString\// " "currentText" "getLabel" "textNode" "getMetadata" "hasDashTag\// " "parseIndex" "parseLabel" "parseIndexType" "getIndex" "getIndexType\// " "shouldIndexLeaf" "maxIndex" "addToIndices" "changeJustLabel\// " "toggleStringExtension" "lookupNextLabel" "commentTypes\// " "invisibleCategories" "invisibleRootCategories" "ipnodes" "messageHistory\// " "scrollToNext" "clearTimeout" "logDetail" "hasLemma" "getLemma\// " "logDetail" "isEmptyNode" "escapeHtml")// indent-tabs-mode: nil// End: