Popups
The popup control opens a modal dialog on a native <dialog> element, usually with markup returned by an AJAX handler. It is styled with Bootstrap 6's dialog classes and needs no Bootstrap JavaScript. Popovers open a panel beside an element, and tooltips show a short hint on hover.
Popup button
Ui::popupButton() renders a button that runs its handler and opens the response in a dialog. The dataSize attribute sets the width, from tiny to giant (every size is listed under Sizes), and requestData sends extra values to the handler, here the size, so the dialog can name it in its title.
{{ Ui::popupButton(label: 'Open a dialog', handler: 'onLoadDialog', primary: true) }}
{{ Ui::popupButton(label: 'Tiny', handler: 'onLoadDialog', secondary: true, requestData: ['size' => 'tiny'], dataSize: 'tiny') }}
{{ Ui::popupButton(label: 'Small', handler: 'onLoadDialog', secondary: true, requestData: ['size' => 'small'], dataSize: 'small') }}
{{ Ui::popupButton(label: 'Large', handler: 'onLoadDialog', secondary: true, requestData: ['size' => 'large'], dataSize: 'large') }}
{{ Ui::popupButton(label: 'Huge', handler: 'onLoadDialog', secondary: true, requestData: ['size' => 'huge'], dataSize: 'huge') }}
{{ Ui::popupButton(label: 'Giant', handler: 'onLoadDialog', secondary: true, requestData: ['size' => 'giant'], dataSize: 'giant') }}The handler returns the dialog markup, rendered from a view:
<?php
namespace App\Controllers\Reference\UiController;
use Illuminate\Support\Sleep;
use Larajax\Classes\AjaxResponse;
/**
* PopupHandlers holds the AJAX handlers behind the Popups page.
*/
trait PopupHandlers
{
/**
* onLoadDialog renders the demo dialog, titled with the size the popup button asked for.
*/
public function onLoadDialog(): string
{
$size = (string) input('size');
if (ctype_digit($size)) {
$title = "{$size}px dialog";
}
elseif (in_array($size, $this->getDialogSizes(), true)) {
$title = ucfirst($size).' dialog';
}
else {
$title = 'Default dialog';
}
return view('reference.ui._dialog', ['title' => $title])->render();
}
/**
* onSubmitDialog handles the dialog's submit button, which closes the popup when it succeeds.
*/
public function onSubmitDialog(): AjaxResponse
{
Sleep::for(500)->milliseconds();
return ajax()->flash('success', 'The dialog was submitted.');
}
/**
* getDialogSizes returns the named popup sizes the dialog demo accepts.
*
* @return array<int, string>
*/
protected function getDialogSizes(): array
{
return ['tiny', 'small', 'large', 'huge', 'giant', 'adaptive', 'auto', 'fullscreen'];
}
}<form data-request="onSubmitDialog" data-request-flash data-popup-load-indicator>
<div class="dialog-header">
<h4 class="dialog-title">
{{ $title }}
</h4>
<button type="button" class="btn-close" data-dismiss="popup" aria-label="Close"></button>
</div>
<div class="dialog-body">
<p>
The handler returned this markup. The button's <code>dataSize</code> sets the width, and Submit hides the dialog behind a loading indicator until its own request succeeds.
</p>
<div class="form-field mb-5">
<label class="form-label" for="uiDialogNote">
Note
</label>
<input type="text" class="form-control" id="uiDialogNote" name="note" placeholder="Anything you like">
</div>
</div>
<div class="dialog-footer">
<button type="submit" class="btn-solid theme-primary">
Submit
</button>
<button type="button" class="btn-solid theme-secondary" data-dismiss="popup">
Cancel
</button>
{{ Ui::popupButton(
label: 'Open another',
handler: 'onLoadDialog',
class: 'btn-link ms-auto',
requestData: ['size' => 'small'],
dataSize: 'small'
) }}
</div>
</form>The markup goes straight inside the <dialog>, in three parts: dialog-header with a dialog-title and a close button, dialog-body, and dialog-footer with the actions. A dialog-body with no header or footer can add dialog-no-header or dialog-no-footer for even padding. Any element with data-dismiss="popup" closes the popup when clicked.
Wrapping the parts in a form lets its buttons submit everything in the dialog. A request element with data-popup-load-indicator, here the form with its data-request, hides the popup behind a loading indicator while its request runs, then closes the popup when the request succeeds or shows it again with a shake when it fails.
A popup opened from inside another stacks over it, and the one below steps back until the top one closes, as the dialog's Open another button shows. While a handler's content loads, a backdrop with a loading indicator covers the page.
Sizes from the window and content
Besides the fixed widths, a size can follow the window or the content. adaptive fills the window less 100 pixels (the whole width on small screens), auto fits the content, a number sets the width in pixels and fullscreen covers the window. dataAdaptiveHeight stretches any size to the window height, less 100 pixels.
{{ Ui::popupButton(label: 'Adaptive', handler: 'onLoadDialog', secondary: true, requestData: ['size' => 'adaptive'], dataSize: 'adaptive') }}
{{ Ui::popupButton(
label: 'Adaptive height',
handler: 'onLoadDialog',
secondary: true,
requestData: ['size' => 'large'],
dataSize: 'large',
dataAdaptiveHeight: true
) }}
{{ Ui::popupButton(label: 'Fits the content', handler: 'onLoadDialog', secondary: true, requestData: ['size' => 'auto'], dataSize: 'auto') }}
{{ Ui::popupButton(label: '600 pixels', handler: 'onLoadDialog', secondary: true, requestData: ['size' => '600'], dataSize: '600') }}
{{ Ui::popupButton(label: 'Fullscreen', handler: 'onLoadDialog', secondary: true, requestData: ['size' => 'fullscreen'], dataSize: 'fullscreen') }}| Size | Width |
|---|---|
| (none) | 598px. |
| tiny | 300px. |
| small | 400px. |
| large | 750px. |
| huge | 900px. |
| giant | 982px. |
| adaptive | the window width less 100px, and the full width on small screens. |
| auto | fits the content. |
| fullscreen | covers the window. |
| a number, such as 600 | that many pixels. |
The handler can also set the size from its response, with a data-popup-size attribute on the first element of the markup or any element inside it. Every size adds a size-* class to the dialog, for your own styles.
Closing
Escape closes a popup, and a click on the backdrop shakes it, so a stray click does not lose what the visitor typed. dataKeyboard: 'false' makes Escape shake it too, and dataBackdrop: true lets a backdrop click close it. The close button and any other data-dismiss="popup" element always close it.
{{ Ui::popupButton(
label: 'Escape does not close',
handler: 'onLoadDialog',
secondary: true,
dataKeyboard: 'false'
) }}
{{ Ui::popupButton(
label: 'Backdrop click closes',
handler: 'onLoadDialog',
secondary: true,
dataBackdrop: true
) }}To close a popup from JavaScript, dispatch a bubbling popup:close event on the popup or any element inside it, such as new CustomEvent('popup:close', { bubbles: true }).
Popup control in markup
data-control="popup" works on any element, which then opens the popup when clicked. This is what Ui::popupButton() renders. data-handler names the handler, and data-request-data sends extra values with it, written as key: value pairs. Every option has a data attribute, listed under Popup options.
<a
href="javascript:;"
data-control="popup"
data-handler="onLoadDialog"
data-request-data="size: 'small'"
data-size="small">
Open a small dialog from a link
</a>Popups from JavaScript
jax.popup.open() takes the same options as the attributes, in camel case, such as handler, extraData and size. content places markup in the dialog without a request, here copied from a <template> already on the page.
<button type="button" class="btn-solid theme-secondary" id="uiTemplatePopupButton">
Open from a template
</button>
<template id="uiTemplatePopup">
<div class="dialog-header">
<h4 class="dialog-title">
From a template
</h4>
<button type="button" class="btn-close" data-dismiss="popup" aria-label="Close"></button>
</div>
<div class="dialog-body dialog-no-footer">
<p class="mb-0">
This content was already on the page, so no request ran.
</p>
</div>
</template>
<script>
document.getElementById('uiTemplatePopupButton').addEventListener('click', () => {
jax.popup.open({
content: document.getElementById('uiTemplatePopup').innerHTML,
size: 'small'
});
});
</script>jax.popup is the Popup class, also exported by the library's index.js for code that imports it. Its static methods:
| Method | Description |
|---|---|
| open(options) | opens a popup with no trigger element and returns it. |
| fromElement(element, options) | opens the popup for a trigger element, or reloads it when it is open. Given a method name in place of the options, it calls that method on the popup: fromElement(element, 'lock', true). |
| getInstance(element) | returns the popup for its trigger, its <dialog> or the first element of its content, or null. |
| bindToPopups(container, vars) | adds the variables as hidden inputs to the form of any popup opened from inside the container, and from popups opened from that popup. |
| focusFirstInput(container) | focuses the first form-control input, unless an element has autofocus. |
A popup instance has show(), hide(), reload(), toggle(), lock(true) to stop it closing, setLoading(true) and setShake().
Popover
data-control="popover" opens a panel next to the element when it is clicked, and a second click closes it. Only one popover is open at a time. data-content-from copies the content from an element on the page, such as a <template>, data-placement picks the side, data-width fixes the width in pixels, and data-modal dims the page behind it.
<button type="button" class="btn-solid theme-secondary" data-control="popover" data-content-from="#uiShippingPopover" data-placement="top">
Top
</button>
<button type="button" class="btn-solid theme-secondary" data-control="popover" data-content-from="#uiShippingPopover" data-placement="bottom">
Bottom
</button>
<button type="button" class="btn-solid theme-secondary" data-control="popover" data-content-from="#uiShippingPopover" data-placement="left">
Left
</button>
<button type="button" class="btn-solid theme-secondary" data-control="popover" data-content-from="#uiShippingPopover" data-placement="right">
Right
</button>
<button type="button" class="btn-solid theme-primary" data-control="popover" data-content-from="#uiShippingPopover" data-modal="true" data-width="320">
Modal, 320px wide
</button>
<template id="uiShippingPopover">
<div class="popover-head">
<h3>
Shipping estimate
</h3>
<p>
From the main warehouse
</p>
<button type="button" class="btn-close" data-dismiss="popover" aria-label="Close"></button>
</div>
<div class="popover-body">
Standard delivery takes 3 to 5 business days.
</div>
</template>The content can use a popover-head, with a heading, a line of text and a close button, and a popover-body. Any element with data-dismiss="popover" inside it closes the popover, and so does a click outside it or the Escape key. When the preferred side has no room, the popover moves to another side that fits.
| Option | Description |
|---|---|
| placement | top, bottom, left, right or center, the middle of the window. Default: bottom. |
| fallbackPlacement | the side used when no side fits. Default: bottom. |
| content | the content as HTML, without a template. |
| contentFrom | a selector of an element whose HTML becomes the content. |
| width | the width in pixels. Default: the width of the content. |
| modal | dims the page behind the popover. Default: false. |
| highlightModalTarget | raises the trigger above the dimmed page. Default: false. |
| closeOnPageClick | closes the popover on a click outside it. Default: true. |
| closeOnEsc | closes the popover on Escape. Default: true. |
| containerClass | extra classes for the popover element. |
| offset | how far the popover is shifted along the trigger, in pixels. Default: 15. |
| offsetX | the offset for the top and bottom sides, in place of offset. |
| offsetY | the offset for the left and right sides, in place of offset. |
| container | an element or selector to mount the popover in. Default: the enclosing <dialog>, or else the body. |
| useAnimation | fades the popover in and out. Default: false. |
| onClose | a function called after the popover is removed, from JavaScript only. |
| onCheckDocumentClickTarget | a function given the target of a click outside the popover, which keeps it open by returning true, from JavaScript only. |
Set each option as a data attribute in kebab case, such as data-content-from or data-close-on-page-click="false". The Popover class, exported by the library's index.js, takes the same options from JavaScript: new Popover(trigger, options).show(), with setContent(html) and hide().
The popover dispatches popover:showing before it opens and popover:hiding before it closes, where preventDefault() cancels either, then popover:show and popover:hide, all on the trigger. Dispatch popover:close on any element inside it to close it.
Tooltip
Any element with data-tooltip-text shows a tooltip after 300 milliseconds of hovering, below it unless data-tooltip-position="top". It hides when the pointer leaves, on any click or key press, and before an AJAX update. The control listens on the document, so elements added later need no setup.
data-tooltip-hotkey adds key labels after the text, separated by commas. This is how buttons with a hotkey show their shortcut, since they set all three attributes.
<button
type="button"
class="btn-solid theme-secondary"
data-tooltip-text="Shown below by default">
Below
</button>
<button
type="button"
class="btn-solid theme-secondary"
data-tooltip-text="Shown above"
data-tooltip-position="top">
Above
</button>
<button
type="button"
class="btn-solid theme-secondary"
data-tooltip-text="Copy the link"
data-tooltip-hotkey="⌃C, ⌘C"
data-tooltip-position="top">
With hotkey labels
</button>| Attribute | Description |
|---|---|
| data-tooltip-text | the tooltip text. |
| data-tooltip-hotkey | key labels shown after the text, separated by commas, such as ⌃S, ⌘S. |
| data-tooltip-position | top places the tooltip above the element. Default: below. |
jax.tooltips.clear() hides the tooltip and cancels one that is waiting to show. Inside a popup, the tooltip mounts in the dialog so it shows above the backdrop. The tooltips on form fields and list columns are a different one: Bootstrap's own tooltip, with data-bs-toggle="tooltip".
Popup options
Set these as data attributes on a data-control="popup" element, as attributes of Ui::popupButton(), or as options of jax.popup.open().
| Attribute | Option | Description |
|---|---|---|
| data-handler | handler | the AJAX handler that returns the dialog markup. |
| data-ajax | ajax | a URL to load the dialog markup from, in place of a handler. |
| (none) | content | the dialog markup as an HTML string, an element or a function, with no request. |
| data-extra-data | extraData | extra request data, as key: value pairs or an object. The trigger's data-request-data is merged in. |
| data-size | size | the width, from the sizes table. |
| data-adaptive-height | adaptiveHeight | stretches the popup to the window height. Default: false. |
| data-keyboard | keyboard | false makes Escape shake the popup in place of closing it. Default: true. |
| data-backdrop | backdrop | what a backdrop click does: static shakes the popup, true closes it and false ignores the click. Default: static. |
Inside the dialog:
| Attribute | Description |
|---|---|
| data-dismiss="popup" | closes the popup when clicked. |
| data-popup-load-indicator | on an element with data-request, hides the popup behind a loading indicator while that request runs, closes it on success and shakes it on failure. |
| data-popup-size | on the first element of the markup, or any element in it, sets the popup size. |
Popup events
The popup dispatches these on its trigger and on the first element of its content. event.detail holds popup, the instance, trigger, and modal, the <dialog> element.
| Event | Description |
|---|---|
| popup:show | the content was placed and the popup is opening. Also dispatched on the document. |
| popup:shown | the opening transition finished. |
| popup:hide | the popup is closing. |
| popup:hidden | the popup closed and was removed. |
| popup:complete | the handler's content loaded. |
| popup:error | the handler's request failed. |
| popup:toggle | toggle() was called. |
| popup:download | the handler responded with a file download. |
Arguments
Ui::popupButton()
| Argument | Description |
|---|---|
| label | the button text, escaped. |
| handler | the AJAX handler that returns the dialog markup, rendered as data-handler. |
| requestData | an array of extra values to send to the handler, rendered as data-request-data. |
| icon | icon classes, rendered before the label. |
| primary | primary styling, btn-solid theme-primary. Default: false. |
| secondary | secondary styling, btn-solid theme-secondary. Default: false. |
| danger | danger styling, btn-solid theme-danger. Default: false. |
| hotkey | a keyboard shortcut, or an array of them, that opens the popup. |
Any other argument becomes an attribute, as on buttons, so the popup options go in as dataSize, dataKeyboard, dataBackdrop and dataAdaptiveHeight.