WebVOWL_work
Changes
src/app/css/toolstyle.css 89(+89 -0)
src/app/js/editSidebar.js 169(+163 -6)
src/app/js/sidebar.js 102(+80 -22)
src/index.html 30(+29 -1)
src/webvowl/js/graph.js 86(+32 -54)
Details
src/app/css/toolstyle.css 89(+89 -0)
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;
src/app/js/editSidebar.js 169(+163 -6)
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.
src/webvowl/js/graph.js 86(+32 -54)
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);