Skip to content

feat(canvas): surface default content in outline panel - #1167

Merged
shsteimer merged 26 commits into
mainfrom
outldflt
Jul 30, 2026
Merged

feat(canvas): surface default content in outline panel#1167
shsteimer merged 26 commits into
mainfrom
outldflt

Conversation

@shsteimer

@shsteimer shsteimer commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

requires adobe/da-nx#627 to land as well for the selection/scroll into view to work

Summary

  • The outline panel now surfaces loose page content — paragraphs, headings, lists, images, code blocks, blockquotes — as expandable "Default content" groups per section, interleaved with blocks in real document order (parseSections in editor-utils.js).
  • Default-content items support drag-reorder, delete, and moving between blocks and content (both directions), mirroring the existing block move/delete behavior (editor-utils/blocks.js).
  • Selecting a block, section, content group, or content child in the outline drives the doc/WYSIWYG selection and vice versa, via a shared editorSelectChange/editorProseSelectChange pub/sub — so the doc editor, WYSIWYG view, and outline all stay in sync.
  • Content-group expand/collapse state only resets on a genuine structural edit (a reparse); selecting a node only additively expands the run it belongs to — including nodes with no row of their own, like a fresh empty paragraph from pressing Enter — and never collapses other groups. Deleting a content child keeps its own run expanded too, without needing a dedicated selection workaround.

Why

Default content (loose paragraphs/headings/etc. not wrapped in a block) previously had no representation in the outline, making it hard to see, reorder, or delete relative to blocks. This closes that gap while keeping the outline's selection and expansion behavior predictable across edits.

fix #1159

https://outldflt--da-live--adobe.aem.live/

Test plan

  • npm run lint passes (JS + CSS)
  • npm test passes (1779 tests, 0 failures, 4 skipped)
  • verified by running aem locally and testing in stage sandbox site for block collection

🤖 Generated with Claude Code

parseSections now walks all direct children of a section (not just
div[class] blocks), grouping contiguous runs of loose content (p,
h1-h6, ul/ol, picture) into read-only "Default content" entries so a
section with only body text no longer shows as empty. Adds
editorProseSelectChange, a generalized position-based select/scroll
channel (separate from the block-index-based editorSelectChange), and
wires it into ew-editor-doc.js to scroll to an arbitrary prose
position when a default-content entry is clicked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@aem-code-sync

aem-code-sync Bot commented Jul 22, 2026

Copy link
Copy Markdown

Hello, I'm the AEM Code Sync Bot and I will run some actions to deploy your branch.
In case there are problems, just click the checkbox below to rerun the respective action.

  • Re-sync branch
Commits

@shsteimer shsteimer changed the title feat(canvas): surface loose page content in outline panel feat(canvas): surface default content in outline panel Jul 23, 2026
@shsteimer
shsteimer requested a review from hannessolo July 23, 2026 00:52
@shsteimer
shsteimer marked this pull request as ready for review July 23, 2026 00:52
@shsteimer

Copy link
Copy Markdown
Contributor Author

note:

I built #1166 and #1165 along with this as part of trying to make the outline panel a bit easier to use. but haven't tested the 3 together, so sort of want to merge one at time and saw there was an open issue for this, so figured I'd start with this one.

@hannessolo

Copy link
Copy Markdown
Contributor

Fix #1159 #1110

@hannessolo

Copy link
Copy Markdown
Contributor

Thanks a lot for the PR!

I'm not sure grouping adjacent default content into a single Default Content group makes sense. If we want to allow users to edit the page 100% from the layout view, we'd need to allow them to also move, add and delete default content, which means we'd have to show each paragraph/image etc separately.

Maybe we could make the current Default Content groups that this PR adds expandable to show all the actual content, so it doesn't become a giant list?

@shsteimer

Copy link
Copy Markdown
Contributor Author

I like the idea of a root section that can be expanded or collapsed. That lends itself if we eventually want to do something similar for items in collection blocks.

will make that change and let you know.

…t children

Default content in the outline is now a collapsed-by-default group listing
each consecutive loose item's kind, instead of one opaque entry. Clicking an
image child also selects it as a NodeSelection and broadcasts it to the
layout-view iframe, matching block selection; text kinds stay doc-view only
pending a cross-repo follow-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…utline

Changing a heading's level (or list ordered/unordered) only nudged the
WYSIWYG mini-editor via getEditor(), never emitting editorHtmlChange —
so the outline (and other listeners) never re-parsed. createTrackingPlugin
now forces a full rerenderPage() whenever a change is 'attrs' or 'replaced',
reserving the lightweight sync path for plain text edits.

Also recognizes <pre> code blocks in the outline's default-content group
(stamped with data-prose-index like other loose nodes), and renames
"Bulleted list" to "Bullet list" to match the slash-command menu wording.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
shsteimer and others added 2 commits July 23, 2026 10:02
- getDefaultContentProseIndex: for kind 'image', prefer the nested
  data-image-index over a wrapping <p>'s own data-prose-index so the
  image node resolves correctly in the doc
- getDefaultContentKind: classify <blockquote> as 'quote' and make the
  fallback content-aware so text-bearing tags aren't mislabeled 'image';
  drop the now-redundant <p> branch
- prose-diff: narrow the full-rerender trigger to attrs/replaced changes
  on EDITABLE_TYPES nodes; image/table attr edits take the lightweight
  getEditor sync path
- ew-page-outline: drop redundant blocks comparison from sectionsEqual
- rename loose* helpers to defaultContent* to match the AEM term
- trim verbose comments to why-only, matching file conventions
- add coverage: _scrollDocToProseIndex, blockquote classification,
  image proseIndex, and image-attr rerender path

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…roups

ArrowRight expands a collapsed "Default content" group header, ArrowLeft
collapses an expanded one, by re-firing the header's existing click
handler so no extra proseIndex plumbing is needed. Falls through to the
existing treeKeydown nav otherwise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@shsteimer

Copy link
Copy Markdown
Contributor Author

@hannessolo This probably warrants additional sets of eyes and testing before merging. Here is what this looks like

collapsed by default
Screenshot 2026-07-23 at 10 08 46 AM

expanded
Screenshot 2026-07-23 at 10 09 17 AM

There are several known gaps here still:

  • clicking a content item syncs the content view (scrollIntoView), but layout view has gaps that will require da-nx changes (currently only works for images). I plan to do this in a set of follow up PRs, but if there is a strong opinion that it needs to be in this PR, let me know and I'll try to tackle it now
  • keyboard nav for outline isn't great. I handled arrow keys to expand/collapse the default content sections, but didn't attempt to fix this generally. That IMHO falls outside scope of this PR
  • drag handles, move/add/delete, both for the default content sections as a whole, and for individual items. I think we should do something here, but I genuinely am not sure what the right approach to this is. My thinking is we ship without it, and see how it feels to use it/get feedback, then decide what feels like the right balance. But again, if you have thoughts on what you'd like to see as a starting point, those are welcome and happy to add them in this PR if you prefer that.

@hannessolo

Copy link
Copy Markdown
Contributor

Nice!

  • Scroll into view is fine to leave as is for now IMO
  • Opening a follow up ticket for KB navigation
  • IMO we should add drag handles for the default content in the section. I want to get to the point where we can do basic editing from the layout mode, and that would be a requirement. Same for deleting, since we already have the UI for that. Adding new default content can wait though IMO

@hannessolo
hannessolo requested a review from sharanyavinod July 24, 2026 14:00
shsteimer and others added 2 commits July 24, 2026 09:36
Lets default-content children (paragraph/heading/list/image/code/quote)
be dragged anywhere in the doc — within a group, across groups in a
section, or into a different section entirely (including one with no
default content yet) — and deleted individually, matching the existing
block/section drag+delete UI. Cross-type moves land as siblings only
(never merged into a block).

blocks.js gains getContentItemRange/deleteContentItem/moveContentItem
and getSectionEndOffset; moveBlock's delete+insert math is extracted
into a shared spliceNode helper (no behavior change, now covered by
regression tests since it previously had none).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s/content interoperate as drop targets

getContentItemRange assumed proseIndex already pointed at a content
item's own node start. In reality getInstrumentedHTML stamps it at the
node's content-start (view.posAtDOM(el, 0)), which only coincided with
the node start for the image case already special-cased. Every other
kind was broken: moving/deleting a paragraph, heading, or code block
would only touch its inner text (orphaning an empty wrapper), a quote
would drop two levels too deep, and a multi-item list would only ever
move/delete its first item. Fixed generally via
doc.resolve(proseIndex).before(1), which recovers the true top-level
node regardless of kind or nesting depth.

Also closes a gap where blocks and content items couldn't target each
other when dragging: content rows/groups only accepted content-type
drags, and a block dragged into a section with no blocks (including a
wholly empty one) had nowhere to land. Adds moveBlockToContentItem and
moveBlockToSection so blocks and content items are drop targets for
each other in both directions. Supersedes PR #1166 (dropempt), which
fixed only the empty-section case in isolation.

Rewrote blocks.js/ew-page-outline test coverage to build child
descriptors via a real EditorView + getInstrumentedHTML + parseSections
(makeRealView in test-helpers.js) instead of hand-picked node
positions, since that gap is exactly what let the original bug through
untested.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Several comments from the previous commit restated the same point
across multiple lines instead of saying it once, directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
.content-item's pointer cursor (for the group-toggle header) was
overriding .block-item's grab cursor on draggable content-child rows,
since it comes later in the cascade. Scope it to the header only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sharanyavinod

Copy link
Copy Markdown
Contributor

Thank you for the contribution, this looks great! Couple minor things:

  • Could we have the text left aligned, similar to how its in Files?
Screenshot 2026-07-29 at 11 36 29 Screenshot 2026-07-29 at 11 36 38
  • Could we have a small part of the text instead of just the type ie. Paragraph, Heading etc? Right now its very confusing to know which is which especially since they can be moved etc.

shsteimer and others added 2 commits July 29, 2026 07:43
Blocks and content-group headers now start at the same left edge
(blocks previously lacked the chevron's reserved indent). Content-child
rows also show a truncated text snippet under the type label so
similarly-typed nodes (Paragraph, Heading, etc.) are distinguishable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
shsteimer and others added 4 commits July 29, 2026 10:15
Selecting a default-content item (paragraph/heading/list/code/quote) from
the outline now scrolls and highlights it in both the content and layout
views, matching existing block/image behavior. Also fixes the reverse
direction: editing or selecting content directly in the content view (or
via layout, once synced) now highlights the matching row in the outline,
which previously only tracked block selections.

Adds blockquote to EDITABLES so quote items get their own data-prose-index
instead of falling back to a nested paragraph's.

Co-Authored-By: Claude <noreply@anthropic.com>
.content-label and .content-snippet had their own explicit gray color,
which overrode the blue text .block-item.selected already applies —
blocks don't hit this since .block-name has no such override.

Co-Authored-By: Claude <noreply@anthropic.com>
Previously showed the full concatenated text of a content child, which
overflowed for long paragraphs/lists. Now derives a per-kind first-line
snippet (first list item, first quote paragraph, text before any <br>)
and relies on existing CSS text-overflow: ellipsis for visual truncation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ty, move/delete selection

Replaces the fragile proseIndex-persistence model for default-content expand/collapse
with one that resets on every selection change: block selection collapses all runs,
content selection expands only its own run, and manual expand/collapse persists only
until the next selection change. This shrinks proseIndex drift exposure to within a
single selection instead of indefinitely across arbitrary doc edits.

Also:
- Empty default-content nodes (e.g. an empty heading/paragraph) now stay in the parsed
  tree flagged `empty`, hidden in the outline unless currently selected — so a node
  being actively edited is visible/selectable. Required keeping empty <p> tags alive
  through prose2aem's canvas-only instrumented-HTML path (keepEmptyParagraphs), and
  fixing getDefaultContentKind's "no text = image" heuristic, which broke once
  genuinely empty (imageless) paragraphs could reach it.
- Drag/drop reorder now selects the moved item (via tr.setSelection inside the same
  transaction); deleting a content item selects the next/previous sibling in the same
  run; block/section delete leave selection untouched.
- Fixed a pre-existing bug where handleCursorMove's redundant blockIndex-only
  editorSelectChange emit clobbered the correct proseIndex-carrying 'doc' emit
  immediately after it, breaking outline sync on the first WYSIWYG cursor move.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cut redundant/verbose comments added in this PR: dropped one that
restated what the name/body already conveyed, deduped a proseIndex
convention explained twice across files, and tightened several
multi-line comments down to their essential point.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reverts the keepEmptyParagraphs flag added to the shared prose2aem module
(used by save/publish/live-preview/diff) and moves empty-node handling
entirely into canvas's getInstrumentedHTML. Emptiness is now determined
from the actual ProseMirror node (content.size === 0) rather than a flag
threaded through shared code, and empty <p>s are masked with a placeholder
comment before serialization, then stripped back out — so the outline can
still see/select an empty node being edited, while SET_BODY to the layout
view and all other prose2aem callers are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
shsteimer and others added 2 commits July 29, 2026 15:10
Reverts the keepEmptyParagraphs plumbing and outline visibility logic that
kept empty paragraphs/headings alive through instrumented HTML so they could
be seen and selected in the outline. Too much complexity for the value;
empty nodes are skipped again when building content runs, same as before
PR #1167 introduced the feature.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… selection/delete

Replaces the per-selection collapse/expand-only-one-run model with a simpler
one: expansion only resets on a real reparse (a structural edit that changes
_sections); selection changes only additively expand the run holding the new
selection, never collapse anything else. A range-based fallback in
_findRunKeyForProseIndex also expands a run when the selection lands on a
node that has no row of its own (e.g. a fresh empty paragraph from pressing
Enter, filtered out of parseSections), not just an exact child match.

Also drops the now-vestigial sibling-select-on-delete workaround in
deleteContentItem (it stopped being what kept a run visible once expansion
became reparse-driven) and replaces it with a direct fix: _onDelete captures
the deleted content item's run by array position and re-expands it after the
delete's reparse, so deleting a child no longer collapses its own run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ead prose2aem arg

getDefaultContentKind now explicitly checks for a nested/matching <img> before
falling back to 'paragraph', instead of assuming a text-less element is always
an image. Also reverts the getInstrumentedHTML->prose2aem call's trailing arg
back to what main already has (unused since before this PR, prose2aem never
read a 4th param) so that line doesn't show up as an unnecessary diff.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@shsteimer

Copy link
Copy Markdown
Contributor Author

@sharanyavinod addressed both your items, agree it looks way better with those. Here is how it looks now

Screenshot 2026-07-29 at 3 58 37 PM Screenshot 2026-07-29 at 3 59 00 PM

As I was testing this more, found myself increasingly annoyed that selection wasn't consistent and expansion was super fragile to other edits because it shifts the prose mirror index of items.

Where this stands now is that if we merge adobe/da-nx#627 with this:

  • moving the cursor in layout or content view selects the appropriate outline item (expands group if needed)
  • click item in cursor scrolls item into view in both layout and content mode
  • on any structural change (move, delete, add) the full outline tree is rebuilt from scratch, meaning existing expansions go back to collapsed, but tries be intelligent about keeping the active group expanded

more feedback is welcome, but I think this is ready to go.

_scrollDocToProseIndex's dispatch synchronously triggers the tracking
plugin's onSelectionChange, which broadcasts a null node payload (only
image/table are recognized by selectedNodePayload) an instant before
the correct content payload overwrites it, briefly flashing the
layout-view highlight to nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@shsteimer
shsteimer merged commit 057e837 into main Jul 30, 2026
5 checks passed
@shsteimer
shsteimer deleted the outldflt branch July 30, 2026 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ew] add default content to outline

4 participants