Files
duckbrain-toolbox/packages/htmx/src/Libs/HTMX.php
T

375 lines
10 KiB
PHP

<?php
namespace Libs;
/**
* HTMX - DuckBrain
*
* HTMX component library for DuckBrain.
*
* @author KJ
* @website https://kj2.me
* @license MIT
*/
class HTMX extends HTMLComponent
{
/**
* Renders a component
*
* @param string $component
* @param array $properties
*
* @return void
*/
public static function render(
string $component,
array $properties = []
): void {
$instance = new static($properties);
$instance->load($component);
$instance->print();
}
/**
* Parses and processes the content in search of more components
*
* @return void
*/
protected function parse(): void
{
$this->parseHTMX();
parent::parse();
}
/**
* Removes or displays sections based on comment tags and
* HTMX request states.
*
* Possible comments are:
*
* | Block | normal | full | partial | Shown if the request is |
* |--------------|--------|------+---------+--------------------------------------|
* | hx | - | + | + | any HTMX request |
* | !hx | + | - | - | not HTMX (plain navigation) |
* | hx-full | - | + | - | full page HTMX request |
* | !hx-full | + | - | + | not a full HTMX request (plain + swap)|
* | hx-partial | - | - | + | partial (targeted) HTMX request |
* | !hx-partial | + | + | - | not a partial request (plain + full) |
*
* "full" includes boosted requests, which are no longer a separate
* state (legacy): distinguish them with HTMX::isBoosted() in PHP if
* needed.
*
* "!" negates a block. Nest blocks for AND, and use sibling blocks
* with the same content for OR. Nesting two blocks with the same
* name is not supported.
*
* @return void
*/
protected function parseHTMX(): void
{
if (!static::isHtmx()) {
$replacement = ['$3', '$3', '$3', '', '', ''];
} elseif (static::isPartialRequest()) {
$replacement = ['', '$3', '', '$3', '', '$3'];
} else {
$replacement = ['', '$3', '$3', '$3', '$3', ''];
}
$this->content = trim(preg_replace(
[
'/([\t \r\n]+)?<!-- !hx -->([\t \r\n]+)?(.+)?([\t \r\n]+)?<!-- \/!hx -->([\t \r\n]+)?/siU',
'/([\t \r\n]+)?<!-- !hx-full -->([\t \r\n]+)?(.+)?([\t \r\n]+)?<!-- \/!hx-full -->([\t \r\n]+)?/siU',
'/([\t \r\n]+)?<!-- !hx-partial -->([\t \r\n]+)?(.+)?([\t \r\n]+)?<!-- \/!hx-partial -->([\t \r\n]+)?/siU',
'/([\t \r\n]+)?<!-- hx -->([\t \r\n]+)?(.+)?([\t \r\n]+)?<!-- \/hx -->([\t \r\n]+)?/siU',
'/([\t \r\n]+)?<!-- hx-full -->([\t \r\n]+)?(.+)?([\t \r\n]+)?<!-- \/hx-full -->([\t \r\n]+)?/siU',
'/([\t \r\n]+)?<!-- hx-partial -->([\t \r\n]+)?(.+)?([\t \r\n]+)?<!-- \/hx-partial -->([\t \r\n]+)?/siU',
],
$replacement,
$this->content
));
}
/**
* Deletes empty lines.
*
* @return void
*/
public function deleteEmptyLines(): void
{
$this->content = trim(preg_replace(
'/^[ \t]*[\r\n]+/m',
'',
$this->content
));
}
/**
* Checks if the request is htmx or not.
*
* @return bool
*/
public static function isHtmx(): bool
{
return isset($_SERVER['HTTP_HX_REQUEST']);
}
/**
* Returns the request type of the HTMX request.
*
* "partial" for targeted swaps, "full" for body-level or
* hx-select requests. Empty string if the request is not HTMX.
*
* @return string
*/
public static function requestType(): string
{
return $_SERVER['HTTP_HX_REQUEST_TYPE'] ?? '';
}
/**
* Checks if the request is a full page HTMX request.
*
* @return bool
*/
public static function isFullRequest(): bool
{
return static::requestType() === 'full';
}
/**
* Checks if the request is a partial (targeted) HTMX request.
*
* @return bool
*/
public static function isPartialRequest(): bool
{
return static::requestType() === 'partial';
}
/**
* Redirects to an internal relative path by sending the appropriate header
* if it's a normal or HTMX request.
*
* @param string $path
* The path relative to the base path.
*
* @return void
*/
public static function redirect(string $path): void
{
if (static::isHtmx()) {
header('HX-Redirect: ' . Router::basePath() . ltrim($path, '/'));
} else {
Router::redirect($path);
}
}
/**
* Checks if the request is Boosted or not.
*
* @return bool
*/
public static function isBoosted(): bool
{
return isset($_SERVER['HTTP_HX_BOOSTED']) && boolval($_SERVER['HTTP_HX_BOOSTED']);
}
/**
* Returns the current browser URL when the HTMX request is made.
*
* @return string
*/
public static function currentURL(): string
{
return $_SERVER['HTTP_HX_CURRENT_URL'] ?? '';
}
/**
* Returns the HTMX target (if it exists).
*
* The HX-Target header uses the "tagName#id" format.
*
* @return string
*/
public static function target(): string
{
return $_SERVER['HTTP_HX_TARGET'] ?? '';
}
/**
* Returns the HX-Source header: the element that triggered the request,
* in "tagName#id" format.
*
* @return string
*/
public static function source(): string
{
return $_SERVER['HTTP_HX_SOURCE'] ?? '';
}
/**
* The tag name of the element that triggered the request (if it exists).
*
* @return string
*/
public static function sourceTag(): string
{
$source = static::source();
$hash = strpos($source, '#');
return $hash === false ? $source : substr($source, 0, $hash);
}
/**
* The ID of the element that triggered the request (if it exists).
*
* @return string
*/
public static function sourceId(): string
{
$source = static::source();
$hash = strpos($source, '#');
return $hash === false ? '' : substr($source, $hash + 1);
}
/**
* Only when it is an HTMX request, it returns the HTMX hx-swap-oob property.
*
* @param string $value Swap value: "true" for the default swap,
* or a swap style with optional options
* (e.g. "beforeend" or "outerHTML").
* @param bool $excludeBoosted Exclude if the request is boosted (default: false)
*
* @return string
*/
public static function swapOob(string $value = 'true', bool $excludeBoosted = false): string
{
if ($excludeBoosted && static::isBoosted()) {
return '';
}
return static::isHtmx() ? 'hx-swap-oob="' . $value . '"' : '';
}
/**
* Only when it is an HTMX request, it returns the HTMX hx-select-oob property.
*
* @param string $selector
*
* @return string
*/
public static function selectOob(string $selector): string
{
return static::isHtmx() ? 'hx-select-oob="' . $selector . '"' : '';
}
/**
* Wraps the given content in an HTMX <hx-partial> element, which is
* swapped into its own target alongside the main response.
*
* Returns an empty string on non-HTMX requests, since partials are only
* meaningful in a swap response. Note that, if the response contains
* only partials, the main swap is skipped unless the triggering element
* uses the "swapEmpty:true" swap option.
*
* @param string $content HTML content to deliver to the target.
* @param string $target CSS selector of the target element.
* @param string $swap Swap style (default: innerHTML).
*
* @return string
*/
public static function partial(
string $content,
string $target,
string $swap = 'innerHTML'
): string {
if (!static::isHtmx()) {
return '';
}
return '<hx-partial hx-target="' . htmlspecialchars($target)
. '" hx-swap="' . htmlspecialchars($swap) . '">'
. $content
. '</hx-partial>';
}
/**
* Forces the browser's URL to change and adds it to the history.
*
* Equivalent to using the htmx hx-push-url property. When the user
* navigates back, htmx re-requests the URL from the server and swaps it
* into <body> (or the [hx-history-elt] element if present).
*
* @param string $url Relative path.
*
* @return void
*/
public static function pushUrl(string $url): void
{
if (static::isHtmx()) {
header('HX-Push-Url: ' . static::route($url));
}
}
/**
* Forces the browser's URL to change without adding it to the history.
*
* Equivalent to using the htmx hx-replace-url property.
*
* @param string $url Relative path.
*
* @return void
*/
public static function replaceUrl(string $url): void
{
if (static::isHtmx()) {
header('HX-Replace-Url: ' . static::route($url));
}
}
/**
* Changes the HTMX swap target.
*
* @param string $selector
*
* @return void
*/
public static function retarget(string $selector): void
{
header('HX-Retarget: ' . $selector);
}
/**
* Changes the HTMX swap rule.
*
* The value is a swap style optionally followed by swap options,
* e.g. "outerHTML transition:true" or "delete".
*
* @param string $rule
*
* @return void
*/
public static function reswap(string $rule = 'innerHTML'): void
{
header('HX-Reswap: ' . $rule);
}
/**
* Triggers an HTMX event on the frontend upon response.
*
* @param Neuron|string $event
*
* @return void
*/
public static function trigger(Neuron|string $event): void
{
if (is_string($event)) {
header('HX-Trigger: ' . $event);
} else {
header('HX-Trigger: ' . json_encode($event));
}
}
}