UNPKG

malgo-brat-frontend-editor

Version:
1,111 lines (1,035 loc) 119 kB
/* * ## brat ## * Copyright (C) 2010-2012 The brat contributors, all rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ // -*- 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 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 allAttributeTypes = null; // TODO: temp workaround, remove var relationTypesHash = null; var showValidAttributes; // callback function var showValidNormalizations; // callback function var dragStartedAt = null; var selRect = null; var lastStartRec = null; var lastEndRec = null; var draggedArcHeight = 30; var spanTypesToShowBeforeCollapse = 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 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 } // WEBANNO EXTENSION BEGIN // We do not use the brat forms /* var hideForm = function() { keymap = null; rapidAnnotationDialogVisible = false; }; */ // WEBANNO EXTENSIONE END 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) { stopArcDrag(); 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 (!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 (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 //Renaud //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]; if (eventDesc.equiv) { arcOptions['left'] = eventDesc.leftSpans.join(','); arcOptions['right'] = eventDesc.rightSpans.join(','); } } $('#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]; // WEBANNO EXTENSION BEGIN fillArcTypesAndDisplayForm(evt, originSpanId, originSpan.type, targetSpanId, targetSpan.type, type, arcId); // WEBANNO EXTENSION END // 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'); var offsets = []; $.each(editedSpan.fragments, function(fragmentNo, fragment) { offsets.push([fragment.from, fragment.to]); }); spanOptions = { action: 'createSpan', offsets: offsets, type: editedSpan.type, id: id, }; // WEBANNO EXTENSION BEGIN fillSpanTypesAndDisplayForm(evt, offsets,editedSpan.text, editedSpan, id); // WEBANNO EXTENSION END // 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) { clearSelection(); svgElement.addClass('unselectable'); svgPosition = svgElement.offset(); arcDragOrigin = originId; arcDragArc = svg.path(svg.createPath(), { markerEnd: 'url(#drag_arrow)', 'class': 'drag_stroke', fill: 'none', }); arcDragOriginGroup = $(data.spans[arcDragOrigin].group); arcDragOriginGroup.addClass('highlight'); arcDragOriginBox = Util.realBBox(data.spans[arcDragOrigin].headFragment); arcDragOriginBox.center = arcDragOriginBox.x + arcDragOriginBox.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? //Renaud //if (!that.user || arcDragOrigin) return; if (arcDragOrigin) return; var target = $(evt.target); var id; // is it arc drag start? if (id = target.attr('data-span-id')) { arcOptions = null; startArcDrag(id); return false; } }; var onMouseMove = function(evt) { if (arcDragOrigin) { if (arcDragJustStarted) { // 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 targetClasses = []; var $targets = $(); $.each(spanDesc.arcs || [], function(possibleArcNo, possibleArc) { if ((arcOptions && possibleArc.type == noNumArcType) || !(arcOptions && arcOptions.old_target)) { $.each(possibleArc.targets || [], function(possibleTargetNo, possibleTarget) { // speedup for #642: relevant browsers should support // this function: http://www.quirksmode.org/dom/w3c_core.html#t11 // so we get off jQuery and get down to the metal: // targetClasses.push('.span_' + possibleTarget); $targets = $targets.add(svgElement[0].getElementsByClassName('span_' + possibleTarget)); }); } }); // $(targetClasses.join(',')).not('[data-span-id="' + arcDragOrigin + '"]').addClass('reselectTarget'); $targets.not('[data-span-id="' + arcDragOrigin + '"]').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')); }; // WEBANNO EXTENSION BEGIN /* 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 spanTypesToShowBeforeCollapse if ($('#entity_types .item').length > spanTypesToShowBeforeCollapse) { $('#entity_types .open').removeClass('open'); } if ($('#event_types .item').length > spanTypesToShowBeforeCollapse) { $('#event_types .open').removeClass('open'); } var showAllAttributes = false; if (span) { var hash = new URLHash(coll, doc, { focus: [[span.id]] }).getHash(); $('#span_highlight_link').attr('href', hash).show(); 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 { $('#span_highlight_link').hide(); 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; } 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.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').show(); shownCount++; } else { $input.button('widget').hide(); } }); return shownCount; } showValidAttributes = function() { var type = $('#span_form input:radio:checked').val(); var entityAttrCount = showAttributesFor(entityAttributeTypes, 'entity', type); var eventAttrCount = showAttributesFor(eventAttributeTypes, 'event', type); showAllAttributes = false; // 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]); $('#span_form-ok').focus(); adjustToCursor(evt, spanForm.parent()); } }; */ var fillSpanTypesAndDisplayForm =function(evt, offsets, spanText, span, id) { if(id) { dispatcher.post('ajax', [ { action: 'spanOpenDialog', offsets: $.toJSON(offsets), id:id, type: span.type, spanText: spanText }, 'serverResult']); } else{ dispatcher.post('ajax', [ { action: 'spanOpenDialog', offsets: $.toJSON(offsets), spanText: spanText }, 'serverResult']); } }; // WEBANNO EXTENSION END var submitReselect = function() { $(reselectedSpan.rect).removeClass('reselect'); reselectedSpan = null; spanForm.submit(); }; // WEBANNO EXTENSION BEGIN // We do not use the brat forms /* 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); }; */ // WEBANNO EXTENSION END var clearSpanNotes = function(evt) { $('#span_notes').val(''); } $('#clear_notes_button').button(); $('#clear_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