Skip to content

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_links per-view-mode formatter setting in AnnotationsOverlayFormatter; the service already supports the $include_empty_fields flag so the hook change would be small.
  • Module split — planned split into annotations_overlay_edit (form alter), annotations_overlay_view (view alter + Manage Display), and annotations_field_group (field_group bridge). See CLAUDE.md for detail.
  • Gin description toggle — inject annotation text into #description + #description_toggle: TRUE to reuse Gin's reveal UX. Parked — ties module to Gin dependency.
  • Chooser view mode — dedicated chooser annotation view mode separate from overlay. Requires resolving the preprocess-to-render-pipeline restriction first.