Annotations Overlay — Developer Reference
Developer-focused reference. For module overview, permissions, and view-page opt-in see README.md.
Templates
Four theme hooks, split into two pairs by context.
Dialog overlays
| Hook | Template | Purpose |
|---|---|---|
annotations_overlay_wrapper |
annotations-overlay-wrapper.html.twig |
Outer <dialog> with heading, close button, optional overview section, items, and create links. Variables: heading, overview, items, create_links, attributes, close_attributes, close_label. |
annotations_overlay_item |
annotations-overlay-item.html.twig |
Single annotation type. Variables: type_id, type_label, content, edit_url, single_type. |
Chooser page descriptions
| Hook | Template | Purpose |
|---|---|---|
annotations_overlay_chooser |
annotations-overlay-chooser.html.twig |
Wraps the original bundle description and annotation items. Variables: description, items. |
annotations_overlay_chooser_item |
annotations-overlay-chooser-item.html.twig |
Single annotation type. Variables: type_id, type_label, content. |
Chooser hooks are separate from dialog hooks because their variable signatures differ — they cannot share annotations_overlay_wrapper without a breaking interface change.
JS architecture
annotations-overlay.js is a plain IIFE with no jQuery or Drupal.behaviors. All annotation content is server-rendered inside <dialog> elements at page load — no AJAX round-trips. Clicking a trigger calls showModal() on the matching <dialog data-annotations-field="...">. Event handling is delegated to document so dynamically injected dialogs (paragraph AJAX) work without re-initialization.
Bundle chooser pages
hook_preprocess_node_add_list and hook_preprocess_entity_add_list inject bundle-level annotation content when show_bundle_chooser_overview is enabled in module settings.
Claro/Gin quirk: Claro's theme-level preprocess rebuilds bundles from the entity description directly, bypassing module-set variables. The fix sets the rendered annotation content on the entity object in memory before Claro runs — the theme then reads the modified value. Request-scoped only; no entity save.
Chooser rendering: HTML is built by buildBundleAnnotationHtml() via direct string construction from the value base field rather than via the render pipeline. Calling renderInIsolation() from inside a preprocess hook nests a PHP Fiber inside Drupal's existing render Fiber, which overflows the stack on cold Twig cache. The render-pipeline-capable alternative (buildBundleAnnotationRenderItems()) exists but cannot be called from a preprocess hook for this reason. Edge case: if an annotation type stores content in a separate field (e.g. a wysiwyg), the chooser will show nothing for that type.
Paragraph support
Inline paragraphs: Dialogs go inside $form[$field_name]['widget'] (the Paragraphs field wrapper), not the global container, so they survive AJAX replacement when paragraphs are added. Field keys are prefixed para__{bundle}__ to avoid collision with parent-form fields. Detection is structural array inspection — no hard Paragraphs dependency.
Layout Paragraphs: Component edit dialogs have getParagraph() on the form object. Caught by elseif (method_exists($form_object, 'getParagraph')) in formAlter(); treated as standalone entity forms — dialogs go into the standard container.
field_group
field_group nests fields into group containers during the theme preprocess phase, after hook_entity_view_alter and hook_form_alter have already run. Trigger buttons added as top-level build siblings are left at the build root while their fields move into groups, breaking the CSS positioning that associates triggers with fields.
Bundle-level triggers and dialog containers are unaffected. The JS dialog lookup still works (the data-annotations-field attribute travels with the field into the group). Only the visual positioning of per-field triggers breaks.
A dedicated annotations_field_group submodule will fix this via hook_field_group_build_pre_render_alter (view) and hook_field_group_form_process_build_alter (form), which fire after nesting and allow trigger reparenting. The fix belongs in a separate module to avoid a hard field_group dependency here.
Parked / planned
- Empty-field "+" triggers on view pages — the form equivalent is shipped. The view equivalent needs a
show_empty_create_linksper-view-mode formatter setting inAnnotationsOverlayFormatter; the service already supports the$include_empty_fieldsflag so the hook change would be small. - Module split — planned split into
annotations_overlay_edit(form alter),annotations_overlay_view(view alter + Manage Display), andannotations_field_group(field_group bridge). See CLAUDE.md for detail. - Gin description toggle — inject annotation text into
#description+#description_toggle: TRUEto reuse Gin's reveal UX. Parked — ties module to Gin dependency. - Chooser view mode — dedicated
chooserannotation view mode separate fromoverlay. Requires resolving the preprocess-to-render-pipeline restriction first.