WebVOWL_work

Improve editing mode: confirmation dialogs, node info panel,

6/9/2026 3:28:48 PM

Changes

src/app/js/sidebar.js 102(+80 -22)

src/index.html 30(+29 -1)

Details

diff --git a/src/app/css/toolstyle.css b/src/app/css/toolstyle.css
index 2bddc81..f2cf2bc 100644
--- a/src/app/css/toolstyle.css
+++ b/src/app/css/toolstyle.css
@@ -1028,6 +1028,95 @@ main {
     padding: 4px 0;
 }
 
+/* ── Edit mode: annotation view panel ───────────────────────────────────── */
+.edit-annotations-panel {
+    margin-top: 8px;
+    margin-bottom: 4px;
+    border-top: 1px solid #1e2a35;
+    padding-top: 4px;
+}
+
+/* ── Edit mode: add annotation area ─────────────────────────────────────── */
+#add-annotation-area {
+    margin-top: 6px;
+    border-top: 1px solid #1e2a35;
+    padding-top: 6px;
+}
+
+label.edit-btn-annotation {
+    display: inline-block;
+    cursor: pointer;
+    font-size: 0.78em;
+    color: #8ab;
+    padding: 2px 6px;
+    border: 1px solid #2d4a5e;
+    border-radius: 3px;
+    user-select: none;
+}
+label.edit-btn-annotation:hover { color: #acd; border-color: #4a7a9b; }
+
+#add-annotation-form {
+    margin-top: 6px;
+}
+
+.add-annotation-field {
+    display: flex;
+    align-items: center;
+    margin-bottom: 4px;
+}
+
+.add-annotation-field .EditLabelForInput {
+    flex: 0 0 38%;
+    font-size: 0.82em;
+}
+
+.add-annotation-field input {
+    flex: 1;
+    background: #1a2530;
+    border: 1px solid #2d4a5e;
+    color: #ecf0f1;
+    font-size: 0.82em;
+    padding: 2px 4px;
+    border-radius: 2px;
+    min-width: 0;
+}
+
+.add-annotation-field input:focus {
+    border-color: #4a7a9b;
+    outline: none;
+}
+
+.add-annotation-actions {
+    display: flex;
+    gap: 6px;
+    margin-top: 4px;
+    justify-content: flex-end;
+}
+
+label.edit-btn-annotation-save {
+    cursor: pointer;
+    font-size: 0.78em;
+    padding: 2px 10px;
+    background: #1e4060;
+    color: #acd;
+    border: 1px solid #4a7a9b;
+    border-radius: 3px;
+    user-select: none;
+}
+label.edit-btn-annotation-save:hover { background: #2a5a80; }
+
+label.edit-btn-annotation-cancel {
+    cursor: pointer;
+    font-size: 0.78em;
+    padding: 2px 10px;
+    background: transparent;
+    color: #8ab;
+    border: 1px solid #2d4a5e;
+    border-radius: 3px;
+    user-select: none;
+}
+label.edit-btn-annotation-cancel:hover { color: #acd; border-color: #4a7a9b; }
+
 #generalDetails h4 {
     background: rgb(27, 37, 46);
     color: #ecf0f1;
diff --git a/src/app/js/editSidebar.js b/src/app/js/editSidebar.js
index 1593bc8..615d757 100644
--- a/src/app/js/editSidebar.js
+++ b/src/app/js/editSidebar.js
@@ -473,12 +473,23 @@ module.exports = function ( graph ){
   
   function deletePrefixLine(){
     if ( this.disabled === true ) return;
-    d3.select("#addPrefixButton").node().innerHTML = "Add Prefix";
     var selector = this.id.split("_")[1];
-    d3.select("#prefixContainerFor_" + selector).remove();
-    graph.options().removePrefix(selector);
-    prefix_editMode = false; // <<TODO make some sanity checks
-    prefixModule.updatePrefixModel();
+    var prefixUrl = "";
+    var urlNode = d3.select("#prefixURLFor_" + selector).node();
+    if ( urlNode ) prefixUrl = urlNode.value || urlNode.innerHTML || "";
+    var text = "Delete prefix \"" + selector + "\"" + (prefixUrl ? " → " + prefixUrl : "") + "?";
+    graph.options().warningModule().responseWarning(
+      "Delete prefix",
+      text,
+      "This action cannot be undone.",
+      function ( sel ){
+        d3.select("#addPrefixButton").node().innerHTML = "Add Prefix";
+        d3.select("#prefixContainerFor_" + sel).remove();
+        graph.options().removePrefix(sel);
+        prefix_editMode = false;
+        prefixModule.updatePrefixModel();
+      },
+      [selector], false);
   }
   
   function enablePrefixEdit( item ){
@@ -970,10 +981,156 @@ module.exports = function ( graph ){
       var fullURI = d3.select("#element_iriEditor").node().value;
       d3.select("#element_iriEditor").node().value = prefixModule.getPrefixRepresentationForFullURI(fullURI);
       d3.select("#element_iriEditor").node().title = fullURI;
+
+      // --- Read-only annotation view (mirrors non-editing mode) ---
+      var annPanel = d3.select("#edit-node-annotations-panel");
+      var sidebarModule = graph.options().sidebar();
+      if ( sidebarModule && sidebarModule.renderAnnotationsForElement ) {
+        sidebarModule.renderAnnotationsForElement(element, annPanel);
+      }
+
+      // --- Add Annotation button / form ---
+      d3.select("#add-annotation-form").classed("hidden", true);
+      d3.select("#ann-iri-input").node().value = "";
+      d3.select("#ann-value-input").node().value = "";
+      d3.select("#ann-lang-input").node().value = "";
+
+      d3.select("#add-annotation-btn").on("click", function (){
+        d3.select("#add-annotation-form").classed("hidden", false);
+        populateAnnotationDatalist(element);
+        d3.select("#ann-iri-input").node().focus();
+      });
+
+      d3.select("#ann-cancel-btn").on("click", function (){
+        d3.select("#add-annotation-form").classed("hidden", true);
+        d3.select("#ann-iri-input").node().value = "";
+        d3.select("#ann-value-input").node().value = "";
+        d3.select("#ann-lang-input").node().value = "";
+      });
+
+      d3.select("#ann-save-btn").on("click", function (){
+        saveAnnotationToElement(element);
+      });
+
+      d3.select("#ann-iri-input").on("keydown", function (){
+        d3.event.stopPropagation();
+        if ( d3.event.keyCode === 13 ) { d3.event.preventDefault(); d3.select("#ann-value-input").node().focus(); }
+      });
+      d3.select("#ann-value-input").on("keydown", function (){
+        d3.event.stopPropagation();
+        if ( d3.event.keyCode === 13 ) { d3.event.preventDefault(); saveAnnotationToElement(element); }
+      });
+      d3.select("#ann-lang-input").on("keydown", function (){
+        d3.event.stopPropagation();
+        if ( d3.event.keyCode === 13 ) { d3.event.preventDefault(); saveAnnotationToElement(element); }
+      });
+
       editSidebar.updateElementWidth();
     }
-    
+
   };
+
+  // ── Annotation helpers ────────────────────────────────────────────────────
+
+  function resolveAnnotationIRI( input ){
+    if ( !input ) return null;
+    input = input.trim();
+    if ( input.startsWith("http://") || input.startsWith("https://") ) return input;
+    var parts = input.split(":");
+    if ( parts.length === 2 && parts[0].length > 0 ) {
+      var prefList = graph.options().prefixList();
+      var base = prefList[parts[0]];
+      if ( base ) return base + parts[1];
+    }
+    // fall back: prepend ontology base IRI
+    var ontoBase = graph.options().getGeneralMetaObjectProperty("iri") || "";
+    return ontoBase + input;
+  }
+
+  function extractLocalName( iri ){
+    var hash = iri.lastIndexOf("#");
+    var slash = iri.lastIndexOf("/");
+    var cut = Math.max(hash, slash);
+    return cut !== -1 ? iri.substring(cut + 1) : iri;
+  }
+
+  function collectOntologyAnnotationIRIs(){
+    var seen = {};
+    var all = [];
+    function addIfNew( iri ){
+      if ( iri && !seen[iri] ) { seen[iri] = true; all.push(iri); }
+    }
+    // Standard annotations
+    addIfNew("http://www.w3.org/2000/01/rdf-schema#comment");
+    addIfNew("http://www.w3.org/2000/01/rdf-schema#label");
+    addIfNew("http://www.w3.org/2002/07/owl#deprecated");
+    addIfNew("http://purl.org/dc/elements/1.1/description");
+    addIfNew("http://purl.org/dc/terms/description");
+    // From nodes in the graph
+    var data = graph.getUnfilteredData();
+    if ( data ) {
+      var all_elements = (data.nodes || []).concat(data.properties || []);
+      all_elements.forEach(function ( el ){
+        var anns = el.annotations ? el.annotations() : null;
+        if ( anns ) {
+          for ( var key in anns ) {
+            if ( anns.hasOwnProperty(key) ) addIfNew(key);
+          }
+        }
+      });
+    }
+    return all;
+  }
+
+  function populateAnnotationDatalist( element ){
+    var dl = d3.select("#ann-iri-datalist");
+    dl.selectAll("option").remove();
+    var iris = collectOntologyAnnotationIRIs();
+    iris.forEach(function ( iri ){
+      var prefixed = prefixModule.getPrefixRepresentationForFullURI(iri);
+      dl.append("option").attr("value", prefixed || iri);
+    });
+  }
+
+  function saveAnnotationToElement( element ){
+    var iriInput = d3.select("#ann-iri-input").node().value.trim();
+    var value    = d3.select("#ann-value-input").node().value.trim();
+    var lang     = d3.select("#ann-lang-input").node().value.trim();
+    if ( !iriInput || !value ) {
+      graph.options().warningModule().showWarning(
+        "Missing fields",
+        "Both Annotation IRI and Value are required.",
+        "", 1, false);
+      return;
+    }
+    var fullIRI = resolveAnnotationIRI(iriInput);
+    if ( !fullIRI ) {
+      graph.options().warningModule().showWarning(
+        "Invalid IRI",
+        "Could not resolve annotation IRI: " + iriInput,
+        "", 1, false);
+      return;
+    }
+    var localName = extractLocalName(fullIRI);
+    var anns = element.annotations() || {};
+    // clone to avoid mutation surprises
+    var updated = {};
+    for ( var k in anns ) { if ( anns.hasOwnProperty(k) ) updated[k] = anns[k]; }
+    if ( !updated[fullIRI] ) updated[fullIRI] = [];
+    updated[fullIRI].push({ identifier: localName, value: value, type: "label", language: lang });
+    element.annotations(updated);
+
+    // reset form, re-render
+    d3.select("#add-annotation-form").classed("hidden", true);
+    d3.select("#ann-iri-input").node().value = "";
+    d3.select("#ann-value-input").node().value = "";
+    d3.select("#ann-lang-input").node().value = "";
+
+    var sidebarModule = graph.options().sidebar();
+    if ( sidebarModule && sidebarModule.renderAnnotationsForElement ) {
+      sidebarModule.renderAnnotationsForElement(element, d3.select("#edit-node-annotations-panel"));
+    }
+  }
   
   editSidebar.updateGeneralOntologyInfo = function (){
     var preferredLanguage = graph && graph.language ? graph.language() : null;

src/app/js/sidebar.js 102(+80 -22)

diff --git a/src/app/js/sidebar.js b/src/app/js/sidebar.js
index 4482bfe..4442c1d 100644
--- a/src/app/js/sidebar.js
+++ b/src/app/js/sidebar.js
@@ -324,6 +324,59 @@ module.exports = function ( graph ){
     renderAnnotationPairs(panel, node.annotations());
   }
   
+  /**
+   * Renders the read-only annotation rows for any element into the given D3 container.
+   * Used by editSidebar to mirror the non-editing view.
+   */
+  sidebar.renderAnnotationsForElement = function ( element, panel ){
+    if ( !element || !panel || panel.empty() ) return;
+    panel.selectAll("*").remove();
+
+    if ( elementTools.isNode(element) ) {
+      renderPair(panel, "Name", prefixedName(element.iri()));
+      renderPair(panel, "Label", element.labelForCurrentLanguage());
+      renderPair(panel, "Type", element.type());
+      renderNodeArrayPairs(panel, "Equivalent", element.equivalents());
+      renderNodeArrayPairs(panel, "Disjoint with", element.disjointWith());
+      renderNodeArrayPairs(panel, "Individual", element.individuals());
+      if ( element.links ) {
+        element.links().forEach(function ( link ){
+          var prop = link.property ? link.property() : null;
+          if ( prop && prop.type() === "rdfs:subClassOf" ) {
+            var parent = prop.range();
+            if ( parent && parent !== element ) {
+              renderPair(panel, "rdfs:subClassOf", prefixedName(parent.iri()), parent.iri());
+            }
+          }
+        });
+      }
+      var attrs = getAttributes(element.attributes());
+      if ( attrs ) renderPair(panel, "Characteristics", attrs);
+      renderCommentPairs(panel, element.comment());
+      renderAnnotationPairs(panel, element.annotations());
+    } else {
+      renderPair(panel, "Name", prefixedName(element.iri()));
+      renderPair(panel, "Label", element.labelForCurrentLanguage());
+      renderPair(panel, "Type", element.type());
+      if ( element.domain() ) renderPair(panel, "Domain", element.domain().labelForCurrentLanguage(), element.domain().iri());
+      if ( element.range() )  renderPair(panel, "Range",  element.range().labelForCurrentLanguage(),  element.range().iri());
+      if ( element.inverse() ) renderPair(panel, "Inverse", element.inverse().labelForCurrentLanguage(), element.inverse().iri());
+      renderNodeArrayPairs(panel, "Equivalent", element.equivalents());
+      renderNodeArrayPairs(panel, "Subproperty", element.subproperties());
+      renderNodeArrayPairs(panel, "Superproperty", element.superproperties());
+      if ( element.minCardinality() !== undefined ) {
+        renderPair(panel, "Min. cardinality", element.minCardinality());
+        renderPair(panel, "Max. cardinality", element.maxCardinality() !== undefined ? element.maxCardinality() : "*");
+      } else if ( element.cardinality() !== undefined ) {
+        renderPair(panel, "Cardinality", element.cardinality());
+      }
+      var attrs2 = getAttributes(element.attributes());
+      if ( attrs2 ) renderPair(panel, "Characteristics", attrs2);
+      renderCommentPairs(panel, element.comment());
+      renderAnnotationPairs(panel, element.annotations());
+    }
+  };
+
   /** Collapsible Sidebar functions; **/
   
   sidebar.showSidebar = function ( val, init ){
@@ -445,30 +498,35 @@ module.exports = function ( graph ){
   };
   
   sidebar.updateGeneralOntologyInfo = function (){
-    // get it from graph.options
+    // Refresh the ontology panel using the live meta object from graph.options()
     var generalMetaObj = graph.options().getGeneralMetaObject();
     var preferredLanguage = graph && graph.language ? graph.language() : null;
-    if ( generalMetaObj.hasOwnProperty("title") ) {
-      // title has language to it -.-
-      if ( typeof generalMetaObj.title === "object" ) {
-        d3.select("#title").node().value = languageTools.textInLanguage(generalMetaObj.title, preferredLanguage);
-      } else {
-        d3.select("#title").node().innerHTML = generalMetaObj.title;
-      }
-      
-    }
-    if ( generalMetaObj.hasOwnProperty("iri") ) d3.select("#about").node().innerHTML = generalMetaObj.iri;
-    if ( generalMetaObj.hasOwnProperty("iri") ) d3.select("#about").node().href = generalMetaObj.iri;
-    if ( generalMetaObj.hasOwnProperty("version") ) d3.select("#version").node().innerHTML = generalMetaObj.version;
-    if ( generalMetaObj.hasOwnProperty("author") ) d3.select("#authors").node().innerHTML = generalMetaObj.author;
-    // this could also be an object >>
-    if ( generalMetaObj.hasOwnProperty("description") ) {
-      if ( typeof generalMetaObj.description === "object" ) {
-        d3.select("#description").node().innerHTML = languageTools.textInLanguage(generalMetaObj.description, preferredLanguage);
-      }
-      else {
-        d3.select("#description").node().innerHTML = generalMetaObj.description;
-      }
+
+    var panel = d3.select("#ontology-panel");
+    if ( panel.empty() ) return;
+    panel.selectAll("*").remove();
+
+    var iri = generalMetaObj.iri;
+    renderPair(panel, "IRI", iri, iri);
+
+    var title = typeof generalMetaObj.title === "object"
+      ? languageTools.textInLanguage(generalMetaObj.title, preferredLanguage)
+      : generalMetaObj.title;
+    renderPair(panel, "Title", title);
+    renderPair(panel, "Version", generalMetaObj.version);
+
+    var author = generalMetaObj.author instanceof Array
+      ? generalMetaObj.author.join(", ")
+      : generalMetaObj.author;
+    renderPair(panel, "Author", author);
+
+    var description = typeof generalMetaObj.description === "object"
+      ? languageTools.textInLanguage(generalMetaObj.description, preferredLanguage)
+      : generalMetaObj.description;
+    renderPair(panel, "Description", description);
+
+    if ( panel.selectAll(".annotation-row").size() === 0 ) {
+      panel.append("p").classed("no-selection-hint", true).text("No ontology information available.");
     }
   };
   

src/index.html 30(+29 -1)

diff --git a/src/index.html b/src/index.html
index 6167b33..186e818 100644
--- a/src/index.html
+++ b/src/index.html
@@ -406,7 +406,7 @@
                 <textarea rows="4" cols="25" title="Ontology description as dc:description" class="descriptionTextClass"
                           id="descriptionEditor"></textarea>
             </div>
-            <h3 class="accordion-trigger noselect accordion-trigger-active">Selected Element</h3>
+            <h3 class="accordion-trigger noselect accordion-trigger-active">Selected Node</h3>
             <div>
                 <div id="selectedElementProperties" class="hidden">
                     <div class="textLineEditWithLabel">
@@ -436,6 +436,34 @@
                         <div id="property_characteristics_Selection" style="padding-top:2px;"></div>
                     </div>
 
+                    <!-- Read-only annotation view (same as non-editing mode) -->
+                    <div id="edit-node-annotations-panel" class="annotation-panel edit-annotations-panel"></div>
+
+                    <!-- Add annotation -->
+                    <div id="add-annotation-area">
+                        <label id="add-annotation-btn" class="edit-btn-annotation">+ Add Annotation</label>
+                        <div id="add-annotation-form" class="hidden">
+                            <div class="add-annotation-field">
+                                <label class="EditLabelForInput" for="ann-iri-input">Annotation:</label>
+                                <input type="text" id="ann-iri-input" list="ann-iri-datalist"
+                                       autocomplete="off" placeholder="e.g. rdfs:comment">
+                                <datalist id="ann-iri-datalist"></datalist>
+                            </div>
+                            <div class="add-annotation-field">
+                                <label class="EditLabelForInput" for="ann-value-input">Value:</label>
+                                <input type="text" id="ann-value-input" autocomplete="off" placeholder="Annotation value">
+                            </div>
+                            <div class="add-annotation-field">
+                                <label class="EditLabelForInput" for="ann-lang-input">Language:</label>
+                                <input type="text" id="ann-lang-input" autocomplete="off"
+                                       placeholder="en, fr, …" maxlength="10">
+                            </div>
+                            <div class="add-annotation-actions">
+                                <label id="ann-save-btn" class="edit-btn-annotation-save">Add</label>
+                                <label id="ann-cancel-btn" class="edit-btn-annotation-cancel">Cancel</label>
+                            </div>
+                        </div>
+                    </div>
                 </div>
                 <div id="selectedElementPropertiesEmptyHint">
                     Select an element in the visualization.
diff --git a/src/webvowl/js/graph.js b/src/webvowl/js/graph.js
index a519e8e..359c963 100644
--- a/src/webvowl/js/graph.js
+++ b/src/webvowl/js/graph.js
@@ -3326,9 +3326,7 @@ module.exports = function ( graphContainerSelector ){
     var propsToRemove = [];
     var nodesToRemove = [];
     var datatypes = 0;
-    
-    var remId;
-    
+
     nodesToRemove.push(node);
     for ( var i = 0; i < unfilteredData.properties.length; i++ ) {
       if ( unfilteredData.properties[i].domain() === node || unfilteredData.properties[i].range() === node ) {
@@ -3340,60 +3338,30 @@ module.exports = function ( graphContainerSelector ){
         }
       }
     }
-    var removedItems = propsToRemove.length + nodesToRemove.length;
-    if ( removedItems > 2 ) {
-      var text = "You are about to delete 1 class and " + propsToRemove.length + " properties";
-      if ( datatypes !== 0 ) {
-        text = "You are about to delete 1 class, " + datatypes + " datatypes  and " + propsToRemove.length + " properties";
-      }
-      
-      
-      graph.options().warningModule().responseWarning(
-        "Removing elements",
-        text,
-        "Awaiting response!", graph.removeNodesViaResponse, [nodesToRemove, propsToRemove], false);
-      
-      
-      //
-      // if (confirm("Remove :\n"+propsToRemove.length + " properties\n"+nodesToRemove.length+" classes? ")===false){
-      //     return;
-      // }else{
-      //     // todo : store for undo delete button ;
-      // }
+
+    // Build confirmation message describing everything that will be removed
+    var nodeLabel = node.labelForCurrentLanguage() || node.iri() || "this node";
+    var text;
+    if ( propsToRemove.length === 0 ) {
+      text = "Delete class \"" + nodeLabel + "\"?";
+    } else if ( datatypes !== 0 ) {
+      text = "Delete class \"" + nodeLabel + "\", " + datatypes + " datatype(s) and " + propsToRemove.length + " propert" + (propsToRemove.length === 1 ? "y" : "ies") + "?";
     } else {
-      // splice them;
-      for ( i = 0; i < propsToRemove.length; i++ ) {
-        remId = unfilteredData.properties.indexOf(propsToRemove[i]);
-        if ( remId !== -1 )
-          unfilteredData.properties.splice(remId, 1);
-        remId = properties.indexOf(propsToRemove[i]);
-        if ( remId !== -1 )
-          properties.splice(remId, 1);
-        propsToRemove[i] = null;
-      }
-      for ( i = 0; i < nodesToRemove.length; i++ ) {
-        remId = unfilteredData.nodes.indexOf(nodesToRemove[i]);
-        if ( remId !== -1 )
-          unfilteredData.nodes.splice(remId, 1);
-        remId = classNodes.indexOf(nodesToRemove[i]);
-        if ( remId !== -1 )
-          classNodes.splice(remId, 1);
-        nodesToRemove[i] = null;
-      }
-      graph.fastUpdate();
-      generateDictionary(unfilteredData);
-      graph.getUpdateDictionary();
-      options.focuserModule().handle(undefined);
-      nodesToRemove = null;
-      propsToRemove = null;
+      text = "Delete class \"" + nodeLabel + "\" and " + propsToRemove.length + " connected propert" + (propsToRemove.length === 1 ? "y" : "ies") + "?";
     }
+
+    graph.options().warningModule().responseWarning(
+      "Delete node",
+      text,
+      "This action cannot be undone.",
+      graph.removeNodesViaResponse, [nodesToRemove, propsToRemove], false);
   };
   
-  graph.removePropertyViaEditor = function ( property ){
+  graph._doRemoveProperty = function ( property ){
     property.domain().removePropertyElement(property);
     property.range().removePropertyElement(property);
     var remId;
-    
+
     if ( property.type().toLocaleLowerCase() === "owl:datatypeproperty" ) {
       var datatype = property.range();
       remId = unfilteredData.nodes.indexOf(property.range());
@@ -3411,12 +3379,9 @@ module.exports = function ( graphContainerSelector ){
     if ( remId !== -1 )
       properties.splice(remId, 1);
     if ( property.inverse() ) {
-      // so we have inverse
       property.inverse().inverse(0);
-      
     }
-    
-    
+
     hoveredPropertyElement = undefined;
     graph.fastUpdate();
     generateDictionary(unfilteredData);
@@ -3424,6 +3389,19 @@ module.exports = function ( graphContainerSelector ){
     options.focuserModule().handle(undefined);
     property = null;
   };
+
+  graph.removePropertyViaEditor = function ( property ){
+    var propLabel = property.labelForCurrentLanguage() || property.iri() || "this property";
+    var text = "Delete property \"" + propLabel + "\"?";
+    if ( property.type().toLocaleLowerCase() === "owl:datatypeproperty" ) {
+      text = "Delete datatype property \"" + propLabel + "\" and its range datatype node?";
+    }
+    graph.options().warningModule().responseWarning(
+      "Delete property",
+      text,
+      "This action cannot be undone.",
+      graph._doRemoveProperty, [property], false);
+  };
   
   graph.executeColorExternalsModule = function (){
     options.colorExternalsModule().filter(unfilteredData.nodes, unfilteredData.properties);