Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## [5.7.1] - 2026-08-06
### Changed
- RDFa editor: annotation overlay rebuilt on demand (`rdfa-editor/overlay.xsl`)

### Fixed
- RDFa editor: exit-canvas save fired a duplicate PATCH that 412'd on a stale `If-Match`; `onfocusout` is now the sole save trigger
- RDFa editor: block drag-handle visibility (bootstrap.css collision) and dropping blocks inside a LinkedDataHub document
- RDFa editor: annotation overlay dismissed on teardown; caret lands at the clicked word

## [5.7.0] - 2026-08-02
### Changed
- WYMEditor replaced with the RDFa editor for `rdf:XMLLiteral` editing (#336)
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<groupId>com.atomgraph</groupId>
<artifactId>linkeddatahub</artifactId>
<version>5.7.0</version>
<version>5.7.1-SNAPSHOT</version>
<packaging>${packaging.type}</packaging>

<name>AtomGraph LinkedDataHub</name>
Expand Down Expand Up @@ -46,7 +46,7 @@
<url>https://github.com/AtomGraph/LinkedDataHub</url>
<connection>scm:git:git://github.com/AtomGraph/LinkedDataHub.git</connection>
<developerConnection>scm:git:git@github.com:AtomGraph/LinkedDataHub.git</developerConnection>
<tag>linkeddatahub-5.7.0</tag>
<tag>linkeddatahub-5.5.4</tag>
</scm>

<repositories>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,7 @@ circle { cursor: move; }
.edit-dialog .rdfa-editor-ui .action-buttons { margin-top: 10px; }
.edit-dialog .rdfa-editor-ui .checkbox-label { display: inline-flex; gap: 6px; align-items: center; font-weight: 400; margin-top: 8px; }
.editor-bar:not(:has(#edit-toolbar)), body:not(:has(.rdfa-editor-content)) .editor-bar, body:not(:has(.rdfa-editor-content)) #rdfa-editor-breadcrumb { display: none; }
/* Host-page gutter for the block drag handles (rendered at left:-1.5em, outside each block's box).
The editor documents this as a host requirement (padding-left >= 2.5em); LDH must reserve it since
the .rdfa-editor-content div is generated bare by the XMLLiteral form control. */
.rdfa-editor-content { padding-left: 2.5em; }
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ xmlns:srx="&srx;"
xmlns:spin="&spin;"
xmlns:dct="&dct;"
xmlns:bs2="http://graphity.org/xsl/bootstrap/2.3.2"
xmlns:rdfae="https://w3id.org/atomgraph/rdfa-editor#"
extension-element-prefixes="ixsl"
exclude-result-prefixes="#all"
>
Expand Down Expand Up @@ -323,27 +324,29 @@ exclude-result-prefixes="#all"
</xsl:if>
</xsl:template>

<!-- click outside the editor canvas (left/right columns) while in edit mode exits edit mode -->

<xsl:template match="div[@typeof = '&ldh;XHTML'][descendant::form]" mode="ixsl:onclick">
<xsl:variable name="target" select="ixsl:get(ixsl:event(), 'target')" as="element()?"/>
<xsl:variable name="canvas" select="(descendant::div[contains-token(@class, 'rdfa-editor-content')])[1]" as="element()?"/>
<!-- skip if target is within the canvas or is an interactive element (interactive elements steal focus, triggering focusout + autosave) -->
<xsl:if test="exists($target) and empty($target/ancestor-or-self::*[. is $canvas or self::button or self::input or self::select or self::a])">
<xsl:variable name="form" select="(descendant::form[contains-token(@class, 'form-horizontal')])[1]" as="element()?"/>
<xsl:if test="$form">
<xsl:sequence select="ixsl:call($form, 'requestSubmit', [])"/>
</xsl:if>
</xsl:if>
</xsl:template>
<!-- exit/save on click-outside is handled by the ixsl:onfocusout autosave below: the canvas holds
focus throughout editing (toolbar/breadcrumb/find chrome all preventDefault on mousedown to keep
it), so any exit click blurs the canvas and fires focusout. A separate onclick handler here would
double-submit - focusout fires on mousedown, the click on mouseup - and the second PATCH 412s on
the now-stale If-Match. -->

<!-- click anywhere on XHTML content to enter edit mode (skip if text is selected) -->

<xsl:template match="div[@typeof = '&ldh;XHTML'][not(descendant::form)][acl:mode() = '&acl;Write']//div[contains-token(@class, 'main')]" mode="ixsl:onclick">
<xsl:if test="ixsl:call(ixsl:call(ixsl:window(), 'getSelection', []), 'toString', []) = ''">
<xsl:variable name="main" select="." as="element()"/>
<xsl:variable name="block" select="ancestor::div[contains-token(@class, 'block')][1]" as="element()"/>
<xsl:variable name="btn-edit" select="($block//button[contains-token(@class, 'btn-edit')][not(contains-token(@class, 'disabled'))])[1]" as="element()?"/>
<xsl:if test="$btn-edit">
<!-- Anchor the caret to the content structure, not pixels: record which content block the
click landed in and the char offset within it, re-resolved in the editor DOM after
render (ldh:focus-editable) - immune to the read/edit re-render and chrome. -->
<xsl:variable name="ldh-state" select="ixsl:get(ixsl:window(), 'LinkedDataHub')"/>
<xsl:variable name="caret" as="map(*)?" select="rdfae:caret-at-point(xs:double(ixsl:get(ixsl:event(), 'clientX')), xs:double(ixsl:get(ixsl:event(), 'clientY')))"/>
<!-- the content block: a top-level element of the XHTML content (div.main > content-div > block) -->
<xsl:variable name="content-block" as="element()?" select="$caret?node/ancestor-or-self::*[parent::*[parent::* is $main]][1]"/>
<ixsl:set-property name="pendingCaretBlock" select="$content-block ! count(preceding-sibling::*)" object="$ldh-state"/>
<ixsl:set-property name="pendingCaretOffset" select="$content-block ! ldh:char-offset-before(., $caret?node, $caret?offset)" object="$ldh-state"/>
<xsl:sequence select="ixsl:call($btn-edit, 'click', [])"/>
</xsl:if>
</xsl:if>
Expand All @@ -362,6 +365,41 @@ exclude-result-prefixes="#all"
</xsl:if>
</xsl:template>

<!-- rdfae:inject-chrome (edit.xsl) injects the block drag handle with class="drag-handle" -
a leftover token from when this file's WYMeditor block handle was ported into the editor.
In LDH that token collides with bootstrap.css's `.row-fluid.block .drag-handle { display: none }`,
which hides every chrome handle rendered inside an edited ldh:XHTML document block. Re-inject the
handle under an editor-specific class so the shared RDFa-Editor code needs no change (and so
block.xsl's own key('elements-by-class', 'drag-handle') stops miscounting the chrome as an old
block handle); the two drag gestures below re-match that class. Styling is unaffected - the
handle is styled off data-role="chrome" in rdfa-editor.css. Keep the body in sync with edit.xsl's
rdfae:inject-chrome (XSLT overrides the whole named template, so only the class string differs). -->
<xsl:template name="rdfae:inject-chrome">
<xsl:param name="block" as="element()"/>
<xsl:if test="empty($block/*[@data-role = 'chrome'])">
<xsl:variable name="chrome" as="element()" select="rdfae:element('span')"/>
<ixsl:set-attribute name="data-role" select="'chrome'" object="$chrome"/>
<ixsl:set-attribute name="class" select="'rdfa-editor-drag-handle'" object="$chrome"/>
<ixsl:set-attribute name="contenteditable" select="'false'" object="$chrome"/>
<ixsl:set-attribute name="title" select="'Drag to reorder'" object="$chrome"/>
<ixsl:set-property name="textContent" select="'&#x283F;'" object="$chrome"/>
<xsl:sequence select="ixsl:call($block, 'prepend', [ $chrome ])[current-date() lt xs:date('2000-01-01')]"/>
</xsl:if>
</xsl:template>

<xsl:template match="span[contains-token(@class, 'rdfa-editor-drag-handle')]" mode="ixsl:onmousedown">
<xsl:for-each select="rdfae:handle-block(.)">
<ixsl:set-attribute name="draggable" select="'true'"/>
</xsl:for-each>
</xsl:template>

<xsl:template match="span[contains-token(@class, 'rdfa-editor-drag-handle')]" mode="ixsl:onmouseup">
<xsl:for-each select="rdfae:handle-block(.)">
<ixsl:remove-attribute name="draggable"/>
</xsl:for-each>
<xsl:call-template name="rdfae:disarm-sweep"/>
</xsl:template>

<!-- override inline editing form for block types (do nothing if the button is disabled) - prioritize over form.xsl -->

<xsl:template match="div[following-sibling::div[@typeof = ('&ldh;XHTML', '&ldh;Object')]]//button[contains-token(@class, 'btn-edit')][not(contains-token(@class, 'disabled'))]" mode="ixsl:onclick" priority="1">
Expand Down Expand Up @@ -474,9 +512,14 @@ exclude-result-prefixes="#all"
</xsl:template>

<!-- dragging block over other block -->
<!-- only handle if drag originated from drag-handle (has text/uri-list item) -->
<!-- only handle if drag originated from drag-handle (has text/uri-list item) -->
<!-- these four document-block DnD handlers (ondragover/enter/leave/drop) exclude the RDFa editor
subtree: the editor renders inside a .content-body > .block, so without the guard this pattern
also matches every element in .rdfa-editor-content and - having higher import precedence than
the imported edit.xsl - shadows the editor's own block DnD (edit.xsl), killing its drop marks
and reorder. Excluding the editor lets those events fall through to the editor's handlers. -->

<xsl:template match="*[ancestor-or-self::div[contains-token(@class, 'block')][parent::div[contains-token(@class, 'content-body')]][acl:mode() = '&acl;Write']]" mode="ixsl:ondragover" priority="1">
<xsl:template match="*[ancestor-or-self::div[contains-token(@class, 'block')][parent::div[contains-token(@class, 'content-body')]][acl:mode() = '&acl;Write']][not(ancestor-or-self::*[contains-token(@class, 'rdfa-editor-content')])]" mode="ixsl:ondragover" priority="1">
<xsl:variable name="block" select="ancestor-or-self::div[contains-token(@class, 'block')][parent::div[contains-token(@class, 'content-body')]][1]" as="element()"/>
<xsl:variable name="uri" select="xs:anyURI($block/parent::div/parent::div[contains-token(@class, 'document-body')]/@about)" as="xs:anyURI"/>
<xsl:variable name="results" select="ixsl:get(ixsl:get(ixsl:get(ixsl:window(), 'LinkedDataHub.contents'), '`' || $uri || '`'), 'results')" as="document-node()"/>
Expand All @@ -495,7 +538,7 @@ exclude-result-prefixes="#all"
<!-- change the style of blocks when block is dragged over them -->
<!-- only handle if drag originated from drag-handle (has text/uri-list item) -->

<xsl:template match="*[ancestor-or-self::div[contains-token(@class, 'block')][parent::div[contains-token(@class, 'content-body')]][acl:mode() = '&acl;Write']]" mode="ixsl:ondragenter" priority="1">
<xsl:template match="*[ancestor-or-self::div[contains-token(@class, 'block')][parent::div[contains-token(@class, 'content-body')]][acl:mode() = '&acl;Write']][not(ancestor-or-self::*[contains-token(@class, 'rdfa-editor-content')])]" mode="ixsl:ondragenter" priority="1">
<xsl:variable name="block" select="ancestor-or-self::div[contains-token(@class, 'block')][parent::div[contains-token(@class, 'content-body')]][1]" as="element()"/>
<xsl:variable name="uri" select="xs:anyURI($block/parent::div/parent::div[contains-token(@class, 'document-body')]/@about)" as="xs:anyURI"/>
<xsl:variable name="results" select="ixsl:get(ixsl:get(ixsl:get(ixsl:window(), 'LinkedDataHub.contents'), '`' || $uri || '`'), 'results')" as="document-node()"/>
Expand All @@ -512,7 +555,7 @@ exclude-result-prefixes="#all"

<!-- only handle if drag originated from drag-handle (has text/uri-list item) -->

<xsl:template match="*[ancestor-or-self::div[contains-token(@class, 'block')][parent::div[contains-token(@class, 'content-body')]][acl:mode() = '&acl;Write']]" mode="ixsl:ondragleave" priority="1">
<xsl:template match="*[ancestor-or-self::div[contains-token(@class, 'block')][parent::div[contains-token(@class, 'content-body')]][acl:mode() = '&acl;Write']][not(ancestor-or-self::*[contains-token(@class, 'rdfa-editor-content')])]" mode="ixsl:ondragleave" priority="1">
<xsl:variable name="block" select="ancestor-or-self::div[contains-token(@class, 'block')][parent::div[contains-token(@class, 'content-body')]][1]" as="element()"/>
<xsl:variable name="uri" select="xs:anyURI($block/parent::div/parent::div[contains-token(@class, 'document-body')]/@about)" as="xs:anyURI"/>
<xsl:variable name="results" select="ixsl:get(ixsl:get(ixsl:get(ixsl:window(), 'LinkedDataHub.contents'), '`' || $uri || '`'), 'results')" as="document-node()"/>
Expand All @@ -535,7 +578,7 @@ exclude-result-prefixes="#all"
<!-- dropping block over other top-level block -->
<!-- only handle if drag originated from drag-handle (has text/uri-list item) -->

<xsl:template match="*[ancestor-or-self::div[contains-token(@class, 'block')][parent::div[contains-token(@class, 'content-body')]][acl:mode() = '&acl;Write']]" mode="ixsl:ondrop" priority="1">
<xsl:template match="*[ancestor-or-self::div[contains-token(@class, 'block')][parent::div[contains-token(@class, 'content-body')]][acl:mode() = '&acl;Write']][not(ancestor-or-self::*[contains-token(@class, 'rdfa-editor-content')])]" mode="ixsl:ondrop" priority="1">
<xsl:variable name="block" select="ancestor-or-self::div[contains-token(@class, 'block')][parent::div[contains-token(@class, 'content-body')]][1]" as="element()"/>
<xsl:variable name="uri" select="xs:anyURI($block/parent::div/parent::div[contains-token(@class, 'document-body')]/@about)" as="xs:anyURI"/>
<xsl:variable name="results" select="ixsl:get(ixsl:get(ixsl:get(ixsl:window(), 'LinkedDataHub.contents'), '`' || $uri || '`'), 'results')" as="document-node()"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,9 @@ WHERE
<!-- first editable region on the page: full editor bring-up (chrome, dialogs, drawers, all regions) -->
<xsl:template match="div[contains-token(@class, 'rdfa-editor-content')][empty(id('edit-toolbar', ixsl:page()))]" mode="ldh:RenderRowForm" priority="2">
<xsl:call-template name="rdfae:init-editor"/>
<xsl:for-each select="(.//*[@contenteditable = 'true'])[1]">
<xsl:sequence select="ixsl:call(., 'focus', [])[current-date() lt xs:date('2000-01-01')]"/>
</xsl:for-each>
<xsl:call-template name="ldh:focus-editable">
<xsl:with-param name="region" select="."/>
</xsl:call-template>
</xsl:template>

<!-- additional region: editor chrome already in the DOM; init only this region's blocks -->
Expand All @@ -164,9 +164,64 @@ WHERE
<xsl:call-template name="rdfae:init-region">
<xsl:with-param name="region" select="."/>
</xsl:call-template>
<xsl:for-each select="(.//*[@contenteditable = 'true'])[1]">
<xsl:sequence select="ixsl:call(., 'focus', [])[current-date() lt xs:date('2000-01-01')]"/>
</xsl:for-each>
<xsl:call-template name="ldh:focus-editable">
<xsl:with-param name="region" select="."/>
</xsl:call-template>
</xsl:template>

<!-- characters of real (non-chrome) text in $block before the (node, offset) caret position; a
Range gives an exact count that maps 1:1 to the same text re-rendered in the editor DOM -->
<xsl:function name="ldh:char-offset-before" as="xs:integer">
<xsl:param name="block" as="element()"/>
<xsl:param name="node"/>
<xsl:param name="offset"/>
<xsl:variable name="range" select="ixsl:call(ixsl:page(), 'createRange', [])"/>
<xsl:sequence select="ixsl:call($range, 'setStart', [ $block, 0 ])[current-date() lt xs:date('2000-01-01')]"/>
<xsl:sequence select="ixsl:call($range, 'setEnd', [ $node, $offset ])[current-date() lt xs:date('2000-01-01')]"/>
<xsl:sequence select="string-length(string(ixsl:call($range, 'toString', [])))"/>
</xsl:function>

<!-- the (text node, local offset) at character $offset into $block's real text, skipping chrome -->
<xsl:function name="ldh:text-position" as="map(*)?">
<xsl:param name="block" as="element()?"/>
<xsl:param name="offset" as="xs:integer"/>
<xsl:variable name="texts" as="text()*" select="$block//text()[not(ancestor::*[@data-role])]"/>
<xsl:variable name="i" as="xs:integer?" select="(for $n in 1 to count($texts)
return if (sum($texts[position() le $n] ! string-length(.)) ge $offset) then $n else ())[1]"/>
<xsl:sequence select="if (empty($texts)) then ()
else if (empty($i)) then map{ 'node': $texts[last()], 'offset': string-length($texts[last()]) }
else map{ 'node': $texts[$i], 'offset': $offset - xs:integer(sum($texts[position() lt $i] ! string-length(.))) }"/>
</xsl:function>

<!-- Focus the region's first editable host. When a click-to-edit stashed a structural caret anchor
(window.LinkedDataHub pendingCaretBlock/Offset, set in block.xsl), re-resolve it against the
editor DOM - the same content block at the same character offset - and place the caret there.
Falls back to plain focus (block start) for non-click entry or when the anchor no longer resolves. -->
<xsl:template name="ldh:focus-editable">
<xsl:param name="region" as="element()"/>

<xsl:variable name="ldh-state" select="ixsl:get(ixsl:window(), 'LinkedDataHub')"/>
<xsl:variable name="block-idx" select="ixsl:get($ldh-state, 'pendingCaretBlock')"/>
<xsl:variable name="char-off" select="ixsl:get($ldh-state, 'pendingCaretOffset')"/>
<ixsl:set-property name="pendingCaretBlock" select="()" object="$ldh-state"/>
<ixsl:set-property name="pendingCaretOffset" select="()" object="$ldh-state"/>

<xsl:variable name="pos" as="map(*)?" select="if (exists($block-idx) and exists($char-off))
then ldh:text-position(($region/*[not(@data-role)])[xs:integer($block-idx) + 1], xs:integer($char-off)) else ()"/>

<xsl:choose>
<xsl:when test="exists($pos)">
<xsl:call-template name="rdfae:focus-caret">
<xsl:with-param name="node" select="$pos?node"/>
<xsl:with-param name="offset" select="$pos?offset"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:for-each select="($region//*[@contenteditable = 'true'])[1]">
<xsl:sequence select="ixsl:call(., 'focus', [])[current-date() lt xs:date('2000-01-01')]"/>
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
</xsl:template>

<!-- override RDFa editor annotation typeahead: use LDH's /ns-querying bs2:Lookup instead of doc()-on-vocabs -->
Expand Down Expand Up @@ -1254,10 +1309,13 @@ WHERE
</xsl:otherwise>
</xsl:choose>

<!-- the editor has reverted to read-only; dismiss the annotation overlay so it does
not linger orphaned over the read view (it rebuilds on the next right-click) -->
<xsl:call-template name="rdfae:hide-overlay"/>
<ixsl:set-style name="cursor" select="'default'" object="ixsl:page()//body"/>
</xsl:for-each>
</xsl:function>

<xsl:function name="ldh:form-submit-created" ixsl:updating="yes">
<xsl:param name="context" as="map(*)"/>
<xsl:variable name="response" select="$context('response')" as="map(*)"/>
Expand Down
Loading
Loading