Dashboard Header & Compact Buttons

Reference documentation for the TButtonContainer and THeaderButton components used in dashboard card headers.

Contents

Overview

The dashboard header system provides two components for managing card header buttons:

Location: Framework/Components/Dashboard/SOURCE/Obj.Dashboard.Header.pas

Header Styles

The header supports two display modes controlled by THeaderStyle:

StyleDescription
hsLegacyTraditional icon-only buttons (map, chart, calendar icons, etc.)
hsCompactActionsText buttons in the middle (Chart, Raw Data, Legend) with maximize and 3-dot menu
/// Set header style on the canvas (propagates to all cards)
Canvas.HeaderStyle := hsCompactActions;

/// Or set on individual cards
Card.HeaderButtons.HeaderStyle := hsCompactActions;

Button Types

View Mode Buttons (Mutually Exclusive)

These buttons represent the current view mode - only one can be active at a time:

Momentary Action Buttons

These buttons trigger an action but don't stay selected:

Button State Logic

/// In OnCompactButtonClick:
IF Action = mtLegend THEN
   BEGIN
      /// Legend is momentary - don't change button states
      /// Just fire the action
   END
ELSE
   BEGIN
      /// Chart/Map and Raw Data are mutually exclusive
      /// Clear both, then set active on clicked button
      FbtnCompactConfigure.ElementHandle.classList.remove('active');
      FbtnCompactRawData.ElementHandle.classList.remove('active');
      ClickedButton.ElementHandle.classList.add('active');
   END;

Compact Buttons System

In hsCompactActions mode, text-based pill buttons are displayed in the header center:

Button Structure

CSS Classes

.compact-buttons-container {
   display: flex;
   align-items: center;
   gap: 2px;
   justify-content: center;
   margin-left: auto;
   margin-right: auto;
}

.compact-action-btn {
   /* Pill-shaped button styling */
}

.compact-action-btn.active {
   /* Active/selected state */
   background: #0ea5e9;
   color: white;
}

Visibility Based on Card Width

Compact buttons are only shown when the card is wide enough (default: 450px minimum):

FMinWidthForButtons := 450;  /// Minimum card width to show compact buttons

/// Buttons hidden on narrow cards, shown on wider cards
IF CardWidth >= FMinWidthForButtons THEN
   FCompactButtonsDiv.ElementHandle.style.setProperty('display', 'flex', 'important')
ELSE
   FCompactButtonsDiv.ElementHandle.style.setProperty('display', 'none', 'important');

ResizeObserver Integration

Important: Never use timers for detecting element size changes. ResizeObserver is the only reliable method because CSS grid/flexbox recalculation timing is unpredictable.

External Class Declaration

ResizeObserver requires {$MODESWITCH EXTERNALCLASS} at the unit top:

UNIT Obj.Dashboard.Header;

{$IFDEF PAS2JS}
{$MODESWITCH EXTERNALCLASS}
{$ENDIF}

INTERFACE
...

TYPE
   {$IFDEF PAS2JS}
   TJSResizeObserverEntry = CLASS external name 'ResizeObserverEntry' (TJSObject)
      PUBLIC
         target: TJSElement; external name 'target';
         contentRect: TJSObject; external name 'contentRect';
   END;
   
   TJSResizeObserverEntryArray = ARRAY OF TJSResizeObserverEntry;
   
   TJSResizeObserver = CLASS external name 'ResizeObserver' (TJSObject)
      PUBLIC
         CONSTRUCTOR new(callback: JSValue);
         PROCEDURE observe(target: TJSElement);
         PROCEDURE unobserve(target: TJSElement);
         PROCEDURE disconnect;
   END;
   {$ENDIF}

Setup and Callback

PROCEDURE TButtonContainer.SetupResizeObserver;
{$IFDEF PAS2JS}
VAR
   CardElement: TJSElement;
{$ENDIF}
BEGIN
   {$IFDEF PAS2JS}
   IF Assigned(FResizeObserver) THEN
      Exit;
      
   CardElement := TJSElement(FCompactButtonsDiv.ElementHandle.closest('.dashboard-card'));
   IF NOT Assigned(CardElement) THEN
      Exit;
   
   /// Create observer with method pointer callback
   FResizeObserver := TJSResizeObserver.new(@OnResizeObserverCallback);
   FResizeObserver.observe(CardElement);
   {$ENDIF}
END;

{$IFDEF PAS2JS}
PROCEDURE TButtonContainer.OnResizeObserverCallback(
   AEntries: TJSResizeObserverEntryArray; 
   AObserver: TJSObject);
VAR
   Entry: TJSResizeObserverEntry;
   CardWidth: Integer;
   ShowButtons: Boolean;
BEGIN
   IF Length(AEntries) = 0 THEN
      Exit;
      
   Entry := AEntries[0];
   /// Access width from contentRect object
   CardWidth := Trunc(Double(Entry.contentRect.Properties['width']));
   ShowButtons := CardWidth >= FMinWidthForButtons;
   
   IF ShowButtons THEN
      FCompactButtonsDiv.ElementHandle.style.setProperty('display', 'flex', 'important')
   ELSE
      FCompactButtonsDiv.ElementHandle.style.setProperty('display', 'none', 'important');
END;
{$ENDIF}

Cleanup in Destructor

DESTRUCTOR TButtonContainer.Destroy;
BEGIN
   {$IFDEF PAS2JS}
   IF Assigned(FResizeObserver) THEN
      BEGIN
         FResizeObserver.disconnect;
         FResizeObserver := NIL;
      END;
   {$ENDIF}
   INHERITED;
END;

Actions Menu

The 3-dot button opens a dropdown menu with categorized actions:

Menu Sections

Menu Positioning

The menu is appended to document.body to avoid overflow clipping:

/// Move to body so it's not clipped by parent overflow
BodyElement := TJSHTMLElement(document.body);
BodyElement.appendChild(FActionsMenu.ElementHandle);

/// Position below the button
ButtonRect := TJSHTMLElement(btnActions.ElementHandle).getBoundingClientRect;
FActionsMenu.ElementHandle.style.setProperty('top', IntToStr(Trunc(ButtonRect.bottom) + 4) + 'px');
FActionsMenu.ElementHandle.style.setProperty('left', IntToStr(Trunc(ButtonRect.right) - 160) + 'px');

Global Close Handler

Clicking outside the menu closes it:

PROCEDURE TButtonContainer.DocumentClickHandler(Event: TJSEvent);
VAR
   TargetElement: TJSHTMLElement;
BEGIN
   TargetElement := TJSHTMLElement(Event.target);

   /// Check if click is inside menu or on actions button
   IF FActionsMenu.ElementHandle.contains(TargetElement) THEN
      Exit;
   IF btnActions.ElementHandle.contains(TargetElement) THEN
      Exit;

   /// Click was outside - hide menu
   HideActionsMenu;
END;

Usage Examples

Setting Header Style on Canvas

/// In canvas initialization
Canvas := TDashboardCanvas.Create(Self);
Canvas.HeaderStyle := hsCompactActions;  /// Use compact text buttons

Handling Button Actions

/// OnHeaderButtonClick handler receives the action
PROCEDURE TDashboardCard.HandleHeaderButtonClick(
   Sender: TObject; 
   Action: TMessageType; 
   Payload: TCardPayload);
BEGIN
   CASE Action OF
      mtChartButtonClick:
         ShowChartView;
      mtMapButtonClick:
         ShowMapView;
      mtCardShowGrid:
         ShowRawDataGrid;
      mtLegend:
         ToggleLegendVisibility;
      mtCanvasMaximize:
         MaximizeCard;
      mtCanvasMinimize:
         RestoreCard;
   END;
END;

Preventing Pressed State on Action Buttons

/// In THeaderButton.DoOnClick - skip state change for action buttons
IF NOT (Action IN [mtCanvasMaximize, mtCanvasMinimize, mtActionsMenu,
                   mtCanvasExpandRight, mtCanvasExpandDown, mtCanvasExpandBoth,
                   mtCanvasShrinkLeft, mtCanvasShrinkUp, mtCanvasShrinkBoth,
                   mtCardDelete, mtAdjustLayout]) THEN
   ButtonContainer.SetButtonGroupStatus(Self);