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
93 changes: 93 additions & 0 deletions app/modules/niklan/assets/js/footnote.tooltip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// cspell:ignore UIDOM
((Drupal, once, { computePosition, offset, shift, flip, inline }) => {

const createTooltipElement = (content) => {
const tooltip = document.createElement('div');
tooltip.innerHTML = Drupal.theme.niklanFootnoteTooltipWrapper(content);
return tooltip.firstElementChild;
};

const setupTooltipBehavior = (footnoteLink, context) => {
const href = footnoteLink.getAttribute('href');
const id = href.startsWith('#') ? href.substring(1) : href;
const footnoteContent = context.querySelector(`#${CSS.escape(id)}`);

if (!footnoteContent) return null;

let tooltip = null;
let hideTimeout = null;

const cancelScheduledHide = () => {
clearTimeout(hideTimeout);
hideTimeout = null;
};

const scheduleHide = () => {
hideTimeout = setTimeout(() => {
tooltip?.style.setProperty('visibility', 'hidden');
tooltip?.remove();
tooltip = null;
}, 300);
};

const setupEventListeners = (element) => {
element.addEventListener('mouseenter', cancelScheduledHide);
element.addEventListener('mouseleave', scheduleHide);
};

const updatePosition = async () => {
if (!tooltip) {
tooltip = createTooltipElement(footnoteContent);
document.body.appendChild(tooltip);
setupEventListeners(tooltip);
}

try {
const { x, y } = await computePosition(footnoteLink, tooltip, {
strategy: 'absolute',
placement: 'top',
middleware: [inline(), flip({ padding: 16 }), offset(16), shift({ padding: 16 })]
});

Object.assign(tooltip.style, {
left: `${x}px`,
top: `${y}px`,
visibility: 'visible'
});
} catch (error) {
console.error('Tooltip positioning failed:', error);
}
};

const handleLinkEnter = () => {
cancelScheduledHide();
updatePosition();
};

footnoteLink.addEventListener('mouseenter', handleLinkEnter);
footnoteLink.addEventListener('mouseleave', scheduleHide);

return () => {
footnoteLink.removeEventListener('mouseenter', handleLinkEnter);
footnoteLink.removeEventListener('mouseleave', scheduleHide);
tooltip?.remove();
};
};

Drupal.theme.niklanFootnoteTooltipWrapper = (content) => `
<div class="tooltip tooltip--type--footnote">
<div class="prose tooltip__content">${content.innerHTML}</div>
</div>
`;

Drupal.behaviors.niklanFootnoteTooltip = {
attach: (context) => {
const callback = () => once('niklan-footnote-tooltip', '.footnote-ref', context)
.forEach(link => setupTooltipBehavior(link, context));

// eslint-disable-next-line no-unused-expressions
window.requestIdleCallback ? requestIdleCallback(callback) : callback();
}
};

})(Drupal, once, FloatingUIDOM);
9 changes: 9 additions & 0 deletions app/modules/niklan/niklan.libraries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,12 @@ content-editing.toolbar:
css:
theme:
assets/css/content-editing.toolbar.css: { }

footnote.tooltip:
js:
assets/js/footnote.tooltip.js: { }
dependencies:
- core/drupal
- core/once
# Intentional, to avoid JS conflicts.
- core/internal.floating-ui
1 change: 1 addition & 0 deletions app/modules/niklan/niklan.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ services:
Drupal\niklan\ExternalContent\Nodes\Callout\RenderArrayBuilder: {}
Drupal\niklan\ExternalContent\Nodes\CodeBlock\RenderArrayBuilder: {}
Drupal\niklan\ExternalContent\Nodes\MediaReference\RenderArrayBuilder: {}
Drupal\niklan\ExternalContent\Nodes\Footnote\RenderArrayBuilder: {}

# External Content: Extensions.
Drupal\niklan\ExternalContent\Extension\RenderArrayBuilderExtension: {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Drupal\niklan\ExternalContent\Nodes\CodeBlock\RenderArrayBuilder as CodeBlockBuilder;
use Drupal\niklan\ExternalContent\Nodes\Figcaption\RenderArrayBuilder as FigcaptionBuilder;
use Drupal\niklan\ExternalContent\Nodes\Figure\RenderArrayBuilder as FigureBuilder;
use Drupal\niklan\ExternalContent\Nodes\Footnote\RenderArrayBuilder as FootnoteBuilder;
use Drupal\niklan\ExternalContent\Nodes\MediaReference\RenderArrayBuilder as MediaReferenceBuilder;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

Expand All @@ -31,6 +32,7 @@ public function __construct(
private CalloutBuilder $calloutBuilder,
private MediaReferenceBuilder $mediaReferenceBuilder,
private ArticleLinkBuilder $articleLinkBuilder,
private FootnoteBuilder $footnoteBuilder,
) {}

public function register(object $target): void {
Expand All @@ -40,6 +42,7 @@ public function register(object $target): void {
$target->add($this->articleLinkBuilder, 10);
$target->add(new FigureBuilder());
$target->add(new FigcaptionBuilder());
$target->add($this->footnoteBuilder);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Drupal\niklan\ExternalContent\Nodes\Footnote;

use Drupal\Core\Render\Element\HtmlTag;
use Drupal\external_content\Contract\Builder\RenderArray\Builder;
use Drupal\external_content\Contract\Builder\RenderArray\ChildBuilder;
use Drupal\external_content\DataStructure\RenderArray;
use Drupal\external_content\Nodes\HtmlElement\HtmlElement;
use Drupal\external_content\Nodes\Node;
use Drupal\external_content\Utils\HtmlTagHelper;

/**
* @implements \Drupal\external_content\Contract\Builder\RenderArray\Builder<\Drupal\external_content\Nodes\HtmlElement\HtmlElement>
*/
final readonly class RenderArrayBuilder implements Builder {

public function supports(Node $node): bool {
return $node instanceof HtmlElement
&& isset($node->attributes['class'])
&& \str_contains($node->attributes['class'], 'footnote-ref');
}

public function buildElement(Node $node, ChildBuilder $child_builder): RenderArray {
$element = new RenderArray([
'#type' => 'html_tag',
'#tag' => $node->tag,
'#attributes' => $node->attributes,
'#pre_render' => [
HtmlTag::preRenderHtmlTag(...),
HtmlTagHelper::preRenderTag(...),
],
'#attached' => [
'library' => [
'niklan/footnote.tooltip',
],
],
]);
$child_builder->buildChildren($node, $element);
return $element;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ public function configureSchema(ConfigurationBuilderInterface $builder): void {
// Note that soft break must be a space, or words will be merged.
'soft_break' => ' ',
]);

// Default prefixes are not valid CSS IDs (with ':' char).
$builder->set('footnote', [
'ref_id_prefix' => 'fn-ref-',
'footnote_id_prefix' => 'fn-',
]);
}

}
1 change: 1 addition & 0 deletions app/themes/laszlo/assets/css/02-element/code.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@
word-break: break-word;
border-radius: var(--spacing-1);
background: var(--color-surface-variant);
font-size: 0.8em;
}
}
1 change: 1 addition & 0 deletions app/themes/laszlo/assets/css/02-element/html.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@layer element {
html {
font-size: 16px;
scroll-behavior: smooth;
}
}
9 changes: 8 additions & 1 deletion app/themes/laszlo/assets/css/03-component/ui/prose.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,23 @@
--link-color: var(--color-on-secondary-container);

padding: var(--spacing-1) var(--spacing-2);
transition: all ease-in-out 0.1s;
text-decoration: none;
border-radius: var(--spacing-4);
background-color: var(--color-secondary-container);
font: var(--typography-label-small);

&:hover {
--link-color: var(--color-on-secondary);

background-color: var(--color-secondary);
}
}

.prose .footnotes {
--link-text-decoration-thickness: 1px;

font: var(--typography-body-medium);
font: var(--typography-body-small);
}

.prose .footnotes hr {
Expand Down
21 changes: 21 additions & 0 deletions app/themes/laszlo/assets/css/03-component/ui/tooltip.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.tooltip {
--link-text-decoration-thickness: 1px;

position: absolute;
width: 100%;
max-width: 360px;
padding: var(--spacing-4);
color: var(--color-on-background);
border: 1px solid var(--color-outline-variant);
border-radius: var(--spacing-2);
background: color-mix(in sRGB, var(--color-background), transparent);
box-shadow:
rgb(0 0 0 / 0.1) 0 20px 25px -5px,
rgb(0 0 0 / 0.04) 0 10px 10px -5px;
font: var(--typography-body-medium);
backdrop-filter: blur(20px);
}

.tooltip .footnote-backref {
display: none;
}
2 changes: 2 additions & 0 deletions app/themes/laszlo/laszlo.info.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ libraries-override:
libraries-extend:
niklan/hljs:
- laszlo/hljs
niklan/footnote.tooltip:
- laszlo/tooltip
core/drupal.message: false
7 changes: 6 additions & 1 deletion app/themes/laszlo/laszlo.libraries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,9 @@ navigation-active-trail:
comment-comment-node-blog-entry-form:
css:
theme:
assets/css/03-component/form/comment-comment-node-blog-entry-form.css: { }
assets/css/03-component/form/comment-comment-node-blog-entry-form.css: { }

tooltip:
css:
theme:
assets/css/03-component/ui/tooltip.css: { }