UNPKG

brat-client

Version:

Client from brat rapid annotation tool

1,207 lines (1,127 loc) 113 kB
// -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; -*- // vim:set ft=javascript ts=2 sw=2 sts=2 cindent: var AnnotatorUI = (function($, window, undefined) { var AnnotatorUI = function(dispatcher, svg) { var that = this; var arcDragOrigin = null; var arcDragOriginBox = null; var arcDragOriginGroup = null; var arcDragArc = null; var arcDragJustStarted = false; var sourceData = null; var data = null; var searchConfig = null; var spanOptions = null; var lockOptions = null; var rapidSpanOptions = null; var arcOptions = null; var spanKeymap = null; var keymap = null; var coll = null; var doc = null; var reselectedSpan = null; var selectedFragment = null; var editedSpan = null; var editedFragment = null; var repeatingArcTypes = []; var spanTypes = null; var entityAttributeTypes = null; var eventAttributeTypes = null; var relationTypesHash = null; var showValidAttributes; // callback function var showValidNormalizations; // callback function var dragStartedAt = null; var selRect = null; var lastStartRec = null; var lastEndRec = null; var inForm = false; var draggedArcHeight = 30; var maxNormSearchHistory = 10; // TODO: this is an ugly hack, remove (see comment with assignment) var lastRapidAnnotationEvent = null; // TODO: another avoidable global; try to work without var rapidAnnotationDialogVisible = false; // amount by which to lighten (adjust "L" in HSL space) span // colors for type selection box BG display. 0=no lightening, // 1=white BG (no color) var spanBoxTextBgColorLighten = 0.4; // for double-click selection simulation hack var lastDoubleClickedChunkId = null; // for normalization: URLs bases by norm DB name var normDbUrlByDbName = {}; var normDbUrlBaseByDbName = {}; // for normalization: appropriate DBs per type var normDbsByType = {}; // for normalization var oldSpanNormIdValue = ''; var lastNormSearches = []; that.user = null; var svgElement = $(svg._svg); var svgId = svgElement.parent().attr('id'); var arcTargets = []; var arcTargetRects; var stripNumericSuffix = function(s) { // utility function, originally for stripping numerix suffixes // from arc types (e.g. "Theme2" -> "Theme"). For values // without suffixes (including non-strings), returns given value. if (typeof(s) != "string") { return s; // can't strip } var m = s.match(/^(.*?)(\d*)$/); return m[1]; // always matches } var showForm = function() { inForm = true; }; var hideForm = function() { inForm = false; keymap = null; rapidAnnotationDialogVisible = false; }; var clearSelection = function() { window.getSelection().removeAllRanges(); if (selRect != null) { for(var s=0; s != selRect.length; s++) { selRect[s].parentNode.removeChild(selRect[s]); } selRect = null; lastStartRec = null; lastEndRec = null; } }; var makeSelRect = function(rx, ry, rw, rh, col) { var selRect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); selRect.setAttributeNS(null, "width", rw); selRect.setAttributeNS(null, "height", rh); selRect.setAttributeNS(null, "x", rx); selRect.setAttributeNS(null, "y", ry); selRect.setAttributeNS(null, "fill", col == undefined ? "lightblue" : col); return selRect; }; var onKeyDown = function(evt) { var code = evt.which; if (code === $.ui.keyCode.ESCAPE) { setTypeLock(false); stopArcDrag(); hideForm(); if (reselectedSpan) { $(reselectedSpan.rect).removeClass('reselect'); reselectedSpan = null; svgElement.removeClass('reselect'); } return; } // in rapid annotation mode, prioritize the keys 0..9 for the // ordered choices in the quick annotation dialog. if (Configuration.rapidModeOn && rapidAnnotationDialogVisible && "0".charCodeAt() <= code && code <= "9".charCodeAt()) { var idx = String.fromCharCode(code); var $input = $('#rapid_span_'+idx); if ($input.length) { $input.click(); } } if (!inForm && code == $.ui.keyCode.ENTER) { evt.preventDefault(); tryToAnnotate(evt); return; } if (!keymap) return; // disable shortcuts when working with elements that you could // conceivably type in var target = evt.target; var nodeName = target.nodeName.toLowerCase(); var nodeType = target.type && target.type.toLowerCase(); if (nodeName == 'input' && (nodeType == 'text' || nodeType == 'password')) return; if (nodeName == 'textarea' || nodeName == 'select') return; var prefix = ''; if (evt.altKey) { prefix = "A-"; } if (Util.isMac ? evt.metaKey : evt.ctrlKey) { prefix = "C-"; } if (evt.shiftKey) { prefix = "S-"; } var binding = keymap[prefix + code]; if (!binding) binding = keymap[prefix + String.fromCharCode(code)]; if (binding) { var boundInput = $('#' + binding)[0]; if (boundInput && !boundInput.disabled) { boundInput.click(); evt.preventDefault(); return false; } } }; var onDblClick = function(evt) { // must be logged in if (that.user === null) return; // must not be reselecting a span or an arc if (reselectedSpan || arcDragOrigin) return; var target = $(evt.target); var id; // do we edit an arc? if (id = target.attr('data-arc-role')) { // TODO clearSelection(); var originSpanId = target.attr('data-arc-origin'); var targetSpanId = target.attr('data-arc-target'); var type = target.attr('data-arc-role'); var originSpan = data.spans[originSpanId]; var targetSpan = data.spans[targetSpanId]; arcOptions = { action: 'createArc', origin: originSpanId, target: targetSpanId, old_target: targetSpanId, type: type, old_type: type, collection: coll, 'document': doc }; var eventDescId = target.attr('data-arc-ed'); if (eventDescId) { var eventDesc = data.eventDescs[eventDescId]; arcOptions.id = eventDescId; arcOptions.comment = eventDesc.comment && eventDesc.comment.text; if (eventDesc.equiv) { arcOptions['left'] = eventDesc.leftSpans.join(','); arcOptions['right'] = eventDesc.rightSpans.join(','); } } else { arcOptions.id = originSpanId + "~" + type + "~" + targetSpanId; } $('#arc_origin').text(Util.spanDisplayForm(spanTypes, originSpan.type) + ' ("' + originSpan.text + '")'); $('#arc_target').text(Util.spanDisplayForm(spanTypes, targetSpan.type) + ' ("' + targetSpan.text + '")'); var arcId = eventDescId || [originSpanId, type, targetSpanId]; fillArcTypesAndDisplayForm(evt, originSpan.type, targetSpan.type, type, arcId); // for precise timing, log dialog display to user. dispatcher.post('logAction', ['arcEditSelected']); // if not an arc, then do we edit a span? } else if (id = target.attr('data-span-id')) { clearSelection(); editedSpan = data.spans[id]; editedFragment = target.attr('data-fragment-id'); // XXX we went from collecting fragment offsets to copying // them from data. Does anything break? spanOptions = { action: 'createSpan', offsets: editedSpan.unsegmentedOffsets, type: editedSpan.type, id: id, }; if (lockOptions) { spanFormSubmit(); dispatcher.post('logAction', ['spanLockEditSubmitted']); } else { fillSpanTypesAndDisplayForm(evt, editedSpan.text, editedSpan); // for precise timing, log annotation display to user. dispatcher.post('logAction', ['spanEditSelected']); } } // if not an arc or a span, is this a double-click on text? else if (id = target.attr('data-chunk-id')) { // remember what was clicked (this is in preparation for // simulating double-click selection on browsers that do // not support it. lastDoubleClickedChunkId = id; } }; var startArcDrag = function(originId) { if (reselectedSpan) return; clearSelection(); svgPosition = svgElement.offset(); svgElement.addClass('unselectable'); arcDragOrigin = originId; arcDragOriginGroup = $(data.spans[arcDragOrigin].group); arcDragOriginGroup.addClass('highlight'); var headFragment = data.spans[arcDragOrigin].headFragment; var chunk = headFragment.chunk; var fragBox = headFragment.rectBox; arcDragOriginBox = { x: fragBox.x + chunk.translation.x, y: fragBox.y + chunk.row.translation.y, height: fragBox.height, width: fragBox.width, center: fragBox.x + chunk.translation.x + fragBox.width / 2, }; arcDragJustStarted = true; }; var getValidArcTypesForDrag = function(targetId, targetType) { var arcType = stripNumericSuffix(arcOptions && arcOptions.type); if (!arcDragOrigin || targetId == arcDragOrigin) return null; var originType = data.spans[arcDragOrigin].type; var spanType = spanTypes[originType]; var result = []; if (spanType && spanType.arcs) { $.each(spanType.arcs, function(arcNo, arc) { if (arcType && arcType != arc.type) return; if ($.inArray(targetType, arc.targets) != -1) { result.push(arc.type); } }); } return result; }; var onMouseDown = function(evt) { dragStartedAt = evt; // XXX do we really need the whole evt? if (!that.user || arcDragOrigin) return; var target = $(evt.target); var id; // is it arc drag start? if (id = target.attr('data-span-id')) { arcOptions = null; startArcDrag(id); evt.stopPropagation(); evt.preventDefault(); return false; } }; var onMouseMove = function(evt) { if (arcDragOrigin) { if (arcDragJustStarted) { arcDragArc.setAttribute('visibility', 'visible'); // show the possible targets var span = data.spans[arcDragOrigin] || {}; var spanDesc = spanTypes[span.type] || {}; // separate out possible numeric suffix from type for highight // (instead of e.g. "Theme3", need to look for "Theme") var noNumArcType = stripNumericSuffix(arcOptions && arcOptions.type); var targetTypes = []; $.each(spanDesc.arcs || [], function(possibleArcNo, possibleArc) { if ((arcOptions && possibleArc.type == noNumArcType) || !(arcOptions && arcOptions.old_target)) { $.each(possibleArc.targets || [], function(possibleTargetNo, possibleTarget) { targetTypes.push(possibleTarget); }); } }); arcTargets = []; arcTargetRects = []; $.each(data.spans, function(spanNo, span) { if (span.id == arcDragOrigin) return; if (targetTypes.indexOf(span.type) != -1) { arcTargets.push(span.id); $.each(span.fragments, function(fragmentNo, fragment) { arcTargetRects.push(fragment.rect); }); } }); $(arcTargetRects).addClass('reselectTarget'); } clearSelection(); var mx = evt.pageX - svgPosition.left; var my = evt.pageY - svgPosition.top + 5; // TODO FIXME why +5?!? var y = Math.min(arcDragOriginBox.y, my) - draggedArcHeight; var dx = (arcDragOriginBox.center - mx) / 4; var path = svg.createPath(). move(arcDragOriginBox.center, arcDragOriginBox.y). curveC(arcDragOriginBox.center - dx, y, mx + dx, y, mx, my); arcDragArc.setAttribute('d', path.path()); } else { // A. Scerri FireFox chunk // if not, then is it span selection? (ctrl key cancels) var sel = window.getSelection(); var chunkIndexFrom = sel.anchorNode && $(sel.anchorNode.parentNode).attr('data-chunk-id'); var chunkIndexTo = sel.focusNode && $(sel.focusNode.parentNode).attr('data-chunk-id'); // fallback for firefox (at least): // it's unclear why, but for firefox the anchor and focus // node parents are always undefined, the the anchor and // focus nodes themselves do (often) have the necessary // chunk ID. However, anchor offsets are almost always // wrong, so we'll just make a guess at what the user might // be interested in tagging instead of using what's given. var anchorOffset = null; var focusOffset = null; if (chunkIndexFrom === undefined && chunkIndexTo === undefined && $(sel.anchorNode).attr('data-chunk-id') && $(sel.focusNode).attr('data-chunk-id')) { // Lets take the actual selection range and work with that // Note for visual line up and more accurate positions a vertical offset of 8 and horizontal of 2 has been used! var range = sel.getRangeAt(0); var svgOffset = $(svg._svg).offset(); var flip = false; var tries = 0; // First try and match the start offset with a position, if not try it against the other end while (tries < 2) { var sp = svg._svg.createSVGPoint(); sp.x = (flip ? evt.pageX : dragStartedAt.pageX) - svgOffset.left; sp.y = (flip ? evt.pageY : dragStartedAt.pageY) - (svgOffset.top + 8); var startsAt = range.startContainer; anchorOffset = startsAt.getCharNumAtPosition(sp); chunkIndexFrom = startsAt && $(startsAt).attr('data-chunk-id'); if (anchorOffset != -1) { break; } flip = true; tries++; } // Now grab the end offset sp.x = (flip ? dragStartedAt.pageX : evt.pageX) - svgOffset.left; sp.y = (flip ? dragStartedAt.pageY : evt.pageY) - (svgOffset.top + 8); var endsAt = range.endContainer; focusOffset = endsAt.getCharNumAtPosition(sp); // If we cannot get a start and end offset stop here if (anchorOffset == -1 || focusOffset == -1) { return; } // If we are in the same container it does the selection back to front when dragged right to left, across different containers the start is the start and the end if the end! if(range.startContainer == range.endContainer && anchorOffset > focusOffset) { var t = anchorOffset; anchorOffset = focusOffset; focusOffset = t; flip = false; } chunkIndexTo = endsAt && $(endsAt).attr('data-chunk-id'); // Now take the start and end character rectangles startRec = startsAt.getExtentOfChar(anchorOffset); startRec.y += 2; endRec = endsAt.getExtentOfChar(focusOffset); endRec.y += 2; // If nothing has changed then stop here if (lastStartRec != null && lastStartRec.x == startRec.x && lastStartRec.y == startRec.y && lastEndRec != null && lastEndRec.x == endRec.x && lastEndRec.y == endRec.y) { return; } if (selRect == null) { var rx = startRec.x; var ry = startRec.y; var rw = (endRec.x + endRec.width) - startRec.x; if (rw < 0) { rx += rw; rw = -rw; } var rh = Math.max(startRec.height, endRec.height); selRect = new Array(); var activeSelRect = makeSelRect(rx, ry, rw, rh); selRect.push(activeSelRect); startsAt.parentNode.parentNode.parentNode.insertBefore(activeSelRect, startsAt.parentNode.parentNode); } else { if (startRec.x != lastStartRec.x && endRec.x != lastEndRec.x && (startRec.y != lastStartRec.y || endRec.y != lastEndRec.y)) { if (startRec.y < lastStartRec.y) { selRect[0].setAttributeNS(null, "width", lastStartRec.width); lastEndRec = lastStartRec; } else if (endRec.y > lastEndRec.y) { selRect[selRect.length - 1].setAttributeNS(null, "x", parseFloat(selRect[selRect.length - 1].getAttributeNS(null, "x")) + parseFloat(selRect[selRect.length - 1].getAttributeNS(null, "width")) - lastEndRec.width); selRect[selRect.length - 1].setAttributeNS(null, "width", 0); lastStartRec=lastEndRec; } } // Start has moved var flip = !(startRec.x == lastStartRec.x && startRec.y == lastStartRec.y); // If the height of the start or end changed we need to check whether // to remove multi line highlights no longer needed if the user went back towards their start line // and whether to create new ones if we moved to a newline if (((endRec.y != lastEndRec.y)) || ((startRec.y != lastStartRec.y))) { // First check if we have to remove the first highlights because we are moving towards the end on a different line var ss = 0; for (; ss != selRect.length; ss++) { if (startRec.y <= parseFloat(selRect[ss].getAttributeNS(null, "y"))) { break; } } // Next check for any end highlights if we are moving towards the start on a different line var es = selRect.length - 1; for (; es != -1; es--) { if (endRec.y >= parseFloat(selRect[es].getAttributeNS(null, "y"))) { break; } } // TODO put this in loops above, for efficiency the array slicing could be done separate still in single call var trunc = false; if (ss < selRect.length) { for (var s2 = 0; s2 != ss; s2++) { selRect[s2].parentNode.removeChild(selRect[s2]); es--; trunc = true; } selRect = selRect.slice(ss); } if (es > -1) { for (var s2 = selRect.length - 1; s2 != es; s2--) { selRect[s2].parentNode.removeChild(selRect[s2]); trunc = true; } selRect = selRect.slice(0, es + 1); } // If we have truncated the highlights we need to readjust the last one if (trunc) { var activeSelRect = flip ? selRect[0] : selRect[selRect.length - 1]; if (flip) { var rw = 0; if (startRec.y == endRec.y) { rw = (endRec.x + endRec.width) - startRec.x; } else { rw = (parseFloat(activeSelRect.getAttributeNS(null, "x")) + parseFloat(activeSelRect.getAttributeNS(null, "width"))) - startRec.x; } activeSelRect.setAttributeNS(null, "x", startRec.x); activeSelRect.setAttributeNS(null, "y", startRec.y); activeSelRect.setAttributeNS(null, "width", rw); } else { var rw = (endRec.x + endRec.width) - parseFloat(activeSelRect.getAttributeNS(null, "x")); activeSelRect.setAttributeNS(null, "width", rw); } } else { // We didnt truncate anything but we have moved to a new line so we need to create a new highlight var lastSel = flip ? selRect[0] : selRect[selRect.length - 1]; var startBox = startsAt.parentNode.getBBox(); var endBox = endsAt.parentNode.getBBox(); if (flip) { lastSel.setAttributeNS(null, "width", (parseFloat(lastSel.getAttributeNS(null, "x")) + parseFloat(lastSel.getAttributeNS(null, "width"))) - endBox.x); lastSel.setAttributeNS(null, "x", endBox.x); } else { lastSel.setAttributeNS(null, "width", (startBox.x + startBox.width) - parseFloat(lastSel.getAttributeNS(null, "x"))); } var rx = 0; var ry = 0; var rw = 0; var rh = 0; if (flip) { rx = startRec.x; ry = startRec.y; rw = $(svg._svg).width() - startRec.x; rh = startRec.height; } else { rx = endBox.x; ry = endRec.y; rw = (endRec.x + endRec.width) - endBox.x; rh = endRec.height; } var newRect = makeSelRect(rx, ry, rw, rh); if (flip) { selRect.unshift(newRect); } else { selRect.push(newRect); } // Place new highlight in appropriate slot in SVG graph startsAt.parentNode.parentNode.parentNode.insertBefore(newRect, startsAt.parentNode.parentNode); } } else { // The user simply moved left or right along the same line so just adjust the current highlight var activeSelRect = flip ? selRect[0] : selRect[selRect.length - 1]; // If the start moved shift the highlight and adjust width if (flip) { var rw = (parseFloat(activeSelRect.getAttributeNS(null, "x")) + parseFloat(activeSelRect.getAttributeNS(null, "width"))) - startRec.x; activeSelRect.setAttributeNS(null, "x", startRec.x); activeSelRect.setAttributeNS(null, "y", startRec.y); activeSelRect.setAttributeNS(null, "width", rw); } else { // If the end moved then simple change the width var rw = (endRec.x + endRec.width) - parseFloat(activeSelRect.getAttributeNS(null, "x")); activeSelRect.setAttributeNS(null, "width", rw); } } } lastStartRec = startRec; lastEndRec = endRec; } } arcDragJustStarted = false; }; var adjustToCursor = function(evt, element, centerX, centerY) { var screenHeight = $(window).height() - 8; // TODO HACK - no idea why -8 is needed var screenWidth = $(window).width() - 8; var elementHeight = element.height(); var elementWidth = element.width(); var cssSettings = {}; var eLeft; var eTop; if (centerX) { eLeft = evt.clientX - elementWidth/2; } else { eLeft = evt.clientX; } if (centerY) { eTop = evt.clientY - elementHeight/2; } else { eTop = evt.clientY; } // Try to make sure the element doesn't go off-screen. // If this isn't possible (the element is larger than the screen), // alight top-left corner of screen and dialog as a compromise. if (screenWidth > elementWidth) { eLeft = Math.min(Math.max(eLeft,0), screenWidth - elementWidth); } else { eLeft = 0; } if (screenHeight > elementHeight) { eTop = Math.min(Math.max(eTop,0), screenHeight - elementHeight); } else { eTop = 0; } element.css({ top: eTop, left: eLeft }); }; var updateCheckbox = function($input) { var $widget = $input.button('widget'); var $textspan = $widget.find('.ui-button-text'); $textspan.html(($input[0].checked ? '&#x2611; ' : '&#x2610; ') + $widget.attr('data-bare')); }; var fillSpanTypesAndDisplayForm = function(evt, spanText, span) { keymap = spanKeymap; // Figure out whether we should show or hide one of the two // main halves of the selection frame (entities / events). // This depends on the type of the current span, if any, and // the availability of types to select. var hideFrame; if (span) { // existing span; only show relevant half if (span.generalType == 'entity') { hideFrame = 'event'; } else { hideFrame = 'entity'; } spanForm.dialog('option', { title: 'Edit Annotation' }); } else { // new span; show everything that's available if ($('#event_types').find('input').length == 0) { hideFrame = 'event'; } else if ($('#entity_types').find('input').length == 0) { hideFrame = 'entity'; } else { hideFrame = 'none'; } spanForm.dialog('option', { title: 'New Annotation' }); } if (hideFrame == 'event') { $('#span_event_section').hide() $('#span_entity_section').show(). removeClass('wrapper_half_left'). addClass('wrapper_full_width'); } else if (hideFrame == 'entity') { $('#span_entity_section').hide() $('#span_event_section').show(). removeClass('wrapper_half_right'). addClass('wrapper_full_width'); } else { // show both entity and event halves $('#span_entity_section').show(). removeClass('wrapper_full_width'). addClass('wrapper_half_left'); $('#span_event_section').show(). removeClass('wrapper_full_width'). addClass('wrapper_half_right'); } // only show "delete" button if there's an existing annotation to delete if (span) { $('#del_span_button').show(); } else { $('#del_span_button').hide(); } $('#span_selected').text(spanText); var encodedText = encodeURIComponent(spanText); $.each(searchConfig, function(searchNo, search) { $('#span_'+search[0]).attr('href', search[1].replace('%s', encodedText)); }); // enable all inputs by default (see setSpanTypeSelectability) $('#span_form input:not([unused])').removeAttr('disabled'); // close span types if there's over typeCollapseLimit if ($('#entity_types .item').length > Configuration.typeCollapseLimit) { $('#entity_types .open').removeClass('open'); } if ($('#event_types .item').length > Configuration.typeCollapseLimit) { $('#event_types .open').removeClass('open'); } var showAllAttributes = false; if (span) { var linkHash = new URLHash(coll, doc, { focus: [[span.id]] }).getHash(); var el = $('#span_' + span.type); if (el.length) { el[0].checked = true; } else { $('#span_form input:radio:checked').each(function (radioNo, radio) { radio.checked = false; }); } // open the span type $('#span_' + span.type).parents('.collapsible').each(function() { toggleCollapsible($(this).parent().prev(), true); }); // count the repeating arc types var arcTypeCount = {}; repeatingArcTypes = []; $.each(span.outgoing, function(arcNo, arc) { // parse out possible number suffixes to allow e.g. splitting // on "Theme" for args ("Theme1", "Theme2"). var splitArcType = arc.type.match(/^(.*?)(\d*)$/); var noNumArcType = splitArcType[1]; if ((arcTypeCount[noNumArcType] = (arcTypeCount[noNumArcType] || 0) + 1) == 2) { repeatingArcTypes.push(noNumArcType); } }); if (repeatingArcTypes.length) { $('#span_form_split').show(); } else { $('#span_form_split').hide(); } } else { var offsets = spanOptions.offsets[0]; var linkHash = new URLHash(coll, doc, { focus: [[offsets[0], offsets[1]]] }).getHash(); var firstRadio = $('#span_form input:radio:not([unused]):first')[0]; if (firstRadio) { firstRadio.checked = true; } else { dispatcher.post('hideForm'); dispatcher.post('messages', [[['No valid span types defined', 'error']]]); return; } $('#span_form_split').hide(); $('#span_notes').val(''); showAllAttributes = true; } $('#span_highlight_link').attr('href', linkHash); if (span && !reselectedSpan) { $('#span_form_reselect, #span_form_delete, #span_form_add_fragment').show(); keymap[$.ui.keyCode.DELETE] = 'span_form_delete'; keymap[$.ui.keyCode.INSERT] = 'span_form_reselect'; keymap['S-' + $.ui.keyCode.ENTER] = 'span_form_add_fragment'; $('#span_notes').val(span.annotatorNotes || ''); } else { $('#span_form_reselect, #span_form_delete, #span_form_add_fragment').hide(); keymap[$.ui.keyCode.DELETE] = null; keymap[$.ui.keyCode.INSERT] = null; keymap['S-' + $.ui.keyCode.ENTER] = null; } if (span && !reselectedSpan && span.offsets.length > 1) { $('#span_form_reselect_fragment, #span_form_delete_fragment').show(); keymap['S-' + $.ui.keyCode.DELETE] = 'span_form_delete_fragment'; keymap['S-' + $.ui.keyCode.INSERT] = 'span_form_reselect_fragment'; } else { $('#span_form_reselect_fragment, #span_form_delete_fragment').hide(); keymap['S-' + $.ui.keyCode.DELETE] = null; keymap['S-' + $.ui.keyCode.INSERT] = null; } // TODO: lots of redundancy in the next two blocks, clean up if (!span) { // no existing annotation, reset attributes var attrCategoryAndTypes = [['entity', entityAttributeTypes], ['event', eventAttributeTypes]]; $.each(attrCategoryAndTypes, function(ctNo, ct) { var category = ct[0]; var attributeTypes = ct[1]; $.each(attributeTypes, function(attrNo, attr) { $input = $('#'+category+'_attr_'+Util.escapeQuotes(attr.type)); if (attr.unused) { $input.val(''); } else if (attr.default) { if (attr.bool) { // take any non-empty default value as "true" $input[0].checked = true; updateCheckbox($input); $input.button('refresh'); } else { $input.val(attr.default).change(); } } else if (attr.bool) { $input[0].checked = false; updateCheckbox($input); $input.button('refresh'); } else { $input.val('').change(); } }); }); } else if (!reselectedSpan) { // existing annotation, fill attribute values from span var attributeTypes; var category; if (span.generalType == 'entity') { attributeTypes = entityAttributeTypes; category = 'entity'; } else if (span.generalType == 'trigger') { attributeTypes = eventAttributeTypes; // TODO: unify category/generalType values ('trigger' vs. 'event') category = 'event'; } else { console.error('Unrecognized generalType:', span.generalType); } $.each(attributeTypes, function(attrNo, attr) { $input = $('#'+category+'_attr_'+Util.escapeQuotes(attr.type)); var val = span.attributes[attr.type]; if (attr.unused) { $input.val(val || ''); } else if (attr.bool) { $input[0].checked = val; updateCheckbox($input); $input.button('refresh'); } else { $input.val(val || '').change(); } }); } var showValidNormalizationsFor = function(type) { // set DB selector to the first appropriate for the type. // TODO: actually disable inappropriate ones. // TODO: support specific IDs, not just DB specifiers var firstDb = type && normDbsByType[type] ? normDbsByType[type][0] : null; if (firstDb) { $('#span_norm_db').val(firstDb); } } showValidNormalizations = function() { // set norm DB selector according to the first selected type var firstSelected = $('#entity_and_event_wrapper input:radio:checked')[0]; var selectedType = firstSelected ? firstSelected.value : null; showValidNormalizationsFor(selectedType); } // fill normalizations (if any) if (!reselectedSpan) { // clear first clearNormalizationUI(); var $normDb = $('#span_norm_db'); var $normId = $('#span_norm_id'); var $normText = $('#span_norm_txt'); // fill if found (NOTE: only shows last on multiple) var normFilled = false; $.each(span ? span.normalizations : [], function(normNo, norm) { var refDb = norm[0], refId = norm[1], refText = norm[2]; $normDb.val(refDb); // could the DB selector be set? (i.e. is refDb configured?) if ($normDb.val() == refDb) { // DB is OK, set the rest also $normId.val(refId); oldSpanNormIdValue = refId; $normText.val(refText); // TODO: check if ID is valid $normId.addClass('valid_value') normFilled = true; } else { // can't set the DB selector; assume DB is not configured, // warn and leave blank (will remove norm when dialog is OK'd) dispatcher.post('messages', [[['Warning: '+refDb+' not configured, removing normalization.', 'warning']]]); } }); // if there is no existing normalization, show valid ones if (!normFilled) { showValidNormalizations(); } // update links updateNormalizationRefLink(); updateNormalizationDbLink(); } var showAttributesFor = function(attrTypes, category, type) { var validAttrs = type ? spanTypes[type].attributes : []; var shownCount = 0; $.each(attrTypes, function(attrNo, attr) { var $input = $('#'+category+'_attr_'+Util.escapeQuotes(attr.type)); var showAttr = showAllAttributes || $.inArray(attr.type, validAttrs) != -1; if (showAttr) { // $input.button('widget').parent().show(); $input.closest('.attribute_type_label').show(); shownCount++; } else { // $input.button('widget').parent().hide(); $input.closest('.attribute_type_label').hide(); } }); return shownCount; } showValidAttributes = function() { var type = $('#span_form input:radio:checked').val(); showAllAttributes = false; var entityAttrCount = showAttributesFor(entityAttributeTypes, 'entity', type); var eventAttrCount = showAttributesFor(eventAttributeTypes, 'event', type); // show attribute frames only if at least one attribute is // shown, and set size classes appropriately if (eventAttrCount > 0) { $('#event_attributes').show(); $('#event_attribute_label').show(); $('#event_types'). removeClass('scroll_wrapper_full'). addClass('scroll_wrapper_upper'); } else { $('#event_attributes').hide(); $('#event_attribute_label').hide(); $('#event_types'). removeClass('scroll_wrapper_upper'). addClass('scroll_wrapper_full'); } if (entityAttrCount > 0) { $('#entity_attributes').show(); $('#entity_attribute_label').show(); $('#entity_types'). removeClass('scroll_wrapper_full'). addClass('scroll_wrapper_upper'); } else { $('#entity_attributes').hide(); $('#entity_attribute_label').hide(); $('#entity_types'). removeClass('scroll_wrapper_upper'). addClass('scroll_wrapper_full'); } } showValidAttributes(); // TODO XXX: if seemed quite unexpected/unintuitive that the // form was re-displayed while the document still shows the // annotation in its old location in the background (check it). // The fix of skipping confirm is not really good either, though. if (reselectedSpan) { // && !Configuration.confirmModeOn) { submitReselect(); } else { dispatcher.post('showForm', [spanForm, true]); $('#span_form-ok').focus(); adjustToCursor(evt, spanForm.parent()); } }; var submitReselect = function() { $(reselectedSpan.rect).removeClass('reselect'); reselectedSpan = null; spanForm.submit(); }; var rapidFillSpanTypesAndDisplayForm = function(start, end, text, types) { // variant of fillSpanTypesAndDisplayForm for rapid annotation mode keymap = spanKeymap; $('#rapid_span_selected').text(text); // fill types var $spanTypeDiv = $('#rapid_span_types_div'); // remove previously filled, if any $spanTypeDiv.empty(); $.each(types, function(typeNo, typeAndProb) { // TODO: this duplicates a part of addSpanTypesToDivInner, unify var type = typeAndProb[0]; var prob = typeAndProb[1]; var $numlabel = $('<span class="accesskey">'+(typeNo+1)+'</span><span>:</span>'); var $input = $('<input type="radio" name="rapid_span_type"/>'). attr('id', 'rapid_span_' + (typeNo+1)). attr('value', type); var spanBgColor = spanTypes[type] && spanTypes[type].bgColor || '#ffffff'; spanBgColor = Util.adjustColorLightness(spanBgColor, spanBoxTextBgColorLighten); // use preferred label instead of type name if available var name = spanTypes[type] && spanTypes[type].name || type; var $label = $('<label class="span_type_label"/>'). attr('for', 'rapid_span_' + (typeNo+1)). text(name+' (' + (100.0 * prob).toFixed(1) + '%)'); $label.css('background-color', spanBgColor); // TODO: check for unnecessary extra wrapping here var $content = $('<div class="item_content"/>'). append($numlabel). append($input). append($label); $spanTypeDiv.append($content); // highlight configured hotkey (if any) in text. // NOTE: this bit doesn't actually set up the hotkey. var hotkeyType = 'span_' + type; // TODO: this is clumsy; there should be a better way var typeHotkey = null; $.each(keymap, function(key, keyType) { if (keyType == hotkeyType) { typeHotkey = key; return false; } }); if (typeHotkey) { var name = $label.html(); var replace = true; name = name.replace(new RegExp("(&[^;]*?)?(" + typeHotkey + ")", 'gi'), function(all, entity, letter) { if (replace && !entity) { replace = false; var hotkey = typeHotkey.toLowerCase() == letter ? typeHotkey.toLowerCase() : typeHotkey.toUpperCase(); return '<span class="accesskey">' + Util.escapeHTML(hotkey) + '</span>'; } return all; }); $label.html(name); } // Limit the number of suggestions to the number of numeric keys if (typeNo >= 8) { return false; } }); // fill in some space and the special "Other" option, with key "0" (zero) $spanTypeDiv.append($('<div class="item_content">&#160;</div>')); // non-breaking space var $numlabel = $('<span class="accesskey">0</span><span>:</span>'); var $input = $('<input type="radio" name="rapid_span_type" id="rapid_span_0" value=""/>'); var $label = $('<label class="span_type_label" for="rapid_span_0" style="background-color:lightgray">Other...</label>'); var $content = $('<div class="item_content"/>'). append($numlabel). append($input). append($label); $spanTypeDiv.append($content); // set up click event handlers rapidSpanForm.find('#rapid_span_types input:radio').click(rapidSpanFormSubmitRadio); var firstRadio = $('#rapid_span_form input:radio:first')[0]; if (firstRadio) { firstRadio.checked = true; } else { dispatcher.post('hideForm'); dispatcher.post('messages', [[['No valid span types defined', 'error']]]); return; } dispatcher.post('showForm', [rapidSpanForm]); rapidAnnotationDialogVisible = true; $('#rapid_span_form-ok').focus(); // TODO: avoid using global for stored click event // adjustToCursor(lastRapidAnnotationEvent, rapidSpanForm.parent(), // true, true); // TODO: avoid coordinate hack to position roughly at first // available selection lastRapidAnnotationEvent.clientX -= 55; lastRapidAnnotationEvent.clientY -= 115; adjustToCursor(lastRapidAnnotationEvent, rapidSpanForm.parent(), false, false); }; var clearArcNotes = function(evt) { $('#arc_notes').val(''); } $('#clear_arc_notes_button').button(); $('#clear_arc_notes_button').click(clearArcNotes); var clearSpanNotes = function(evt) { $('#span_notes').val(''); } $('#clear_span_notes_button').button(); $('#clear_span_notes_button').click(clearSpanNotes); var clearSpanNorm = function(evt) { clearNormalizationUI(); } $('#clear_norm_button').button(); $('#clear_norm_button').click(clearSpanNorm); // invoked on response to ajax request for id lookup var setSpanNormText = function(response) { if (response.exception) { // TODO: better response to failure dispatcher.post('messages', [[['Lookup error', 'warning', -1]]]); return false; } // set input style according to whether we have a valid value var $idinput = $('#span_norm_id'); // TODO: make sure the key echo in the response matches the // current value of the $idinput $idinput.removeClass('valid_value').removeClass('invalid_value'); if (response.value === null) { $idinput.addClass('invalid_value'); hideNormalizationRefLink(); } else { $idinput.addClass('valid_value'); updateNormalizationRefLink(); } $('#span_norm_txt').val(response.value); } // on any change to the normalization DB, clear everything and // update link var spanNormDbUpdate = function(evt) { clearNormalizationUI(); updateNormalizationDbLink(); } $('#span_norm_db').change(spanNormDbUpdate); // on any change to the normalization ID, update the text of the // reference var spanNormIdUpdate = function(evt) { var key = $(this).val(); var db = $('#span_norm_db').val(); if (key != oldSpanNormIdValue) { if (key.match(/^\s*$/)) { // don't query empties, just clear instead clearNormalizationUI(); } else { dispatcher.post('ajax', [ { action: 'normGetName', database: db, key: key, collection: coll}, 'normGetNameResult']); } oldSpanNormIdValue = key; } } // see http://stackoverflow.com/questions/1948332/detect-all-changes-to-a-input-type-text-immediately-using-jquery $('#span_norm_id').bind('propertychange keyup input paste', spanNormIdUpdate); // nice-looking select for normalization $('#span_norm_db').addClass('ui-widget ui-state-default ui-button-text'); var normSearchDialog = $('#norm_search_dialog'); initForm(normSearchDialog, { width: 800, width: 600, resizable: true, alsoResize: '#norm_search_result_select', open: function(evt) { keymap = {}; }, close: function(evt) { // assume that we always want to return to the span dialog // on normalization dialog close dispatcher.post('showForm', [spanForm, true]); }, }); $('#norm_search_query').autocomplete({ source: function(request, callback) { var query = $.ui.autocomplete.escapeRegex(request.term); var pattern = new RegExp('\\b' + query, 'i'); callback($.grep(lastNormSearches, function(search) { return pattern.test(search.value) || pattern.test(search.id); })); }, minLength: 0, select: function(evt, ui) { evt.stopPropagation(); normSubmit(ui.item.id, ui.item.value); }, focus: function(evt, ui) { // do nothing }, }).autocomplete('instance')._renderItem = function($ul, item) { // XXX TODO TEST return $('<li></li>'). data('item.autocomplete', item). append('<a>' + Util.escapeHTML(item.value) + '<div class="autocomplete-id">' + Util.escapeHTML(item.id) + "</div></a>"). appendTo($ul); }; var normSubmit = function(selectedId, selectedTxt) { // we got a value; act if it was a submit $('#span_norm_id').val(selectedId); // don't forget to update this reference value oldSpanNormIdValue = selectedId; $('#span_norm_txt').val(selectedTxt); updateNormalizationRefLink(); // update history var nextLastNormSearches = [ { value: selectedTxt, id: selectedId, }, ]; $.each(lastNormSearches, function(searchNo, search) { if (search.id != selectedId || search.value != selectedTxt) { nextLastNormSearches.push(search); } }); lastNormSearches = nextLastNormSearches; lastNormSearches.slice(0, maxNormSearchHistory); // Switch dialogs. NOTE: assuming we closed the spanForm when // bringing up the normSearchDialog. normSearchDialog.dialog('close'); }; var normSearchSubmit = function(evt) { if (normSearchSubmittable) { var selectedId = $('#norm_search_id').val(); var selectedTxt = $('#norm_search_query').val(); normSubmit(selectedId, selectedTxt); } else { performNormSearch(); } return false; } var normSearchSubmittable = false; var setNormSearchSubmit = function(enable) { $('#norm_search_dialog-ok').button(enable ? 'enable' : 'disable'); normSearchSubmittable = enable; }; normSearchDialog.submit(normSearchSubmit); var chooseNormId = function(evt) { var $element = $(evt.target).closest('tr'); $('#norm_search_result_select tr').removeClass