: * * - an enqueued inline style that hides every scheduled target until the * script has run (`html:not(.yangra-conditional-js)`). Visitors without * JavaScript, or whose browser blocks or delays the script, never see * content before or after its window, but they also miss content that is * live; * - the schedules and the script from build/frontend/schedules.js, inline at * the top of , so targets are hidden before the page body is drawn * (no flash of content). Its first line adds the class that turns the * style above off. * * Decided in the browser, with the visitor's clock, so it works the same on * cached pages. It isn't protection: the targets are still in the HTML. */ class ScheduleRuntime { const SCRIPT_ID = 'yangra-conditional-schedules'; /** * Class the script adds to as soon as it runs. */ const JS_CLASS = 'yangra-conditional-js'; /** * Scheduled elements. * * @var ScheduledElements */ private $schedules; /** * Constructor. * * @param ScheduledElements $schedules Scheduled elements. */ public function __construct( ScheduledElements $schedules ) { $this->schedules = $schedules; } /** * Registers hooks. * * @return void */ public function register(): void { add_action( 'wp_enqueue_scripts', array( $this, 'enqueue' ) ); add_action( 'wp_head', array( $this, 'print' ), 1 ); } /** * Enqueues the style that hides every scheduled target until the script * has run. * * @return void */ public function enqueue(): void { $data = $this->data(); if ( null === $data || '' === $this->script() ) { return; } wp_register_style( self::SCRIPT_ID, false, array(), YANGRA_CONDITIONAL_VERSION ); wp_enqueue_style( self::SCRIPT_ID ); wp_add_inline_style( self::SCRIPT_ID, self::hiding_css( $data['schedules'] ) ); } /** * CSS that hides every target of the schedules while doesn't have * the class the script adds. * * @param array}> $schedules Schedules. * @return string */ public static function hiding_css( array $schedules ): string { $targets = array(); foreach ( $schedules as $schedule ) { $targets = array_merge( $targets, $schedule['targets'] ); } // Targets are validated class and ID names, so they're safe in CSS. $selectors = array_map( static function ( string $target ): string { return 'html:not(.' . self::JS_CLASS . ') ' . $target; }, array_values( array_unique( $targets ) ) ); return implode( ',', $selectors ) . '{display:none!important}'; } /** * The data the script needs on the current page, or null when no schedule * applies. Times are in milliseconds, for JavaScript. * * @return array{schedules: array, show: int|null, hide: int|null}>, now?: int}|null */ public function data(): ?array { $queried = is_singular() ? (int) get_queried_object_id() : 0; $schedules = array(); foreach ( $this->schedules->for_page( $queried ) as $schedule ) { $schedules[] = array( 'targets' => $schedule['targets'], 'show' => null === $schedule['show_at'] ? null : $schedule['show_at'] * 1000, 'hide' => null === $schedule['hide_at'] ? null : $schedule['hide_at'] * 1000, ); } if ( array() === $schedules ) { return null; } /** * Filters the data printed for the scheduled elements script. Pro adds * `now` (ms) while an admin previews a date and time: the script's * clock starts there instead of at the visitor's time. * * @param mixed $data `schedules`, each with `targets`, `show` and `hide` (ms, or null). * Anything but an array is ignored, and `schedules` can't be changed. * @param int $queried Post ID of the page being viewed, or 0. */ $data = apply_filters( 'yangra_conditional_schedule_runtime_data', array( 'schedules' => $schedules ), $queried ); $data = is_array( $data ) ? $data : array(); if ( isset( $data['now'] ) && ! is_int( $data['now'] ) ) { unset( $data['now'] ); } $data['schedules'] = $schedules; return $data; } /** * Prints the script. Its first line marks , which turns off the * style that hides every target; the script then hides only those that * should be hidden now. * * @return void */ public function print(): void { $data = $this->data(); if ( null === $data ) { return; } $script = $this->script(); if ( '' === $script ) { return; } wp_print_inline_script_tag( 'document.documentElement.classList.add(' . wp_json_encode( self::JS_CLASS ) . ");\n" . 'window.yangraConditionalSchedules = ' . wp_json_encode( $data, JSON_HEX_TAG | JSON_HEX_AMP ) . ";\n" . $script, array( 'id' => self::SCRIPT_ID ) ); } /** * The built script, or '' when it hasn't been built. * * @return string */ private function script(): string { $file = YANGRA_CONDITIONAL_PATH . 'build/frontend/schedules.js'; if ( ! is_readable( $file ) ) { return ''; } // A local file in the plugin, read to print inline. return trim( (string) file_get_contents( $file ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents } }