fix: support hyperlinks on DrawingML images (a:hlinkClick)#2552
fix: support hyperlinks on DrawingML images (a:hlinkClick)#2552iguit0 wants to merge 3 commits intosuperdoc-dev:mainfrom
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7bea148896
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
|
||
| const anchor = this.doc.createElement('a'); | ||
| anchor.href = sanitized.href; | ||
| anchor.classList.add('superdoc-link'); |
There was a problem hiding this comment.
Preserve image selection for linked images in edit mode
Wrapping image elements with a.superdoc-link causes EditorInputManager.#handlePointerDown to take the link fast-path (closest('a.superdoc-link')) and return before the inline/block image selection paths run (#handleInlineImageClick / #handleFragmentClick). As a result, in editable documents, clicking a hyperlinked image no longer produces image node selection or resize-overlay activation, so linked images become effectively non-selectable by pointer.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
@iguit0 this is a good catch — clicking a linked image in edit mode opens the link instead of selecting the image. the link handler runs first and skips the image selection path entirely.
caio-pizzol
left a comment
There was a problem hiding this comment.
@iguit0 nice contribution, thanks for tackling this! the approach looks good — checked the spec and the dual placement, relationship handling, and tooltip are all done right.
one small thing on tooltip encoding (left a suggestion). one optional cleanup on shared code.
tests: block-level is well covered. the inline paths (imageNodeToRun and the 3 renderer return paths) don't have hyperlink tests yet — would be great to add those.
left a few inline comments.
|
Nice work on this. I independently implemented the same fix on my fork while digging through #2065, and I converged on the same root cause. One thing I found useful was reusing the existing shared link pipeline ( / / ) rather than introducing an image-specific hyperlink path, since it automatically inherits the current blocked-link handling, dataset propagation, tooltip accessibility, and other shared link behavior. I also added explicit inline-image coverage in both the PM adapter and DOM painter tests, since the inline paths were the main gap I noticed while reviewing. If any of that is helpful, happy to share or compare against my branch here: https://github.com/PeterHollens/superdoc/tree/feature/drawingml-image-hyperlinks |
|
Nice work on this. I independently implemented the same fix on my fork while One thing I found useful was reusing the existing shared link pipeline If any of that is helpful, happy to share or compare against my branch here: |
I diffed your branch against my implementation and we converged on the same root cause, but your version is cleaner. Reusing the shared link pipeline ( You were also right about the inline gap. That was the missing case. The fix gets a lot more solid once you add explicit PM adapter and DOM painter coverage for the inline-image path. This is my first issue, so I'm not totally sure what's the best approach here 🤔 |
|
Hey @iguit0, this PR has had no activity for 8 days. |
e198d13 to
eb51d4f
Compare
Hey! Apologies for the delay. I have now resolved the merge conflicts and incorporated your suggestions. Please let me know if anything else needs adjustment. |
caio-pizzol
left a comment
There was a problem hiding this comment.
@iguit0 prior feedback is all addressed, nice.
one thing: linked images work in viewing mode but not in editing mode — you can select them but can't follow the link. left an inline comment with details.
rendering looks good, tested with a Word file with different hyperlink setups and everything renders correctly.
| * is returned unchanged. | ||
| * | ||
| * @param imageEl - The image element (img or span wrapper) to potentially wrap. | ||
| * @param hyperlink - Hyperlink metadata from the ImageBlock/ImageRun, or undefined. |
There was a problem hiding this comment.
editing mode: clicking a linked image selects it (resize handles show up) but doesn't open the link. ctrl+click doesn't work either (in Word ctrl+click or just click will open img URL straight away - no resize selection appears)

tooltip: shows "Image" instead of the actual tooltip URL (e.g. "https://superdoc.dev").

There was a problem hiding this comment.
The editing mode issue was happening because linked images were still going through the text-link popover flow, which only knows how to resolve ProseMirror link marks. Image hyperlinks are stored on the image node attrs instead, so the click ended up selecting the image rather than following the link.
I changed that path so linked images open/navigate directly in editing mode, same as viewing mode.
The tooltip issue was happening because the wrapped image was still carrying its own title (often "Image"), which could win over the link tooltip on hover. I changed linked image rendering to prefer the hyperlink tooltip, fall back to the hyperlink URL when no tooltip is set, and clear the nested image title so the correct text is shown.
Added tests for:
- linked images opening directly in editing mode
- linked image anchor navigation in editing mode
- inline linked images using hyperlink tooltip/URL instead of the image title
Also verified manually with the repro doc: click now follows the image link in editing mode, no resize selection appears on that click, and the hover tooltip is correct.
Summary
DOCX images with
a:hlinkClickhyperlinks rendered correctly in Word but clicking them in SuperDoc did nothing. The importer already parsed the relationship and storedattrs.hyperlinkon the ProseMirror image node — but the hyperlink was silently dropped further down the pipeline, so the renderer never had a URL to render.Root cause: a three-step gap between import and rendering:
ImageBlock/ImageRuncontract types had nohyperlinkfieldimageNodeToBlockandimageNodeToRundid not forward the attrDomPainter.renderImageFragment/renderImageRunnever created an anchorWhat Changed
hyperlink?: { url: string; tooltip?: string }to bothImageBlockandImageRunin the shared contracts packageimageNodeToBlockandimageNodeToRun, with the same validation (non-empty string URL) used for other optional fieldsDomPainter.buildImageHyperlinkAnchor()— a private helper that wraps an image element in<a class="superdoc-link">when a valid hyperlink is present. UsessanitizeHref(same guard as text links) and appliestarget/relfor external URLs with accessibility attributes mirroringapplyLinkAttributesrenderImageFragment(block images) and at all three return points ofrenderImageRun(inline images with/without clip-path wrapper)The existing
EditorInputManagerclick-delegation already detectsa.superdoc-linkon pointer-down and dispatchessuperdoc-link-click.LinkClickHandlerthen opens the URL in viewing mode — no changes needed in either component. Unlinked images are unaffected:buildImageHyperlinkAnchoris a no-op whenhyperlinkis undefined, and image resize/selection behavior is preserved because the fragment<div>(which carriesdata-image-metadata) remains the outermost element.Test Plan
pm-adapter/src/converters/image.test.ts— verifieshyperlinkis forwarded with tooltip, without tooltip, and is omitted for null/empty casespainters/dom/src/index.test.ts— verifies linked images render as<a class="superdoc-link">with correcthref, tooltip becomestitle, unlinked images have no anchor, and unsafe URLs (e.g.javascript:) are blockedFixes #2065