UNPKG

brat-client

Version:

Client from brat rapid annotation tool

1,311 lines (1,184 loc) 87.9 kB
// -*- 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); 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, &#8212 == mdash, &#8594 == Unicode right arrow var arrowStr = symmetric ? '&#8212;' : '&#8594;'; 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