375 lines
10 KiB
PHP
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));
|
|
}
|
|
}
|
|
}
|