first commit.
This commit is contained in:
@@ -0,0 +1,447 @@
|
||||
<?php
|
||||
|
||||
namespace Libs;
|
||||
|
||||
/**
|
||||
* HTMX - DuckBrain
|
||||
*
|
||||
* HTMX component library for DuckBrain.
|
||||
*
|
||||
* @author KJ
|
||||
* @website https://kj2.me
|
||||
* @license MIT
|
||||
*/
|
||||
class HTMX extends HTMLComponent
|
||||
{
|
||||
/**
|
||||
* Valid HTMX swap styles.
|
||||
*/
|
||||
public const SWAP_STYLES = [
|
||||
'innerHTML', 'outerHTML', 'outerSync',
|
||||
'beforebegin', 'before',
|
||||
'afterbegin', 'prepend',
|
||||
'beforeend', 'append',
|
||||
'afterend', 'after',
|
||||
'delete', 'none',
|
||||
'innerMorph', 'outerMorph',
|
||||
'textContent',
|
||||
];
|
||||
|
||||
/**
|
||||
* Valid HTMX swap option keys (used as key:value after the style).
|
||||
*/
|
||||
public const SWAP_OPTIONS = [
|
||||
'swap', 'settle', 'transition', 'ignoreTitle', 'strip',
|
||||
'focusScroll', 'swapEmpty', 'scroll', 'show', 'showTarget',
|
||||
'scrollTarget', 'target',
|
||||
];
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @throws \InvalidArgumentException On an unknown style or option.
|
||||
*/
|
||||
public static function swapOob(string $value = 'true', bool $excludeBoosted = false): string
|
||||
{
|
||||
if ($excludeBoosted && static::isBoosted()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($value !== 'true') {
|
||||
$value = static::validateSwap($value);
|
||||
}
|
||||
|
||||
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 '';
|
||||
}
|
||||
|
||||
$swap = static::validateSwap($swap);
|
||||
|
||||
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
|
||||
*
|
||||
* @throws \InvalidArgumentException On an unknown style or option.
|
||||
*/
|
||||
public static function reswap(string $rule = 'innerHTML'): void
|
||||
{
|
||||
header('HX-Reswap: ' . static::validateSwap($rule));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a HTMX swap value ("style [key:value]...").
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return string The normalized value.
|
||||
*
|
||||
* @throws \InvalidArgumentException On an unknown style or option.
|
||||
*/
|
||||
public static function validateSwap(string $value): string
|
||||
{
|
||||
$parts = preg_split(
|
||||
'/\s+/',
|
||||
trim($value),
|
||||
-1,
|
||||
PREG_SPLIT_NO_EMPTY
|
||||
);
|
||||
|
||||
if (empty($parts)) {
|
||||
throw new \InvalidArgumentException('Empty swap value.');
|
||||
}
|
||||
|
||||
if (!in_array($parts[0], static::SWAP_STYLES, true)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Invalid swap style: ' . $parts[0]
|
||||
);
|
||||
}
|
||||
|
||||
foreach (array_slice($parts, 1) as $option) {
|
||||
$key = explode(':', $option)[0];
|
||||
if (!in_array($key, static::SWAP_OPTIONS, true)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Invalid swap option: ' . $option
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user