feat(renderer): do not open lightbox on images with links

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson
2026-07-27 17:41:44 +02:00
committed by Philip Molares
parent bc03f33fa5
commit 230f901323
2 changed files with 71 additions and 6 deletions
@@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { EventEmittingProxyImageFrame } from './event-emitting-proxy-image-frame'
import { ProxyImageFrame } from './proxy-image-frame'
import { ProxyImageReplacer } from './proxy-image-replacer'
import { Element } from 'domhandler'
import type { ReactElement } from 'react'
describe('ProxyImageReplacer', () => {
const replacer = new ProxyImageReplacer()
it('uses the lightbox image frame for standalone images', () => {
const image = new Element('img', { src: 'https://example.com/image.png' })
const replacement = replacer.replace(image) as ReactElement
expect(replacement.type).toBe(EventEmittingProxyImageFrame)
expect(replacement.props.className).toBe('cursor-zoom-in')
})
it('does not use the lightbox image frame for images inside links', () => {
const image = new Element('img', { src: 'https://example.com/image.png' })
const span = new Element('span', {}, [image])
const link = new Element('a', { href: 'https://example.com' }, [span])
image.parent = span
span.parent = link
const replacement = replacer.replace(image) as ReactElement
expect(replacement.type).toBe(ProxyImageFrame)
expect(replacement.props.className).toBe('')
})
})
@@ -6,22 +6,51 @@
import type { NodeReplacement } from '../../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
import { EventEmittingProxyImageFrame } from './event-emitting-proxy-image-frame'
import type { Element } from 'domhandler'
import { ProxyImageFrame } from './proxy-image-frame'
import { isTag, type Element } from 'domhandler'
import React from 'react'
import { concatCssClasses } from '../../../../utils/concat-css-classes'
export type ImageClickHandler = (event: React.MouseEvent<HTMLImageElement, MouseEvent>) => void
/**
* Traverses the DOM upwards to check if one parent element of the current one is a link.
* Stops when reaching the body element.
*
* @param node The node from which to start checking.
* @returns true if the given node has some link parent in the DOM, false otherwise
*/
const isDescendantOfLink = (node: Element): boolean => {
let parent = node.parent
while (parent) {
if (isTag(parent) && parent.name === 'a') {
return true
}
if (isTag(parent) && parent.name === 'body') {
return false
}
parent = parent.parent
}
return false
}
/**
* Detects image tags and loads them via image proxy if configured.
*/
export class ProxyImageReplacer extends ComponentReplacer {
public replace(node: Element): NodeReplacement {
return node.name !== 'img' ? (
DO_NOT_REPLACE
) : (
<EventEmittingProxyImageFrame
if (node.name !== 'img') {
return DO_NOT_REPLACE
}
const linkedImage = isDescendantOfLink(node)
const ImageFrame = linkedImage ? ProxyImageFrame : EventEmittingProxyImageFrame
return (
<ImageFrame
id={node.attribs.id}
className={`${node.attribs.class} cursor-zoom-in`}
className={concatCssClasses(node.attribs.class, {
'cursor-zoom-in': !linkedImage
})}
src={node.attribs.src}
alt={node.attribs.alt}
title={node.attribs.title}