Node identity and parentage are XPath, not DOM calls - #31
Open
namedgraph wants to merge 1 commit into
Open
namedgraph wants to merge 1 commit into
namedgraph wants to merge 1 commit into
Conversation
Nine reads reached through ixsl:call/ixsl:get for facts the data model
already exposes, so the module asked JavaScript a question XPath answers.
isSameNode($a, $b) is `$a is $b`; parentNode is `..`; parentElement, which
is null when the parent is not an element, is exactly `parent::*`; and a
Range container's nodeType = 3 test is `instance of text()`. The event
targets that carried two of the identity tests take `as="node()?"` so the
comparison has a node to compare - the same typing relatedTarget already
had in navigate.xsl, and the reason `is` was reachable there and not here.
Two constructions follow: Object() through ixsl:call is ixsl:new('Object',
[]), the extension that exists for it; and typeahead's three-line scroll
options object - allocate, set `block`, pass - is one XDM map literal, the
form navigate.xsl was already passing to the same scrollIntoView on the
line above its own call.
Nothing moves in the DOM layer proper. Building detached subtrees through
rdfae:element and splicing them with insertNode/appendChild/before stays:
xsl:result-document emits at a location and returns nothing to hold, and
the insertion model hands the constructed node onward to be spliced, to
receive the caret and to seed a Range. That is an architecture, not a
workaround, and converting it would be a redesign of the editing core.
make sef compiles both entries; make test passes 52; make test-browser
reports 532 assertions true with the two pre-existing canonicalSource
paste-probe values unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
namedgraph
added a commit
to AtomGraph/LinkedDataHub
that referenced
this pull request
Sep 11, 2026
…L stops asking JavaScript what XPath already knows Paginating a view, or reversing its sort, collapsed the whole block to a placeholder and back, and took the pagination controls down with it - the button the reader had just pressed vanished from under the cursor for as long as the query took. Two causes, both structural. The pager was emitted INSIDE .container-results, by each of ldh:ListViewBlock, ldh:TableViewBlock and ldh:GridViewBlock beside their rows. That div is what ldh:RenderViewMode replaces on every mode, facet, sort and page change, so a control was a child of the thing it re-renders. It now has its own host - #<view>-pager, rendered once beside the results region by ldh:RenderViewResults and filled by ldh:RenderPager - so a results swap cannot reach it, and ldh:result-count-response addresses it by id instead of descending into the region. The $show test moves into ldh:RenderPager and picks up the mode check, so switching to the map empties the host the list left behind rather than stranding it. This is also the design system's own anatomy for a paged view: rows and pager as siblings of the block body, which app.css has laid out all along and which ldh.css was bridging around with position: sticky plus three surface-matching backgrounds - compensation for the pager scrolling among the rows, and now deleted along with the reason for it. Nothing scrolls under the pager any more, so it needs no opaque surface, which is what made the nblock overrides removable rather than merely movable. The other cause was ldh:show-view-skeleton, which emptied the results region on every re-query and appended a fixed FOUR-row ldh:BlockSkeleton regardless of page size: twenty rows became four and back on every click. A skeleton stands in for content whose shape is known but which is not yet drawn, and a re-query's rows are already on the screen. ldh:begin-view-refresh leaves them standing and marks the region busy - dimmed and inert, so the height cannot move and a second page request cannot be queued on the first - cleared in ldh:RenderViewMode on success, in ldh:promise-failure for the chain that dies after the results land (a metadata request rejecting reaches neither the render nor the results error handler, and would have left a view dimmed for the session). ldh:BlockSkeleton loses its only caller and goes; the kit's .ldh-block-loading stays in app.css for a block whose shape is known and undrawn, which this never was. Upstream in the design system (published to the Claude Design project, and app.css re-vendored from it): .ldh-pager:empty is display: none, and the paged-view layout re-scopes from :has(> .ldh-pager) to :has(> .ldh-pager:not(:empty)). The kit's README promised "an unpaged view is untouched", which only held while the pager was conditionally rendered; a client that renders the host up front - which it must, or the control vanishes - has one on every view, so the bare :has() capped and scrolled bodies that never page, and the empty host drew its padding, margin and top border as a stray rule under the rows. Measured: 25px strip and max-height 620px before, 0px and none after. While in the client XSL, the DOM calls that have ixsl or XPath equivalents retire. ixsl:remove-attribute for six removeAttribute calls, ixsl:set-attribute for seven setAttribute, ixsl:set-style for the block bar's --p, ixsl:new for Reflect.construct-by-name, ixsl:remove-property for Reflect.deleteProperty, ixsl:json-parse for three JSON.parse, ixsl:style for getComputedStyle (it IS window.getComputedStyle - checked in the SaxonJS source, not the docs), ixsl:location for window.location.href and .hash, xsl:result-document for an append of constructed markup, and plain XPath for nine ixsl:get(node, 'id') reads and two Node.contains tests - the latter the idiom navigation.xsl was already using a few lines away. classList goes the same way, all 85 sites. ldh:set-token($class, $token, $on) returns @Class with the token present iff $on, which covers add (true()), remove (false()) and - this is why one function suffices - the two-argument classList.toggle(token, force), which 47 of the 48 toggles were. The one genuine flip, the 3D canvas fullscreen, spells it not(contains-token(...)). distinct-values rather than a concatenation: a token already present keeps its position and is not repeated, which is what classList.add promises and what @Class || ' x' quietly breaks on the second call. Class state is now one vocabulary - contains-token() to read, ldh:set-token() to write - where it had been XPath on one side and a DOM object on the other. The vendored rdfa-editor copies take the same substitutions (isSameNode is `is`, parentNode is `..`, parentElement is `parent::*`, nodeType = 3 is `instance of text()`, plus ixsl:new and an XDM map literal for scrollIntoView options), applied in place rather than re-vendored, because those seven files carry LDH's translation layer that a wholesale copy would destroy. Filed upstream as AtomGraph/RDFa-Editor#31. RDFa2RDFXML-v3.xsl takes upstream's comment wording, the one hunk in that tree not explained by the translation divergence; the vendored copies are otherwise byte-identical or translations-only, verified hunk by hunk. Verified rather than assumed. The busy state holds geometry exactly - block 677px to 677px across the refresh and across a page flip, results 427px unchanged, pager fixed at top 613 and never hidden - measured in headless Chromium against the four stylesheets in link order, along with a 100-row page scrolling inside the region with the pager still fully visible, an unpaged view back to display: block and max-height: none, and a view nested in a well getting a transparent static pager below its scrollport. ldh:set-token passes 14 cases on SaxonJS including prefix safety (is-open-wide is not is-open), idempotent add and an absent @Class. And because several sites write class three times in a row, each re-reading @Class, the composition was checked in a real browser with the shipped SaxonJS runtime: ixsl:set-attribute calls setAttribute() immediately, no pending update list, so the writes compose exactly as the classList calls did. The SEF compiles clean. Not yet live: the LTLOD image bakes the WAR, so the click-through rides the next rebuild. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nine reads in the editor reached through
ixsl:call/ixsl:getfor facts the data model already exposes, so the modules were asking JavaScript questions XPath answers:ixsl:call($a, 'isSameNode', [ $b ])$a is $bannotate.xsl×2,edit.xslixsl:get($n, 'parentNode')..annotate.xsl,edit.xsl×2ixsl:get($n, 'parentElement')parent::*navigate.xslixsl:get($n, 'nodeType') = 3instance of text()annotate.xslixsl:call(ixsl:window(), 'Object', [])ixsl:new('Object', [])index.xsl,typeahead.xslmap{ 'block': 'nearest' }typeahead.xslparentElementis null when the parent is not an element, which is exactly whatparent::*selects, so that one is a faithful swap rather than a near-one.The two event targets carrying identity tests take
as="node()?"so the comparison has a node to compare — the same typingrelatedTargetalready had innavigate.xsl, which is whyiswas reachable there and not here.typeahead.xslwas allocating an object, settingblockon it and passing it toscrollIntoView;navigate.xslpasses an XDM map literal to the same method a few lines from its own call, so the map form was already the house idiom.What deliberately does not move
The DOM layer proper. Building detached subtrees through
rdfae:elementand splicing them withinsertNode/appendChild/beforestays as it is:xsl:result-documentemits at a location and hands back nothing to hold, while the insertion model passes the constructed node onward to be spliced, to receive the caret and to seed a Range. That is an architecture, not a workaround, and converting it would be a redesign of the editing core rather than a cleanup.Verification
make sef— both entries compilemake test— 52 pass, 0 failmake test-browser— 532 assertions true; the twocanonicalSourcepaste-probe values that report false are unchanged frommaster(verified by stashing and re-running)🤖 Generated with Claude Code
https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd