diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureEditor.html b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureEditor.html
index 25455de48b2..63eefaae17e 100644
--- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureEditor.html
+++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureEditor.html
@@ -31,6 +31,11 @@
+
diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureEditor.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureEditor.java
index d5409cdd88a..f87d20b2403 100644
--- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureEditor.java
+++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureEditor.java
@@ -21,32 +21,48 @@
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
+import static org.apache.wicket.event.Broadcast.BUBBLE;
import static org.apache.wicket.markup.head.JavaScriptHeaderItem.forReference;
import java.util.Collections;
import java.util.List;
+import java.util.Locale;
+import org.apache.commons.lang3.StringUtils;
import org.apache.uima.cas.CAS;
import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
+import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
+import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
import org.apache.wicket.feedback.IFeedback;
import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.model.CompoundPropertyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
+import org.apache.wicket.request.IRequestParameters;
import org.apache.wicket.request.cycle.RequestCycle;
+import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.spring.injection.annot.SpringBean;
+import org.apache.wicket.util.convert.IConverter;
+import org.apache.wicket.util.string.StringValue;
+import org.danekja.java.util.function.serializable.SerializableFunction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.googlecode.wicket.jquery.core.JQueryBehavior;
+
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler;
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.FeatureSupport;
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.FeatureSupportRegistry;
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.editor.FeatureEditor;
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.editor.KendoChoiceDescriptionScriptReference;
+import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.event.FeatureEditorValueChangedEvent;
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.keybindings.KeyBindingsPanel;
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState;
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.FeatureState;
@@ -67,7 +83,8 @@ public class ConceptFeatureEditor
private static final long serialVersionUID = 7763348613632105600L;
- private FormComponent focusComponent;
+ private AutoCompleteField focusComponent;
+ private Label description;
private IriInfoBadge iriBadge;
private ExternalLink openIriLink;
@@ -92,7 +109,7 @@ public ConceptFeatureEditor(String aId, MarkupContainer aItem, IModel
+ add(focusComponent = new AutoCompleteField(MID_VALUE, _query ->
getCandidates(aStateModel, aHandler, _query)));
AnnotationFeature feat = getModelObject().feature;
@@ -103,6 +120,11 @@ public ConceptFeatureEditor(String aId, MarkupContainer aItem, IModel getLabelComponent().isVisible())));
+
+ description = new Label("description", LoadableDetachableModel.of(this::descriptionValue));
+ description.setOutputMarkupPlaceholderTag(true);
+ description.add(visibleWhen(() -> getLabelComponent().isVisible()));
+ add(description);
}
@Override
@@ -112,7 +134,17 @@ public void renderHead(IHeaderResponse aResponse)
aResponse.render(forReference(KendoChoiceDescriptionScriptReference.get()));
}
-
+
+ private String descriptionValue()
+ {
+ return getModel().map(FeatureState::getValue)
+ .map(value -> (KBHandle) value)
+ .map(KBHandle::getDescription)
+ .map(value -> StringUtils.abbreviate(value, 130))
+ .orElse("no description")
+ .getObject();
+ }
+
private String iriTooltipValue()
{
return getModel().map(FeatureState::getValue)
@@ -174,10 +206,191 @@ private ConceptFeatureTraits readFeatureTraits(AnnotationFeature aAnnotationFeat
ConceptFeatureTraits traits = fs.readTraits(aAnnotationFeature);
return traits;
}
+
+ @Override
+ public void addFeatureUpdateBehavior()
+ {
+ focusComponent.add(new AjaxFormComponentUpdatingBehavior("change")
+ {
+ private static final long serialVersionUID = -8944946839865527412L;
+
+ @Override
+ protected void updateAjaxAttributes(AjaxRequestAttributes aAttributes)
+ {
+ super.updateAjaxAttributes(aAttributes);
+ aAttributes.getDynamicExtraParameters()
+ .add(focusComponent.getIdentifierDynamicAttributeScript());
+ addDelay(aAttributes, 250);
+ }
+
+ @Override
+ protected void onUpdate(AjaxRequestTarget aTarget)
+ {
+ aTarget.add(description);
+ send(focusComponent, BUBBLE,
+ new FeatureEditorValueChangedEvent(ConceptFeatureEditor.this, aTarget));
+ }
+ });
+ }
@Override
public FormComponent getFocusComponent()
{
return focusComponent;
}
+
+ /**
+ * Special version of the {@link KnowledgeBaseItemAutoCompleteField} for used in the concept
+ * feature editor.
+ */
+ public static class AutoCompleteField
+ extends KnowledgeBaseItemAutoCompleteField
+ {
+ private static final long serialVersionUID = 5461442869971269291L;
+
+ private IConverter converter;
+ private List choiceCache;
+ private boolean allowChoiceCache = false;
+
+ public AutoCompleteField(String aId,
+ SerializableFunction> aChoiceProvider)
+ {
+ super(aId, aChoiceProvider);
+ converter = newConverter();
+ }
+
+ @Override
+ public void onConfigure(JQueryBehavior aBehavior)
+ {
+ super.onConfigure(aBehavior);
+
+ // We need to explicitly trigger the change event on the input element in order to
+ // trigger the Wicket AJAX update (if there is one). If we do not do this, then Kendo
+ // will "forget" to trigger a change event if the label of the newly selected item is
+ // the same as the label of the previously selected item!!!
+ // Using the default select behavior of AutoCompleteTextField which is coupled to the
+ // onSelected(AjaxRequestTarget aTarget) callback does unfortunatle not work well
+ // because onSelected does not tell us when the auto-complete field is CLEARED!
+ aBehavior.setOption("select", String.join(" ",
+ "function (e) {",
+ " e.sender.element.trigger('change');",
+ "}"));
+ }
+
+ @Override
+ protected List getChoices(String aInput)
+ {
+ if (!allowChoiceCache || choiceCache == null) {
+ choiceCache = super.getChoices(aInput);
+ }
+ return choiceCache;
+ }
+
+ @Override
+ public String[] getInputAsArray()
+ {
+ // If the web request includes the additional "identifier" parameter which is supposed
+ // to contain the IRI of the selected item instead of its label, then we use that as the
+ // value.
+ WebRequest request = getWebRequest();
+ IRequestParameters requestParameters = request.getRequestParameters();
+ StringValue identifier = requestParameters
+ .getParameterValue(getInputName() + ":identifier");
+
+ if (!identifier.isEmpty()) {
+ return new String[] { identifier.toString() };
+ }
+
+ return super.getInputAsArray();
+ }
+
+ /**
+ * When using this input component with an {@link AjaxFormChoiceComponentUpdatingBehavior},
+ * it is necessary to request the identifier of the selected item as an additional dynamic
+ * attribute, otherwise no distinction can be made between two items with the same label!
+ */
+ public String getIdentifierDynamicAttributeScript()
+ {
+ return String.join(" ",
+ "var item = $(attrs.event.target).data('kendoAutoComplete').dataItem();",
+ "if (item) {",
+ " return [{",
+ " 'name': '" + getInputName() + ":identifier', ",
+ " 'value': $(attrs.event.target).data('kendoAutoComplete').dataItem().identifier",
+ " }]",
+ "}",
+ "return [];");
+ }
+
+ @Override
+ public IConverter getConverter(Class aType)
+ {
+ if (aType != null && aType.isAssignableFrom(this.getType())) {
+ return (IConverter) converter;
+ }
+
+ return super.getConverter(aType);
+ }
+
+ private IConverter newConverter()
+ {
+ return new IConverter() {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public KBHandle convertToObject(String value, Locale locale)
+ {
+ if (value == null) {
+ return null;
+ }
+
+ if (value.equals(getModelValue())) {
+ return getModelObject();
+ }
+
+ // Check choices only here since fetching choices can take some time. If we
+ // already have choices from a previous query, then we use them instead of
+ // reloading all the choices. This avoids having to load the choices when
+ // opening the dropdown AND when selecting one of the items from it.
+ List choices;
+ try {
+ allowChoiceCache = true;
+ choices = getChoices(value);
+ }
+ finally {
+ allowChoiceCache = false;
+ }
+
+ if (choices.isEmpty()) {
+ return null;
+ }
+
+ // Check if we can find a match by the identifier. The identifier is unique
+ // while the same label may appear on multiple items
+ for (KBHandle handle : choices) {
+ if (value.equals(handle.getIdentifier())) {
+ return handle;
+ }
+ }
+
+// // Check labels if there was no match on the identifier
+// for (KBHandle handle : choices) {
+// if (value.equals(getRenderer().getText(handle))) {
+// return handle;
+// }
+// }
+
+ // If there was no match at all, return null
+ return null;
+ }
+
+ @Override
+ public String convertToString(KBHandle value, Locale locale)
+ {
+ return getRenderer().getText(value);
+ }
+ };
+ }
+ }
}
diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureSupport.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureSupport.java
index c79967ec7a3..a3e523d10e1 100644
--- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureSupport.java
+++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureSupport.java
@@ -138,11 +138,11 @@ public boolean accepts(AnnotationFeature aFeature)
}
@Override
- public String renderFeatureValue(AnnotationFeature aFeature, String aLabel)
+ public String renderFeatureValue(AnnotationFeature aFeature, String aIdentifier)
{
String renderValue = null;
- if (aLabel != null) {
- return labelCache.get(new Key(aFeature, aLabel)).getUiLabel();
+ if (aIdentifier != null) {
+ return labelCache.get(new Key(aFeature, aIdentifier)).getUiLabel();
}
return renderValue;
}
@@ -202,7 +202,10 @@ public KBHandle wrapFeatureValue(AnnotationFeature aFeature, CAS aCAS, Object aV
{
if (aValue instanceof String) {
String identifier = (String) aValue;
- return new KBHandle(identifier, renderFeatureValue(aFeature, identifier));
+ String label = renderFeatureValue(aFeature, identifier);
+ String description = labelCache.get(new Key(aFeature, identifier)).getDescription();
+
+ return new KBHandle(identifier, label, description);
}
else if (aValue instanceof KBHandle) {
return (KBHandle) aValue;
diff --git a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/KnowledgeBaseItemAutoCompleteField.java b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/KnowledgeBaseItemAutoCompleteField.java
index 8cd7d0e6f8b..7e58ac40a44 100644
--- a/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/KnowledgeBaseItemAutoCompleteField.java
+++ b/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/KnowledgeBaseItemAutoCompleteField.java
@@ -35,6 +35,9 @@
import de.tudarmstadt.ukp.inception.kb.graph.KBHandle;
+/**
+ * Auto-complete field for accessing a knowledge base.
+ */
public class KnowledgeBaseItemAutoCompleteField
extends AutoCompleteTextField
{
@@ -76,7 +79,7 @@ protected List getChoices(String aInput)
{
return choiceProvider.apply(aInput);
}
-
+
@Override
public void onConfigure(JQueryBehavior behavior)
{
@@ -94,6 +97,14 @@ public void onConfigure(JQueryBehavior behavior)
"function(e) {",
" e.sender.list.width(Math.max($(window).width()*0.3,300));",
"}"));
+
+ // Reset the values in the dropdown listbox to avoid that when opening the dropdown the next
+ // time ALL items with the same label as the selected item appear as selected
+ behavior.setOption("filtering", String.join(" ",
+ "function(e) {",
+ " e.sender.listView.value([]);",
+ "}"));
+
// Prevent scrolling action from closing the dropdown while the focus is on the input field
// Use one-third of the browser width but not less than 300 pixels. This is better than
// using the Kendo auto-sizing feature because that sometimes doesn't get the width right.
@@ -127,7 +138,7 @@ public String getText()
sb.append(" [${ data.rank }]");
sb.append(" ");
sb.append(" # } #");
- sb.append(" ${ data.name }");
+ sb.append(" ${ data.uiLabel }");
sb.append(" ");
sb.append(" ");
sb.append(" ${ data.identifier }");
@@ -148,7 +159,6 @@ public String getText()
public List getTextProperties()
{
List properties = new ArrayList<>();
- properties.add("name");
properties.add("identifier");
properties.add("description");
properties.add("rank");