brat-client
Version:
Client from brat rapid annotation tool
1,311 lines (1,184 loc) • 87.9 kB
JavaScript
// -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; -*-
// vim:set ft=javascript ts=2 sw=2 sts=2 cindent:
var VisualizerUI = (function($, window, undefined) {
var VisualizerUI = function(dispatcher, svg) {
var that = this;
var messagePostOutFadeDelay = 1000;
var messageDefaultFadeDelay = 3000;
var defaultFloatFormat = '%.1f/right';
var documentListing = null; // always documents of current collection
var selectorData = null; // can be search results when available
var searchActive = false; // whether search results received and in use
var loadedSearchData = null;
var currentForm;
var spanTypes = null;
var relationTypesHash = null;
// TODO: confirm unnecessary and remove
// var attributeTypes = null;
var data = null;
var mtime = null;
var searchConfig = null;
var coll, doc, args;
var collScroll;
var docScroll;
var user = null;
var annotationAvailable = false;
var svgElement = $(svg._svg);
var svgId = svgElement.parent().attr('id');
var maxMessages = 100;
var currentDocumentSVGsaved = false;
var fileBrowserClosedWithSubmit = false;
// normalization:
var normServerDbByNormDbName = {};
var normInfoCache = {};
var normInfoCacheSize = 0;
var normInfoCacheMaxSize = 100;
var matchFocus = '';
var matches = '';
/* START "no svg" message - related */
var noSvgTimer = null;
// this is necessary for centering
$('#no_svg_wrapper').css('display', 'table');
// on initial load, hide the "no SVG" message
$('#no_svg_wrapper').hide();
var hideNoDocMessage = function() {
clearTimeout(noSvgTimer);
$('#no_svg_wrapper').hide(0);
$('#source_files').show();
}
var showNoDocMessage = function() {
clearTimeout(noSvgTimer);
noSvgTimer = setTimeout(function() {
$('#no_svg_wrapper').fadeIn(500);
}, 2000);
$('#source_files').hide();
}
/* END "no svg" message - related */
/* START collection browser sorting - related */
var lastGoodCollection = '/';
var sortOrder = [2, 1]; // column (0..), sort order (1, -1)
var collectionSortOrder; // holds previous sort while search is active
var docSortFunction = function(a, b) {
// parent at the top
if (a[2] === '..') return -1;
if (b[2] === '..') return 1;
// then other collections
var aIsColl = a[0] == "c";
var bIsColl = b[0] == "c";
if (aIsColl !== bIsColl) return aIsColl ? -1 : 1;
// desired column in the desired order
var col = sortOrder[0];
var aa = a[col];
var bb = b[col];
if (selectorData.header[col - 2][1] === 'string-reverse') {
aa = aa.split('').reverse().join('');
bb = bb.split('').reverse().join('');
}
if (aa != bb) return (aa < bb) ? -sortOrder[1] : sortOrder[1];
// prevent random shuffles on columns with duplicate values
// (alphabetical order of documents)
aa = a[2];
bb = b[2];
if (aa != bb) return (aa < bb) ? -1 : 1;
return 0;
};
var makeSortChangeFunction = function(sort, th, thNo) {
$(th).click(function() {
// TODO: avoid magic numbers in access to the selector
// data (column 0 is type, 1 is args, rest is data)
if (sort[0] === thNo + 1) sort[1] = -sort[1];
else {
var type = selectorData.header[thNo - 1][1];
var ascending = type === "string";
sort[0] = thNo + 1;
sort[1] = ascending ? 1 : -1;
}
selectorData.items.sort(docSortFunction);
docScroll = 0;
showFileBrowser(); // resort
});
}
/* END collection browser sorting - related */
/* START message display - related */
var showPullupTrigger = function() {
$('#pulluptrigger').show('puff');
}
var $messageContainer = $('#messages');
var $messagepullup = $('#messagepullup');
var pullupTimer = null;
var displayMessages = function(msgs) {
var initialMessageNum = $messagepullup.children().length;
if (msgs === false) {
$messageContainer.children().each(function(msgElNo, msgEl) {
$(msgEl).remove();
});
} else {
$.each(msgs, function(msgNo, msg) {
var element;
var timer = null;
try {
element = $('<div class="' + msg[1] + '">' + msg[0] + '</div>');
}
catch(x) {
escaped = msg[0].replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
element = $('<div class="error"><b>[ERROR: could not display the following message normally due to malformed XML:]</b><br/>' + escaped + '</div>');
}
var pullupElement = element.clone();
$messageContainer.append(element);
$messagepullup.append(pullupElement.css('display', 'none'));
slideToggle(pullupElement, true, true);
var fader = function() {
if ($messagepullup.is(':visible')) {
element.remove();
} else {
element.hide('slow', function() {
element.remove();
});
}
};
var delay = (msg[2] === undefined)
? messageDefaultFadeDelay
: (msg[2] === -1)
? null
: (msg[2] * 1000);
if (delay === null) {
var button = $('<input type="button" value="OK"/>');
element.prepend(button);
button.click(function(evt) {
timer = setTimeout(fader, 0);
});
} else {
timer = setTimeout(fader, delay);
element.mouseover(function() {
clearTimeout(timer);
element.show();
}).mouseout(function() {
timer = setTimeout(fader, messagePostOutFadeDelay);
});
}
// setTimeout(fader, messageDefaultFadeDelay);
});
// limited history - delete oldest
var $messages = $messagepullup.children();
for (var i = 0; i < $messages.length - maxMessages; i++) {
$($messages[i]).remove();
}
}
// if there is change in the number of messages, may need to
// tweak trigger visibility
var messageNum = $messagepullup.children().length;
if (messageNum != initialMessageNum) {
if (messageNum == 0) {
// all gone; nothing to trigger
$('#pulluptrigger').hide('slow');
} else if (initialMessageNum == 0) {
// first messages, show trigger at fade
setTimeout(showPullupTrigger, messageDefaultFadeDelay+250);
}
}
};
// hide pullup trigger by default, show on first message
$('#pulluptrigger').hide();
$('#pulluptrigger').
mouseenter(function(evt) {
$('#pulluptrigger').hide('puff');
clearTimeout(pullupTimer);
slideToggle($messagepullup.stop(), true, true, true);
});
$('#messagepullup').
mouseleave(function(evt) {
setTimeout(showPullupTrigger, 500);
clearTimeout(pullupTimer);
pullupTimer = setTimeout(function() {
slideToggle($messagepullup.stop(), false, true, true);
}, 500);
});
/* END message display - related */
/* START comment popup - related */
var adjustToCursor = function(evt, element, offset, top, right) {
// get the real width, without wrapping
element.css({ left: 0, top: 0 });
var screenHeight = $(window).height();
var screenWidth = $(window).width();
// FIXME why the hell is this 22 necessary?!?
var elementHeight = element.height() + 22;
var elementWidth = element.width() + 22;
var x, y;
offset = offset || 0;
if (top) {
y = evt.clientY - elementHeight - offset;
if (y < 0) top = false;
}
if (!top) {
y = evt.clientY + offset;
}
if (right) {
x = evt.clientX + offset;
if (x >= screenWidth - elementWidth) right = false;
}
if (!right) {
x = evt.clientX - elementWidth - offset;
}
if (y < 0) y = 0;
if (x < 0) x = 0;
element.css({ top: y, left: x });
};
var commentPopup = $('#commentpopup');
var commentDisplayed = false;
var displayCommentTimer = null;
var displayComment = function(evt, target, comment, commentText, commentType, immediately) {
var idtype;
if (commentType) {
// label comment by type, with special case for default note type
var commentLabel;
if (commentType == 'AnnotatorNotes') {
commentLabel = '<b>Note:</b> ';
} else {
commentLabel = '<b>'+Util.escapeHTML(commentType)+':</b> ';
}
comment += commentLabel + Util.escapeHTMLwithNewlines(commentText);
idtype = 'comment_' + commentType;
}
commentPopup[0].className = idtype;
commentPopup.html(comment);
adjustToCursor(evt, commentPopup, 10, true, true);
clearTimeout(displayCommentTimer);
/* slight "tooltip" delay to allow highlights to be seen
before the popup obstructs them. */
displayCommentTimer = setTimeout(function() {
commentPopup.stop(true, true).fadeIn();
commentDisplayed = true;
}, immediately ? 0 : 500);
};
// to avoid clobbering on delayed response
var commentPopupNormInfoSeqId = 0;
var normInfoSortFunction = function(a, b) {
// images at the top
if (a[0].toLowerCase() == '<img>') return -1;
if (b[0].toLowerCase() == '<img>') return 1;
// otherwise stable
return Util.cmp(a[2],b[2]);
}
var fillNormInfo = function(infoData, infoSeqId) {
// extend comment popup with normalization data
var norminfo = '';
// flatten outer (name, attr, info) array (idx for sort)
var infos = [];
var idx = 0;
for (var i = 0; i < infoData.length; i++) {
for (var j = 0; j < infoData[i].length; j++) {
var label = infoData[i][j][0];
var value = infoData[i][j][1];
infos.push([label, value, idx++]);
}
}
// sort, prioritizing images (to get floats right)
infos = infos.sort(normInfoSortFunction);
// combine several consequtive values with the same label
var combined = [];
var prev_label = '';
for (var i = 0; i < infos.length; i++) {
var label = infos[i][0];
var value = infos[i][1];
if (label == prev_label) {
combined[combined.length-1][1] += ', '+value;
} else {
combined.push([label, value]);
}
prev_label = label;
}
infos = combined;
// generate HTML
for (var i = 0; i < infos.length; i++) {
var label = infos[i][0];
var value = infos[i][1];
if (label && value) {
// special treatment for some label values
if (label.toLowerCase() == '<img>') {
norminfo += ('<img class="norm_info_img" src="'+
Util.escapeHTML(value)+
'"/>');
} else {
// normal, as text
// max length restriction
if (value.length > 300) {
value = value.substr(0, 300) + ' ...';
}
norminfo += ('<span class="norm_info_label">'+
Util.escapeHTML(label)+
'</span>'+
'<span class="norm_info_value">'+':'+
Util.escapeHTML(value)+
'</span>'+
'<br/>');
}
}
}
var drop=$('#norm_info_drop_point_'+infoSeqId);
if (drop) {
drop.html(norminfo);
} else {
console.log('norm info drop point not found!'); //TODO XXX
}
}
var normCacheGet = function(dbName, dbKey, value) {
return normInfoCache[dbName+':'+dbKey];
}
var normCachePut = function(dbName, dbKey, value) {
// TODO: non-stupid cache max size limit
if (normInfoCacheSize >= normInfoCacheMaxSize) {
normInfoCache = {};
normInfoCacheSize = 0;
}
normInfoCache[dbName+':'+dbKey] = value;
normInfoCacheSize++;
}
var displaySpanComment = function(
evt, target, spanId, spanType, mods, spanText, commentText,
commentType, normalizations) {
var immediately = false;
var comment = ( '<div><span class="comment_type_id_wrapper">' +
'<span class="comment_type">' +
Util.escapeHTML(Util.spanDisplayForm(spanTypes,
spanType)) +
'</span>' +
' ' +
'<span class="comment_id">' +
'ID:'+Util.escapeHTML(spanId) +
'</span></span>' );
if (mods.length) {
comment += '<div>' + Util.escapeHTML(mods.join(', ')) + '</div>';
}
comment += '</div>';
comment += ('<div class="comment_text">"' +
Util.escapeHTML(spanText) +
'"</div>');
var validArcTypesForDrag = dispatcher.post('getValidArcTypesForDrag', [spanId, spanType]);
if (validArcTypesForDrag && validArcTypesForDrag[0]) {
if (validArcTypesForDrag[0].length) {
comment += '<div>' + validArcTypesForDrag[0].join(', ') + '</div>';
} else {
$('rect[data-span-id="' + spanId + '"]').addClass('badTarget');
}
immediately = true;
}
// process normalizations
var normsToQuery = [];
$.each(normalizations, function(normNo, norm) {
var dbName = norm[0], dbKey = norm[1];
comment += ( '<hr/>' +
'<span class="comment_id">' +
Util.escapeHTML(dbName) + ':' +
Util.escapeHTML(dbKey) + '</span>');
if (dbName in normServerDbByNormDbName &&
normServerDbByNormDbName[dbName] != '<NONE>') {
// DB available, add drop-off point to HTML and store
// query parameters
commentPopupNormInfoSeqId++;
comment += ('<br/><div id="norm_info_drop_point_'+
commentPopupNormInfoSeqId+'"/>');
normsToQuery.push([dbName, dbKey, commentPopupNormInfoSeqId]);
} else {
// no DB, just attach "human-readable" text provided
// with the annotation, if any
if (norm[2]) {
comment += ('<br/><span class="norm_info_value">'+
Util.escapeHTML(norm[2])+'</span>');
}
}
});
// display initial comment HTML
displayComment(evt, target, comment, commentText, commentType,
immediately);
// initiate AJAX calls for the normalization data to query
$.each(normsToQuery, function(normqNo, normq) {
// TODO: cache some number of most recent norm_get_data results
var dbName = normq[0], dbKey = normq[1], infoSeqId = normq[2];
if (normCacheGet(dbName, dbKey)) {
fillNormInfo(normCacheGet(dbName, dbKey), infoSeqId);
} else {
dispatcher.post('ajax', [{
action: 'normData',
database: dbName,
key: dbKey,
collection: coll,
},
function(response) {
if (response.exception) {
; // TODO: response to error
} else if (!response.value) {
; // TODO: response to missing key
} else {
fillNormInfo(response.value, infoSeqId);
normCachePut(dbName, dbKey, response.value);
}
}]);
}
});
};
var onDocChanged = function() {
commentPopup.hide();
commentDisplayed = false;
};
var displayArcComment = function(
evt, target, symmetric, arcId,
originSpanId, originSpanType, role,
targetSpanId, targetSpanType,
commentText, commentType) {
var arcRole = target.attr('data-arc-role');
// in arrowStr, — == mdash, → == Unicode right arrow
var arrowStr = symmetric ? '—' : '→';
var arcDisplayForm = Util.arcDisplayForm(spanTypes,
data.spans[originSpanId].type,
arcRole,
relationTypesHash);
var comment = "";
comment += ('<span class="comment_type_id_wrapper">' +
'<span class="comment_type">' +
Util.escapeHTML(Util.spanDisplayForm(spanTypes,
originSpanType)) +
' ' + arrowStr + ' ' +
Util.escapeHTML(arcDisplayForm) +
' ' + arrowStr + ' ' +
Util.escapeHTML(Util.spanDisplayForm(spanTypes,
targetSpanType)) +
'</span>' +
'<span class="comment_id">' +
(arcId ? 'ID:'+arcId :
Util.escapeHTML(originSpanId) +
arrowStr +
Util.escapeHTML(targetSpanId)) +
'</span>' +
'</span>');
comment += ('<div class="comment_text">' +
Util.escapeHTML('"'+data.spans[originSpanId].text+'"') +
arrowStr +
Util.escapeHTML('"'+data.spans[targetSpanId].text + '"') +
'</div>');
displayComment(evt, target, comment, commentText, commentType);
};
var displaySentComment = function(
evt, target, commentText, commentType) {
displayComment(evt, target, '', commentText, commentType);
};
var hideComment = function() {
clearTimeout(displayCommentTimer);
if (commentDisplayed) {
commentPopup.stop(true, true).fadeOut(function() { commentDisplayed = false; });
}
};
var onMouseMove = function(evt) {
if (commentDisplayed) {
adjustToCursor(evt, commentPopup, 10, true, true);
}
};
/* END comment popup - related */
/* START form management - related */
initForm = function(form, opts) {
opts = opts || {};
var formId = form.attr('id');
// alsoResize is special
var alsoResize = opts.alsoResize;
delete opts.alsoResize;
// Always add OK and Cancel
var buttons = (opts.buttons || []);
if (opts.no_ok) {
delete opts.no_ok;
} else {
buttons.push({
id: formId + "-ok",
text: "OK",
click: function() { form.submit(); }
});
}
if (opts.no_cancel) {
delete opts.no_cancel;
} else {
buttons.push({
id: formId + "-cancel",
text: "Cancel",
click: function() { form.dialog('close'); }
});
}
delete opts.buttons;
opts = $.extend({
autoOpen: false,
closeOnEscape: true,
buttons: buttons,
modal: true
}, opts);
form.dialog(opts);
form.bind('dialogclose', function() {
if (form == currentForm) {
currentForm = null;
}
});
// HACK: jQuery UI's dialog does not support alsoResize
// nor does resizable support a jQuery object of several
// elements
// See: http://bugs.jqueryui.com/ticket/4666
if (alsoResize) {
form.parent().resizable('option', 'alsoResize',
'#' + form.attr('id') + ', ' + alsoResize);
}
};
var unsafeDialogOpen = function($dialog) {
// does not restrict tab key to the dialog
// does not set the focus, nor change position
// but is much faster than dialog('open') for large dialogs, see
// https://github.com/nlplab/brat/issues/934
var self = $dialog.dialog('instance');
if (self._isOpen) { return; }
self._isOpen = true;
self.opener = $(self.document[0].activeElement);
self._size();
self._createOverlay();
self._moveToTop(null, true);
if (self.overlay) {
self.overlay.css( "z-index", self.uiDialog.css( "z-index" ) - 1 );
}
self._show(self.uiDialog, self.options.show);
self._trigger('open');
};
var showForm = function(form, unsafe) {
currentForm = form;
// as suggested in http://stackoverflow.com/questions/2657076/jquery-ui-dialog-fixed-positioning
form.parent().css({position:"fixed"});
if (unsafe) {
unsafeDialogOpen(form);
} else {
form.dialog('open');
}
slideToggle($('#pulldown').stop(), false);
return form;
};
var hideForm = function() {
if (!currentForm) return;
// currentForm.fadeOut(function() { currentForm = null; });
currentForm.dialog('close');
currentForm = null;
};
/* END form management - related */
/* START collection browser - related */
var selectElementInTable = function(table, docname, mf) {
table = $(table);
table.find('tr').removeClass('selected');
var sel = 'tr';
var $element;
if (docname) {
sel += '[data-doc="' + docname + '"]';
if (mf) {
sel += '[data-mf="' + Util.paramArray(mf) + '"]';
}
var $element = table.find(sel).first();
$element.addClass('selected');
}
matchFocus = $element && $element.attr('data-mf');
matches = $element && $element.attr('data-match');
}
var chooseDocument = function(evt) {
var $element = $(evt.target).closest('tr');
$('#document_select tr').removeClass('selected');
$('#document_input').val($element.attr('data-doc'));
$element.addClass('selected');
matchFocus = $element.attr('data-mf');
matches = $element.attr('data-match');
}
var chooseDocumentAndSubmit = function(evt) {
chooseDocument(evt);
fileBrowserSubmit(evt);
}
var fileBrowser = $('#collection_browser');
initForm(fileBrowser, {
alsoResize: '#document_select',
close: function(evt) {
if (!doc) {
// no document; set and show the relevant message, and
// clear the "blind" unless waiting for a collection
if (fileBrowserClosedWithSubmit) {
$('#no_document_message').hide();
$('#loading_message').show();
} else {
$('#loading_message').hide();
$('#no_document_message').show();
$('#waiter').dialog('close');
}
showNoDocMessage();
} else if (!fileBrowserClosedWithSubmit && !searchActive) {
dispatcher.post('setArguments', [{}, true]);
}
},
width: 500
});
/* XXX removed per #900
// insert the Save link
var $fileBrowserButtonset = fileBrowser.
parent().find('.ui-dialog-buttonpane .ui-dialog-buttonset').prepend(' ');
$('<a href="ajax.cgi?action=downloadSearchFile" id="save_search">Save</a>').
prependTo($fileBrowserButtonset).button().css('display', 'none');
*/
var docInputHandler = function(evt) {
selectElementInTable('#document_select', $(this).val());
};
$('#document_input').keyup(docInputHandler);
var fileBrowserSubmit = function(evt) {
var _coll, _doc, _args, found;
var input = $('#document_input').
val().
replace(/\/?\s+$/, '').
replace(/^\s+/, '');
if (!input.length) return false;
if (input.substr(0, 2) === '..') {
// ..
var pos = coll.substr(0, coll.length - 1).lastIndexOf('/');
if (pos === -1) {
dispatcher.post('messages', [[['At the root', 'error', 2]]]);
$('#document_input').focus().select();
return false;
} else {
_coll = coll.substr(0, pos + 1);
_doc = '';
}
} else if (found = input.match(/^(\/?)((?:[^\/]*\/)*)([^\/?]*)$/)) {
var abs = found[1];
var collname = found[2].substr(0, found[2].length - 1);
var docname = found[3];
if (abs) {
_coll = abs + collname;
if (_coll.length < 2) coll += '/';
_doc = docname;
} else {
if (collname) collname += '/';
_coll = coll + collname;
_doc = docname;
}
} else {
dispatcher.post('messages', [[['Invalid document name format', 'error', 2]]]);
$('#document_input').focus().select();
}
docScroll = $('#document_select')[0].scrollTop;
fileBrowser.find('#document_select tbody').empty();
if (coll != _coll || doc != _doc ||
!Util.isEqual(Util.paramArray(args.matchfocus), (matchFocus || []))) {
// something changed
// set to allow keeping "blind" down during reload
fileBrowserClosedWithSubmit = true;
// ... and change BG message to a more appropriate one
// trigger clear and changes if something other than the
// current thing is chosen, but only blank screen before
// render if the document changed (prevent "flicker" on
// e.g. picking search results)
if (coll != _coll || doc != _doc) {
dispatcher.post('clearSVG');
}
dispatcher.post('allowReloadByURL');
var newArgs = [];
if (matchFocus) newArgs.push('matchfocus=' + matchFocus);
if (matches) newArgs.push('match=' + matches);
dispatcher.post('setCollection', [_coll, _doc, Util.deparam(newArgs.join('&'))]);
} else {
// hide even on select current thing
hideForm();
}
return false;
};
fileBrowser.
submit(fileBrowserSubmit).
bind('reset', hideForm);
var fileBrowserWaiting = false;
var showFileBrowser = function() {
// keep tabs on how the browser is closed; we need to keep the
// "blind" up when retrieving a collection, but not when canceling
// without selection (would hang the UI)
fileBrowserClosedWithSubmit = false;
// no point in showing this while the browser is shown
hideNoDocMessage();
if (currentForm == tutorialForm) {
fileBrowserWaiting = true;
return;
}
fileBrowserWaiting = false;
// hide "no document" message when file browser shown
// TODO: can't make this work; I can't detect when it gets hidden.
// hideNoDocMessage();
if (!(selectorData && showForm(fileBrowser))) return false;
var html = ['<tr><th/>'];
var tbody;
$.each(selectorData.header, function(headNo, head) {
html.push('<th>' + head[0] + '</th>');
});
html.push('</tr>');
$('#document_select thead').html(html.join(''));
html = [];
// NOTE: we seem to have some excessive sorting going on;
// disabling this as a test. If everything works, just remove
// the following commented-out line (and this comment):
//selectorData.items.sort(docSortFunction);
$.each(selectorData.items, function(docNo, doc) {
var isColl = doc[0] == "c"; // "collection"
// second column is optional annotation-specific pointer,
// used (at least) for search results
var annp = doc[1] ? ('?' + Util.escapeHTML(Util.param(doc[1]))) : '';
var name = Util.escapeHTML(doc[2]);
var collFile = isColl ? 'collection' : 'file';
//var collFileImg = isColl ? 'ic_list_folder.png' : 'ic_list_drafts.png';
//var collFileImg = isColl ? 'Fugue-folder-horizontal-open.png' : 'Fugue-document.png';
var collFileImg = isColl ? 'Fugue-shadowless-folder-horizontal-open.png' : 'Fugue-shadowless-document.png';
var collSuffix = isColl ? '/' : '';
if (doc[1]) {
var matchfocus = doc[1].matchfocus || [];
var mfstr = ' data-mf="' + Util.paramArray(matchfocus) + '"';
var match = doc[1].match || [];
var matchstr = ' data-match="' + Util.paramArray(match) + '"';
} else {
var matchstr = '';
var mfstr = '';
}
html.push('<tr class="' + collFile + '" data-doc="'
+ name + collSuffix + '"' + matchstr + mfstr + '>');
html.push('<th><img src="static/img/' + collFileImg + '" alt="' + collFile + '"/></th>');
html.push('<th>' + name + collSuffix + '</th>');
var len = selectorData.header.length - 1;
for (var i = 0; i < len; i++) {
var type = selectorData.header[i + 1][1];
var datum = doc[i + 3];
// format rest according to "data type" specified in header
var formatted = null;
var cssClass = null;
if (!type) {
console.error('Missing document list data type');
formatted = datum;
} else if (datum === undefined) {
formatted = '';
} else if (type === 'string') {
formatted = Util.escapeHTML(datum);
} else if (type === 'string-right' || type === 'string-reverse') {
formatted = Util.escapeHTML(datum);
cssClass = 'rightalign';
} else if (type === 'string-center') {
formatted = Util.escapeHTML(datum);
cssClass = 'centeralign';
} else if (type === 'time') {
formatted = Util.formatTimeAgo(datum * 1000);
} else if (type === 'float') {
type = defaultFloatFormat;
cssClass = 'rightalign';
} else if (type === 'int') {
formatted = '' + datum;
cssClass = 'rightalign';
}
if (formatted === null) {
var m = type.match(/^(.*?)(?:\/(right))?$/);
cssClass = m[2] ? 'rightalign' : null;
formatted = sprintf(m[1], datum);
}
html.push('<td' + (cssClass ? ' class="' + cssClass + '"' : '') + '>' +
formatted + '</td>');
}
html.push('</tr>');
});
html = html.join('');
tbody = $('#document_select tbody').html(html);
$('#document_select')[0].scrollTop = docScroll;
tbody.find('tr').
click(chooseDocument).
dblclick(chooseDocumentAndSubmit);
$('#document_select thead tr *').each(function(thNo, th) {
makeSortChangeFunction(sortOrder, th, thNo);
});
$('#collection_input').val(selectorData.collection);
$('#document_input').val(doc);
$('#readme').val(selectorData.description || '');
if (selectorData.description &&
(selectorData.description.match(/\n/) ||
selectorData.description.length > 50)) {
// multi-line or long description; show "more" button and fill
// dialog text
$('#more_readme_button').button(); // TODO: more reasonable place
$('#more_readme_button').show();
// only display text up to the first newline in the short info
var split_readme_text = selectorData.description.match(/^[^\n]*/);
$('#readme').val(split_readme_text[0]);
$('#more_info_readme').text(selectorData.description);
} else {
// empty or short, single-line description; no need for more
$('#more_readme_button').hide();
$('#more_info_readme').text('');
}
selectElementInTable($('#document_select'), doc, args.matchfocus);
setTimeout(function() {
$('#document_input').focus().select();
}, 0);
}; // end showFileBrowser()
$('#collection_browser_button').click(function(evt) {
dispatcher.post('clearSearch');
});
var currentSelectorPosition = function() {
var pos;
$.each(selectorData.items, function(docNo, docRow) {
if (docRow[2] == doc) {
// args may have changed, so lacking a perfect match return
// last matching document as best guess
pos = docNo;
// check whether 'focus' agrees; the rest of the args are
// irrelevant for determining position.
var collectionArgs = docRow[1] || {};
if (Util.isEqual(collectionArgs.matchfocus, args.matchfocus)) {
pos = docNo;
return false;
}
}
});
return pos;
}
/* END collection browser - related */
/* START search - related */
var addSpanTypesToSelect = function($select, types, included) {
if (!included) included = {};
if (!included['']) {
included[''] = true;
$select.html('<option value="">- Any -</option>');
}
$.each(types, function(typeNo, type) {
if (type !== null) {
if (!included[type.name]) {
included[type.name] = true;
var $option = $('<option value="' + Util.escapeQuotes(type.type) + '"/>').text(type.name);
$select.append($option);
if (type.children) {
addSpanTypesToSelect($select, type.children, included);
}
}
}
});
};
var rememberNormDb = function(response) {
// the visualizer needs to remember aspects of the norm setup
// so that it can avoid making queries for unconfigured or
// missing normalization DBs.
var norm_resources = response.normalization_config || [];
$.each(norm_resources, function(normNo, norm) {
var normName = norm[0];
var serverDb = norm[3];
normServerDbByNormDbName[normName] = serverDb;
});
}
var setupSearchTypes = function(response) {
addSpanTypesToSelect($('#search_form_entity_type'), response.entity_types);
addSpanTypesToSelect($('#search_form_event_type'), response.event_types);
addSpanTypesToSelect($('#search_form_relation_type'), response.relation_types);
// nice-looking selects and upload fields
$('#search_form select').addClass('ui-widget ui-state-default ui-button-text');
$('#search_form_load_file').addClass('ui-widget ui-state-default ui-button-text');
}
// when event role changes, event types do as well
var searchEventRoles = [];
var searchEventRoleChanged = function(evt) {
var $type = $(this).parent().next().children('select');
var type = $type.val();
var role = $(this).val();
var origin = $('#search_form_event_type').val();
var eventType = spanTypes[origin];
var arcTypes = eventType && eventType.arcs || [];
var arcType = null;
$type.html('<option value="">- Any -</option>');
$.each(arcTypes, function(arcNo, arcDesc) {
if (arcDesc.type == role) {
arcType = arcDesc;
return false;
}
});
var targets = arcType && arcType.targets || [];
$.each(targets, function(targetNo, target) {
var spanType = spanTypes[target];
var spanName = spanType.name || spanType.labels[0] || target;
var option = '<option value="' + Util.escapeQuotes(target) + '">' + Util.escapeHTML(spanName) + '</option>'
$type.append(option);
});
// return the type to the same value, if possible
if (type) {
$type.val(type);
};
};
$('#search_form_event_roles').on('change', '.search_event_role select', searchEventRoleChanged);
// adding new role rows
var addEmptySearchEventRole = function() {
var $roles = $('#search_form_event_roles');
var rowNo = $roles.children().length;
var $role = $('<select class="fullwidth"/>');
$role.append('<option value="">- Any -</option>');
$.each(searchEventRoles, function(arcTypePairNo, arcTypePair) {
var option = '<option value="' + Util.escapeQuotes(arcTypePair[0]) + '">' + Util.escapeHTML(arcTypePair[1]) + '</option>'
$role.append(option);
});
var $type = $('<select class="fullwidth"/>');
var $text = $('<input class="fullwidth"/>');
var button = $('<input type="button"/>');
var rowButton = $('<td/>').append(button);
if (rowNo) {
rowButton.addClass('search_event_role_del');
button.val('\u2013'); // n-dash
} else {
rowButton.addClass('search_event_role_add');
button.val('+');
}
var $tr = $('<tr/>').
append($('<td class="search_event_role"/>').append($role)).
append($('<td class="search_event_type"/>').append($type)).
append($('<td class="search_event_text"/>').append($text)).
append(rowButton);
$roles.append($tr);
$role.trigger('change');
// style selector
$role.addClass('ui-widget ui-state-default ui-button-text');
$type.addClass('ui-widget ui-state-default ui-button-text');
// style button
button.button();
button.addClass('small-buttons ui-button-text').removeClass('ui-button');
};
// deleting role rows
var delSearchEventRole = function(evt) {
$row = $(this).closest('tr');
$row.remove();
}
$('#search_form_event_roles').on('click', '.search_event_role_add input', addEmptySearchEventRole);
$('#search_form_event_roles').on('click', '.search_event_role_del input', delSearchEventRole);
// When event type changes, the event roles do as well
// Also, put in one empty role row
$('#search_form_event_type').change(function(evt) {
var $roles = $('#search_form_event_roles').empty();
searchEventRoles = [];
var eventType = spanTypes[$(this).val()];
var arcTypes = eventType && eventType.arcs || [];
$.each(arcTypes, function(arcTypeNo, arcType) {
var arcTypeName = arcType.labels && arcType.labels[0] || arcType.type;
searchEventRoles.push([arcType.type, arcTypeName]);
});
addEmptySearchEventRole();
});
// when relation changes, change choices of arg1 type
$('#search_form_relation_type').change(function(evt) {
var relTypeType = $(this).val();
var $arg1 = $('#search_form_relation_arg1_type').
html('<option value="">- Any -</option>');
var $arg2 = $('#search_form_relation_arg2_type').empty();
$.each(spanTypes,
function(spanTypeType, spanType) {
if (spanType.arcs) {
$.each(spanType.arcs, function(arcTypeNo, arcType) {
if (arcType.type === relTypeType) {
var spanName = spanType.name;
var option = '<option value="' + Util.escapeQuotes(spanTypeType) + '">' + Util.escapeHTML(spanName) + '</option>'
$arg1.append(option);
}
});
}
});
$('#search_form_relation_arg1_type').change();
// style the selects
$arg1.addClass('ui-widget ui-state-default ui-button-text');
$arg2.addClass('ui-widget ui-state-default ui-button-text');
});
// when arg1 type changes, change choices of arg2 type
$('#search_form_relation_arg1_type').change(function(evt) {
var $arg2 = $('#search_form_relation_arg2_type').
html('<option value="">- Any -</option>');
var relType = $('#search_form_relation_type').val();
var arg1Type = spanTypes[$(this).val()];
var arcTypes = arg1Type && arg1Type.arcs || [];
var arcType = null;
$.each(arcTypes, function(arcNo, arcDesc) {
if (arcDesc.type == relType) {
arcType = arcDesc;
return false;
}
});
if (arcType && arcType.targets) {
$.each(arcType.targets, function(spanTypeNo, spanTypeType) {
var spanName = Util.spanDisplayForm(spanTypes, spanTypeType);
var option = '<option value="' + Util.escapeQuotes(spanTypeType) + '">' + Util.escapeHTML(spanName) + '</option>'
$arg2.append(option);
});
}
});
$('#search_form_note_category').change(function(evt) {
var category = $(this).val();
var $type = $('#search_form_note_type');
if ($.inArray(category, ['entity', 'event', 'relation']) != -1) {
$type.html($('#search_form_' + category + '_type').html()).val('');
$('#search_form_note_type_row:not(:visible)').show('highlight');
} else {
$type.html('');
$('#search_form_note_type_row:visible').hide('highlight');
}
});
// context length setting should only be visible if
// concordancing is on
// TODO: @amadanmath: help, my jQuery is horrible
if ($('#concordancing_on').is(':checked')) {
$('#context_size_div').show("highlight");
} else {
$('#context_size_div').hide("highlight");
}
$('#concordancing input[type="radio"]').change(function() {
if ($('#concordancing_on').is(':checked')) {
$('#context_size_div').show("highlight");
} else {
$('#context_size_div').hide("highlight");
}
});
$('#search_options div.advancedOptions').hide("highlight");
// set up advanced search options; only visible is clicked
var advancedSearchOptionsVisible = false;
$('#advanced_search_option_toggle').click(function(evt) {
if (advancedSearchOptionsVisible) {
$('#search_options div.advancedOptions').hide("highlight");
$('#advanced_search_option_toggle').text("Show advanced");
} else {
$('#search_options div.advancedOptions').show("highlight");
$('#advanced_search_option_toggle').text("Hide advanced");
}
advancedSearchOptionsVisible = !advancedSearchOptionsVisible;
// block default
return false;
});
var activeSearchTab = function() {
// activeTab: 0 = Text, 1 = Entity, 2 = Event, 3 = Relation, 4 = Notes, 5 = Load
var activeTab = $('#search_tabs').tabs('option', 'active');
return ['searchText', 'searchEntity', 'searchEvent',
'searchRelation', 'searchNote', 'searchLoad'][activeTab];
}
var onSearchTabSelect = function() {
var action = activeSearchTab();
switch (action) {
case 'searchText':
$('#search_form_text_text').focus().select();
break;
case 'searchEntity':
$('#search_form_entity_text').focus().select();
break;
case 'searchEvent':
$('#search_form_event_trigger').focus().select();
break;
case 'searchRelation':
$('#search_form_relation_type').focus().select();
break;
case 'searchNote':
$('#search_form_note_text').focus().select();
break;
case 'searchLoad':
$('#search_form_load_file').focus().select();
break;
}
};
// set up jQuery UI elements in search form
$('#search_tabs').tabs({
show: onSearchTabSelect
});
$('#search_form').find('.radio_group').buttonset();
var applySearchResults = function(response) {
if (!searchActive) {
collectionSortOrder = sortOrder;
}
dispatcher.post('searchResultsReceived', [response]);
searchActive = true;
updateSearchButtons();
};
var searchForm = $('#search_form');
var searchFormSubmit = function(evt) {
// hack around empty document; "" would be interpreted as
// missing argument by server dispatcher (issue #513)
// TODO: do this properly, avoiding magic strings
var action = activeSearchTab();
var docArg = doc ? doc : "/NO-DOCUMENT/";
var opts = {
action : action,
collection : coll,
document: docArg,
// TODO the search form got complex :)
};
switch (action) {
case 'searchText':
opts.text = $('#search_form_text_text').val();
if (!opts.text.length) {
dispatcher.post('messages', [[['Please fill in the text to search for!', 'comment']]]);
return false;
}
break;
case 'searchEntity':
opts.type = $('#search_form_entity_type').val() || '';
opts.text = $('#search_form_entity_text').val();
break;
case 'searchEvent':
opts.type = $('#search_form_event_type').val() || '';
opts.trigger = $('#search_form_event_trigger').val();
var eargs = [];
$('#search_form_event_roles tr').each(function() {
var earg = {};
earg.role = $(this).find('.search_event_role select').val() || '';
earg.type = $(this).find('.search_event_type select').val() || '';
earg.text = $(this).find('.search_event_text input').val();
eargs.push(earg);
});
opts.args = $.toJSON(eargs);
break;
case 'searchRelation':
opts.type = $('#search_form_relation_type').val() || '';
opts.arg1 = $('#search_form_relation_arg1_text').val();
opts.arg1type = $('#search_form_relation_arg1_type').val() || '';
opts.arg2 = $('#search_form_relation_arg2_text').val();
opts.arg2type = $('#search_form_relation_arg2_type').val() || '';
opts.show_text = $('#search_form_relation_show_arg_text_on').is(':checked');
opts.show_type = $('#search_form_relation_show_arg_type_on').is(':checked');
break;
case 'searchNote':
opts.category = $('#search_form_note_category').val() || '';
opts.type = $('#search_form_note_type').val() || '';
opts.text = $('#search_form_note_text').val() || '';
break;
case 'searchLoad':
applySearchResults(loadedSearchData);
return false;
}
// fill in scope of search ("document" / "collection")
var searchScope = $('#search_scope input:checked').val();
opts.scope = searchScope;
// adjust specific action to invoke by scope
if (searchScope == "document") {
opts.action = opts.action + "InDocument";
} else {
opts.action = opts.action + "InCollection";
}
// fill in concordancing options
opts.concordancing = $('#concordancing_on').is(':checked');
opts.context_length = $('#context_length').val();
// fill in text match options
opts.text_match = $('#text_match input:checked').val()
opts.match_case = $('#match_case_on').is(':checked');
dispatcher.post('hideForm');
dispatcher.post('ajax', [opts, function(response) {
if(response && response.items && response.items.length == 0) {
// TODO: might consider having this message come from the
// server instead
dispatcher.post('messages', [[['No matches to search.', 'comment']]]);
dispatcher.post('clearSearch', [true]);
} else {
applySearchResults(response);
}
}]);
return false;
};
$('#search_form_load_file').change(function(evt) {
var $file = $('#search_form_load_file');
var file = $file[0].files[0];
var reader = new FileReader();
reader.onerror = function(evt) {
dispatcher.post('messages', [[['The file could not be read.', 'error']]]);
};
reader.onloadend = function(evt) {
try {
loadedSearchData = JSON.parse(evt.target.result);
// TODO XXX check for validity of contents, not just whether
// it's valid JSON or not; throw something if not
} catch (x) {
dispatc