:
*
* - 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
}
}