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]+)?([\t \r\n]+)?(.+)?([\t \r\n]+)?([\t \r\n]+)?/siU', '/([\t \r\n]+)?([\t \r\n]+)?(.+)?([\t \r\n]+)?([\t \r\n]+)?/siU', '/([\t \r\n]+)?([\t \r\n]+)?(.+)?([\t \r\n]+)?([\t \r\n]+)?/siU', '/([\t \r\n]+)?([\t \r\n]+)?(.+)?([\t \r\n]+)?([\t \r\n]+)?/siU', '/([\t \r\n]+)?([\t \r\n]+)?(.+)?([\t \r\n]+)?([\t \r\n]+)?/siU', '/([\t \r\n]+)?([\t \r\n]+)?(.+)?([\t \r\n]+)?([\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 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 '' . $content . ''; } /** * 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 (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)); } } }