'assets/ui/tokens.css', self::UI_BASE => 'assets/ui/kit/base.css', self::UI_BUTTON => 'assets/ui/kit/button.css', self::UI_BADGE => 'assets/ui/kit/badge.css', self::UI_CARD => 'assets/ui/kit/card.css', self::UI_TABS => 'assets/ui/kit/tabs.css', self::UI_FORM => 'assets/ui/kit/form.css', self::UI_FIELDROW => 'assets/ui/kit/fieldrow.css', self::UI_NOTICE => 'assets/ui/kit/notice.css', self::UI_DIALOG => 'assets/ui/kit/dialog.css', self::UI_TOGGLE => 'assets/ui/kit/toggle.css', self::UI_PROGRESS => 'assets/ui/kit/progress.css', self::UI_TIP => 'assets/ui/kit/tip.css', self::ADMIN_UI => 'assets/ui/admin-ui.css', self::COMBO_SHELL => 'assets/ui/combo-shell.css', self::COMBO => 'assets/ui/combo.css', self::TABLE => 'assets/ui/table.css', self::ROW_ACTIONS => 'assets/ui/row-actions.css', self::ADMIN_STYLE => 'assets/admin/admin.css', ); public const MANIFEST = 'admin-bundle.json'; public const CHROME_MANIFEST = 'admin-chrome-bundle.json'; public static function chrome_bundle_sources(): array { return array_filter( self::BUNDLE_SOURCES, static fn( string $path ): bool => ! str_starts_with( $path, 'assets/ui/kit/' ) && ! in_array( $path, array( 'assets/ui/combo-shell.css', 'assets/ui/combo.css' ), true ) ); } private static function bundle_sources(): array { return Ui::chrome() ? self::chrome_bundle_sources() : self::BUNDLE_SOURCES; } public const SCREEN_STYLES = array( AdminModule::ORGS_SLUG => array( array( self::class, 'org_structure_styles' ) ), \RoosterLMS\Admin\Persons\PersonsPage::SLUG => array( array( self::class, 'persons_editor_styles' ) ), AdminModule::LIVE_SLUG => array( array( \RoosterLMS\Live\Calendar\CalendarAssets::class, 'enqueue_admin_styles' ) ), AdminModule::LESSONS_SLUG => array( array( \RoosterLMS\Admin\Lessons\LessonsPage::class, 'enqueue_styles' ) ), \RoosterLMS\Admin\MA\BanksPage::SLUG => array( array( \RoosterLMS\Admin\Questions\QuestionsPage::class, 'enqueue_styles' ) ), \RoosterLMS\Admin\CourseLearners\CourseLearnersPage::SLUG => array( array( \RoosterLMS\Admin\CourseLearners\CourseLearnersPage::class, 'enqueue_styles' ), self::REVIEW ), AdminModule::ATTEMPTS_SLUG => array( self::REVIEW ), AdminModule::GRADING_SLUG => array( self::REVIEW ), ); public const CARDS_SCREENS = array( \RoosterLMS\Admin\MA\GradeScalesPage::SLUG ); private static bool $hooked = false; private static ?array $bundle = null; private static bool $bundle_checked = false; private static array $dist = array( '', '' ); public function register(): void { if ( self::$hooked ) { return; } self::$hooked = true; add_action( 'init', array( self::class, 'register_assets' ), 5 ); add_action( 'init', array( self::class, 'order_block_styles' ), 99 ); add_action( 'enqueue_block_editor_assets', array( Theme::class, 'editor_flag' ) ); add_filter( 'rooster/academy_page', array( Theme::class, 'academy_page' ), 10, 2 ); add_filter( 'style_loader_src', array( self::class, 'file_version' ) ); add_filter( 'script_loader_src', array( self::class, 'file_version' ) ); add_filter( 'script_module_loader_src', array( self::class, 'file_version' ) ); add_action( 'wp_enqueue_scripts', array( self::class, 'add_inline_tokens' ), 99 ); add_action( 'admin_enqueue_scripts', array( self::class, 'add_inline_tokens' ), 99 ); add_action( 'admin_enqueue_scripts', array( self::class, 'use_bundle' ), 0 ); add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_screen' ), 1 ); add_filter( 'body_class', array( self::class, 'body_class' ) ); add_action( 'wp_footer', array( self::class, 'enqueue_select' ), 1 ); add_action( 'admin_footer', array( self::class, 'enqueue_select' ), 1 ); add_filter( 'rooster/performance/delay_keep', array( self::class, 'delay_keep' ) ); add_filter( 'render_block', array( ComboShell::class, 'block' ), 30, 2 ); } public static function delay_keep( $handles ): array { $keep = (array) $handles; foreach ( array( self::TABLE, self::TABLE_LIVE, self::TABLE_SEARCH, 'rooster-table', self::ACCOUNT ) as $handle ) { if ( wp_script_is( $handle, 'enqueued' ) ) { $keep[] = $handle; } } return $keep; } public static function ver( string $file ): string { $mtime = @filemtime( dirname( ROOSTER_LMS_FILE ) . '/assets/ui/' . $file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a missing file only loses the suffix. return ROOSTER_LMS_VERSION . ( false === $mtime ? '' : '.' . $mtime ); } public static function min( string $rel ): string { if ( ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) || ! preg_match( '#^(assets/ui/(?:kit/)?[\w-]+)\.(js|css)$#', $rel, $parts ) ) { return $rel; } $twin = $parts[1] . '.min.' . $parts[2]; return is_file( ROOSTER_LMS_DIR . $twin ) ? $twin : $rel; } private static function ui_url( string $file ): string { $rel = self::min( 'assets/ui/' . $file ); return add_query_arg( 'ver', self::ver( substr( $rel, strlen( 'assets/ui/' ) ) ), plugins_url( $rel, ROOSTER_LMS_FILE ) ); } public static function file_version( $src ) { if ( ! is_string( $src ) || '' === $src ) { return $src; } $base = plugins_url( '/', ROOSTER_LMS_FILE ); $path = strtok( $src, '?' ); if ( ! is_string( $path ) || ! str_starts_with( set_url_scheme( $path ), set_url_scheme( $base ) ) ) { return $src; } $rel = substr( set_url_scheme( $path ), strlen( set_url_scheme( $base ) ) ); $twin = self::min( $rel ); if ( $twin !== $rel ) { $src = substr( $path, 0, -strlen( $rel ) ) . $twin . substr( $src, strlen( $path ) ); $rel = $twin; } if ( str_starts_with( $rel, 'assets/dist/' ) ) { return $src; } if ( str_starts_with( $rel, 'lib/' ) ) { return $src; } $file = dirname( ROOSTER_LMS_FILE ) . '/' . $rel; $mtime = @filemtime( $file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a missing file keeps the URL. return false === $mtime ? $src : add_query_arg( 'ver', ROOSTER_LMS_VERSION . '.' . $mtime, $src ); } public static function register_assets(): void { $url = plugins_url( 'assets/ui/', ROOSTER_LMS_FILE ); wp_register_style( self::TOKENS, $url . 'tokens.css', array(), self::ver( 'tokens.css' ) ); wp_register_style( self::UI_BASE, $url . 'kit/base.css', array( self::TOKENS ), self::ver( 'kit/base.css' ) ); foreach ( self::KIT as $part ) { if ( 'base' !== $part ) { wp_register_style( 'rooster-lms-ui-' . $part, $url . 'kit/' . $part . '.css', array( self::UI_BASE ), self::ver( 'kit/' . $part . '.css' ) ); } } wp_register_style( self::UI, false, array_map( static fn( string $part ): string => 'rooster-lms-ui-' . $part, array_values( array_diff( self::KIT, array( 'tip' ) ) ) ), ROOSTER_LMS_VERSION ); wp_register_style( self::UI_BUTTON_THEME, $url . 'kit/button-theme.css', array(), self::ver( 'kit/button-theme.css' ) ); if ( Theme::designs() && isset( wp_styles()->registered[ self::UI_BUTTON ] ) ) { wp_styles()->registered[ self::UI_BUTTON ]->deps[] = self::UI_BUTTON_THEME; } wp_register_style( self::UI_CAROUSEL, $url . 'kit/carousel.css', array( self::UI_BUTTON ), self::ver( 'kit/carousel.css' ) ); wp_register_style( self::UI_ACCORDION, $url . 'kit/accordion.css', array( self::UI_BASE ), self::ver( 'kit/accordion.css' ) ); wp_register_style( self::LESSON_LAYOUT, $url . 'lesson-layout.css', array( self::TOKENS ), self::ver( 'lesson-layout.css' ) ); wp_register_style( self::SHELL, $url . 'shell.css', array( self::TOKENS ), self::ver( 'shell.css' ) ); wp_register_style( self::PATH, $url . 'path.css', array( self::UI_BASE ), self::ver( 'path.css' ) ); wp_register_style( self::PAGER, $url . 'pager.css', array( self::UI_BUTTON ), self::ver( 'pager.css' ) ); wp_register_style( self::POINTS, $url . 'points.css', array( self::UI_CARD ), self::ver( 'points.css' ) ); wp_register_style( self::COURSE_PARTS, $url . 'course-parts.css', array( self::UI_BUTTON ), self::ver( 'course-parts.css' ) ); wp_register_style( self::SCHEDULE, $url . 'schedule.css', array( self::UI_BUTTON, self::UI_BADGE ), self::ver( 'schedule.css' ) ); wp_register_style( self::FILTERS, $url . 'filters.css', array(), self::ver( 'filters.css' ) ); wp_register_style( self::TABLE, $url . 'table.css', array( self::UI_CARD, self::UI_FORM, self::UI_BUTTON, self::UI_BADGE ), self::ver( 'table.css' ) ); wp_register_style( self::STATS, $url . 'stats.css', array( self::UI_CARD ), self::ver( 'stats.css' ) ); wp_register_style( self::PORTAL, $url . 'portal.css', array( self::TABLE, self::STATS, self::FILTERS ), self::ver( 'portal.css' ) ); wp_register_style( self::ROW_ACTIONS, $url . 'row-actions.css', array( self::TOKENS ), self::ver( 'row-actions.css' ) ); wp_register_style( self::ADMIN_STYLE, plugins_url( 'assets/admin/admin.css', ROOSTER_LMS_FILE ), array( self::UI ), ROOSTER_LMS_VERSION ); wp_register_script( self::CONFIRM, plugins_url( 'assets/admin/admin.js', ROOSTER_LMS_FILE ), array(), ROOSTER_LMS_VERSION, array( 'in_footer' => true, 'strategy' => 'defer', ) ); wp_register_script( self::TABLE, $url . 'table.js', array(), self::ver( 'table.js' ), array( 'in_footer' => true, 'strategy' => 'defer', ) ); wp_register_script( self::TABLE_LIVE, $url . 'table-live.js', array( self::TABLE ), self::ver( 'table-live.js' ), array( 'in_footer' => true, 'strategy' => 'defer', ) ); wp_register_script( self::TABLE_SEARCH, $url . 'table-search.js', array( self::TABLE ), self::ver( 'table-search.js' ), array( 'in_footer' => true, 'strategy' => 'defer', ) ); wp_register_script( self::TABLE_LIBRARY, $url . 'table-library.js', array( 'wp-api-fetch' ), self::ver( 'table-library.js' ), array( 'in_footer' => true, 'strategy' => 'defer', ) ); wp_register_script( self::SELECT, $url . 'select.js', array(), self::ver( 'select.js' ), array( 'in_footer' => true ) ); wp_register_style( self::ADMIN_UI, $url . 'admin-ui.css', array( self::UI ), self::ver( 'admin-ui.css' ) ); wp_register_style( self::COMBO_SHELL, $url . 'combo-shell.css', array( self::UI ), self::ver( 'combo-shell.css' ) ); wp_register_style( self::COMBO, $url . 'combo.css', array( self::UI, self::COMBO_SHELL ), self::ver( 'combo.css' ) ); wp_register_script_module( self::BOOT, $url . 'front-boot.js', array(), self::ver( 'front-boot.js' ), array( 'fetchpriority' => 'low', 'in_footer' => true, ) ); wp_register_style( self::FRONT_ICON, $url . 'front.css', array( self::UI_BASE ), self::ver( 'front.css' ) ); wp_register_style( self::FRONT, false, array( self::FRONT_ICON, self::UI_BUTTON ), self::ver( 'front.css' ) ); wp_register_style( self::FRONT_MENU, $url . 'front-menu.css', array( self::FRONT ), self::ver( 'front-menu.css' ) ); wp_register_style( self::FRONT_SEARCH, $url . 'front-search.css', array( self::FRONT ), self::ver( 'front-search.css' ) ); wp_register_style( self::FRONT_ACCOUNT, $url . 'front-account.css', array( self::FRONT_ICON ), self::ver( 'front-account.css' ) ); wp_register_style( self::FRONT_ACCOUNT_OPEN, $url . 'front-account-open.css', array( self::FRONT_ACCOUNT, self::UI_BUTTON, self::UI_FORM, self::UI_NOTICE, self::UI_DIALOG ), self::ver( 'front-account-open.css' ) ); wp_register_script( self::FRONT, $url . 'front.js', array(), self::ver( 'front.js' ), array( 'in_footer' => true, 'strategy' => 'defer', ) ); wp_register_style( self::CHARTS, $url . 'charts.css', array(), self::ver( 'charts.css' ) ); wp_register_style( self::ACCOUNT, $url . 'account.css', array( self::UI, self::TABLE, self::DOCK, self::CHARTS, self::STATS, self::FILTERS ), self::ver( 'account.css' ) ); wp_register_script( self::ASK, $url . 'ask.js', array(), self::ver( 'ask.js' ), array( 'in_footer' => true, 'strategy' => 'defer', ) ); wp_register_script( self::ACCOUNT, $url . 'account.js', array( self::ASK ), self::ver( 'account.js' ), array( 'in_footer' => true, 'strategy' => 'defer', ) ); wp_register_style( self::DOCK, $url . 'dock.css', array( self::UI_BUTTON ), self::ver( 'dock.css' ) ); wp_register_style( self::REVIEW, $url . 'review.css', array( self::TOKENS ), self::ver( 'review.css' ) ); wp_register_style( self::QUIZ_REPORT, $url . 'quiz-report.css', array(), self::ver( 'quiz-report.css' ) ); wp_register_script( self::QUIZ_REPORT, $url . 'quiz-report.js', array(), self::ver( 'quiz-report.js' ), array( 'in_footer' => true, 'strategy' => 'defer', ) ); wp_register_script( self::TIP, $url . 'tip.js', array(), self::ver( 'tip.js' ), array( 'in_footer' => true, 'strategy' => 'defer', ) ); wp_register_script( self::DOCK, $url . 'dock.js', array(), self::ver( 'dock.js' ), array( 'in_footer' => true, 'strategy' => 'defer', ) ); wp_register_style( self::LIVE, $url . 'live.css', array( self::UI_BUTTON, self::UI_BADGE ), self::ver( 'live.css' ) ); wp_register_script( self::LIVE, $url . 'live.js', array(), self::ver( 'live.js' ), array( 'in_footer' => true, 'strategy' => 'defer', ) ); wp_register_style( self::KINDS, $url . 'kinds.css', array(), self::ver( 'kinds.css' ) ); wp_register_style( self::TREE_TABLE, $url . 'tree-table.css', array( self::TABLE, self::KINDS ), self::ver( 'tree-table.css' ) ); wp_register_script( self::TREE_TABLE, $url . 'tree-table.js', array(), self::ver( 'tree-table.js' ), array( 'in_footer' => true, 'strategy' => 'defer', ) ); wp_register_style( self::MATRIX, $url . 'matrix.css', array( self::TABLE ), self::ver( 'matrix.css' ) ); wp_register_script( self::MATRIX, $url . 'matrix.js', array( self::TREE_TABLE ), self::ver( 'matrix.js' ), array( 'in_footer' => true, 'strategy' => 'defer', ) ); wp_register_script_module( self::ROUTER, $url . 'table-router.js', array( array( 'id' => '@wordpress/interactivity-router', 'import' => 'dynamic', ), ), self::ver( 'table-router.js' ) ); self::add_paths(); } private static function add_paths(): void { $base = plugins_url( '/', ROOSTER_LMS_FILE ); foreach ( wp_styles()->registered as $handle => $style ) { if ( ! str_starts_with( (string) $handle, 'rooster-lms' ) || ! is_string( $style->src ) || ! str_starts_with( $style->src, $base ) ) { continue; } $file = ROOSTER_LMS_DIR . self::min( substr( $style->src, strlen( $base ) ) ); if ( is_readable( $file ) ) { wp_style_add_data( (string) $handle, 'path', $file ); } } } public static function add_inline_tokens(): void { if ( ! wp_style_is( self::TOKENS, 'registered' ) || ! empty( wp_styles()->get_data( self::TOKENS, 'after' ) ) ) { return; } $css = Tokens::inline_css(); if ( '' !== $css ) { wp_add_inline_style( self::TOKENS, wp_strip_all_tags( $css ) ); } } public static function body_class( $classes ): array { $classes = is_array( $classes ) ? $classes : array(); return array_merge( $classes, Tokens::body_classes() ); } public static function enqueue_ui(): void { self::ensure_registered(); wp_enqueue_style( self::UI ); } public static function enqueue_front( bool $now = false ): void { self::ensure_registered(); if ( ! $now && ! is_admin() ) { self::enqueue_boot(); return; } wp_enqueue_script( self::FRONT ); if ( ! empty( wp_scripts()->get_data( self::FRONT, 'data' ) ) ) { return; } wp_localize_script( self::FRONT, 'roosterLmsFrontL10n', self::front_l10n() ); } private static bool $boot = false; public static function enqueue_boot(): void { if ( self::$boot ) { return; } self::$boot = true; add_action( 'wp_footer', array( self::class, 'print_boot_data' ), 2 ); } public static function boot_enqueued(): bool { return self::$boot; } public static function reset_front_state(): void { self::$boot = false; self::$select_now = false; self::$account = false; } public static function boot_data(): array { $combo_min = (int) apply_filters( 'rooster_lms_combo_min_options', 12 ); $account = self::account_css(); return array( 'front' => self::ui_url( 'front.js' ), 'select' => self::ui_url( 'select.js' ), 'combo' => self::ui_url( 'combo.css' ), 'comboShell' => self::ui_url( 'combo-shell.css' ), 'comboMin' => $combo_min, 'frontL10n' => self::front_l10n(), 'selectL10n' => self::select_l10n(), 'accountCss' => $account, 'accountTokens' => isset( $account[ self::TOKENS ] ) ? wp_strip_all_tags( Tokens::inline_css( false ) ) : '', ); } public static function account_css(): array { $styles = wp_styles(); if ( ! isset( $styles->registered[ self::FRONT_ACCOUNT_OPEN ] ) || ! ( self::$account || $styles->query( self::FRONT_ACCOUNT, 'enqueued' ) || $styles->query( self::FRONT_ACCOUNT, 'done' ) ) ) { return array(); } $printed = array(); $mark = static function ( string $handle ) use ( &$mark, &$printed, $styles ): void { if ( isset( $printed[ $handle ] ) || ! isset( $styles->registered[ $handle ] ) ) { return; } $printed[ $handle ] = true; foreach ( $styles->registered[ $handle ]->deps as $dep ) { $mark( (string) $dep ); } }; foreach ( array_merge( $styles->queue, $styles->done ) as $handle ) { $mark( (string) $handle ); } if ( isset( $printed[ self::FRONT_ACCOUNT_OPEN ] ) ) { return array(); } return self::style_chain( array( self::FRONT_ACCOUNT_OPEN ), $printed ); } public static function style_chain( array $handles, array $skip = array() ): array { self::ensure_registered(); $styles = wp_styles(); $out = array(); $walk = static function ( string $handle ) use ( &$walk, &$out, $skip, $styles ): void { if ( isset( $out[ $handle ] ) || isset( $skip[ $handle ] ) || ! isset( $styles->registered[ $handle ] ) ) { return; } $style = $styles->registered[ $handle ]; foreach ( $style->deps as $dep ) { $walk( (string) $dep ); } if ( is_string( $style->src ) && '' !== $style->src ) { $out[ $handle ] = (string) self::file_version( (string) add_query_arg( 'ver', (string) $style->ver, $style->src ) ); } }; foreach ( $handles as $handle ) { $walk( (string) $handle ); } return $out; } public static function print_boot_data(): void { wp_print_inline_script_tag( (string) wp_json_encode( self::boot_data() ), array( 'type' => 'application/json', 'id' => 'rooster-lms-boot-data', ) ); $code = self::boot_code(); if ( '' !== $code ) { wp_print_inline_script_tag( $code, array( 'type' => 'module' ) ); } } public static function boot_code(): string { $file = ROOSTER_LMS_DIR . self::min( 'assets/ui/front-boot.js' ); $code = is_readable( $file ) ? (string) file_get_contents( $file ) : ''; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- the plugin's own file. $code = (string) preg_replace( array( '#^[ \t]*/\*.*?\*/[ \t]*\R#ms', '#^[ \t]*//.*\R#m' ), '', $code ); return str_contains( $code, ' __( 'No courses found.', 'rooster-lms' ), 'searching' => __( 'Searching…', 'rooster-lms' ), /* translators: %d: number of suggested courses. */ 'results' => __( '%d courses suggested. Use the arrow keys to choose one.', 'rooster-lms' ), 'error' => __( 'The search failed. Try again.', 'rooster-lms' ), 'loginWait' => __( 'Logging in…', 'rooster-lms' ), 'loginFail' => __( 'The login failed. Try again.', 'rooster-lms' ), 'previous' => __( 'Previous courses', 'rooster-lms' ), 'next' => __( 'Next courses', 'rooster-lms' ), ); } public static function enqueue_dock(): void { self::ensure_registered(); wp_enqueue_style( self::DOCK ); wp_enqueue_script( self::DOCK ); } public static function enqueue_live(): void { self::ensure_registered(); wp_enqueue_style( self::LIVE ); wp_enqueue_script( self::LIVE ); if ( ! empty( wp_scripts()->get_data( self::LIVE, 'data' ) ) ) { return; } wp_localize_script( self::LIVE, 'roosterLmsLiveL10n', array( /* translators: %s: time left, e.g. "2 days", "3 hours" or "12:05". */ 'upcoming' => __( 'Starts in %s', 'rooster-lms' ), 'open' => __( 'Open: join now', 'rooster-lms' ), 'running' => __( 'Live now', 'rooster-lms' ), 'ended' => __( 'Ended', 'rooster-lms' ), /* translators: %s: the meeting's time in the viewer's time zone, e.g. "17:00" or "Mon 17:00". */ 'local' => __( '%s your time', 'rooster-lms' ), ) ); } public static function enqueue_account(): void { self::ensure_registered(); wp_enqueue_style( self::ACCOUNT ); wp_enqueue_script( self::ACCOUNT ); if ( ! empty( wp_scripts()->get_data( self::ACCOUNT, 'data' ) ) ) { return; } wp_localize_script( self::ACCOUNT, 'roosterLmsAccountL10n', array( 'root' => esc_url_raw( rest_url( 'rooster-lms/v1/' ) ), 'nonce' => is_user_logged_in() ? wp_create_nonce( 'wp_rest' ) : '', 'saving' => __( 'Saving…', 'rooster-lms' ), 'saved' => __( 'Saved.', 'rooster-lms' ), 'failed' => __( 'That did not work. Try again.', 'rooster-lms' ), 'confirm' => __( 'Are you sure?', 'rooster-lms' ), 'cancel' => __( 'Cancel', 'rooster-lms' ), ) ); } public static function ask(): string { self::ensure_registered(); return self::ASK; } public static function enqueue_review(): void { self::ensure_registered(); wp_enqueue_style( self::REVIEW ); } public static function review_href(): string { self::ensure_registered(); $style = wp_styles()->registered[ self::REVIEW ] ?? null; return $style ? (string) add_query_arg( 'ver', (string) $style->ver, (string) $style->src ) : ''; } public static function enqueue_quiz_report(): void { self::ensure_registered(); wp_enqueue_style( self::QUIZ_REPORT ); wp_enqueue_script( self::QUIZ_REPORT ); if ( ! empty( wp_scripts()->get_data( self::QUIZ_REPORT, 'data' ) ) ) { return; } $reasons = array(); foreach ( \RoosterLMS\Assessment\Reports\ReportReason::cases() as $reason ) { $reasons[] = array( 'value' => $reason->slug(), 'label' => $reason->label(), ); } wp_localize_script( self::QUIZ_REPORT, 'roosterLmsQuizReportL10n', array( 'root' => esc_url_raw( rest_url( 'rooster-lms/v1/' ) ), 'nonce' => is_user_logged_in() ? wp_create_nonce( 'wp_rest' ) : '', 'reasons' => $reasons, 'title' => __( 'Report a problem', 'rooster-lms' ), 'reasonLabel' => __( 'What is wrong?', 'rooster-lms' ), 'messageLabel' => __( 'Message (optional)', 'rooster-lms' ), /* translators: %d: characters left (of 1,000). */ 'left' => __( '%d characters left', 'rooster-lms' ), 'send' => __( 'Send', 'rooster-lms' ), 'cancel' => __( 'Cancel', 'rooster-lms' ), 'pick' => __( 'Choose what is wrong.', 'rooster-lms' ), 'tooLong' => __( 'The message is too long.', 'rooster-lms' ), 'failed' => __( 'The report was not sent. Try again.', 'rooster-lms' ), ) ); } public static function enqueue_matrix(): void { self::ensure_registered(); wp_enqueue_style( self::MATRIX ); wp_enqueue_script( self::MATRIX ); } public static function enqueue_tree_table(): void { self::ensure_registered(); wp_enqueue_style( self::TREE_TABLE ); wp_enqueue_script( self::TREE_TABLE ); } public static function enqueue_styles(): void { self::ensure_registered(); wp_enqueue_style( self::TABLE ); } public static function enqueue_admin(): void { self::ensure_registered(); self::chrome_takes_the_kit(); wp_enqueue_style( self::UI ); wp_enqueue_style( self::UI_TIP ); wp_enqueue_style( self::ADMIN_UI ); wp_enqueue_style( self::COMBO ); self::enqueue_styles(); self::enqueue_row_actions(); if ( is_admin() ) { wp_enqueue_style( self::ADMIN_STYLE ); } } public static function enqueue_screen(): void { if ( ! Chrome::is_ours() ) { return; } self::enqueue_admin(); $slug = (string) ( $GLOBALS['plugin_page'] ?? '' ); if ( function_exists( 'rooster_admin_table_assets' ) && ( isset( TableRegistry::list_screens()[ $slug ] ) || in_array( $slug, self::CARDS_SCREENS, true ) ) ) { rooster_admin_table_assets( array( 'cards' ) ); } foreach ( self::SCREEN_STYLES[ $slug ] ?? array() as $style ) { if ( is_string( $style ) ) { if ( wp_style_is( $style, 'registered' ) ) { wp_enqueue_style( $style ); } } elseif ( is_callable( $style ) ) { call_user_func( $style ); } } } public static function org_structure_styles(): void { if ( 'edit' === \RoosterLMS\Admin\Request::action() && \RoosterLMS\Admin\Request::org() > 0 && 'registration' === \RoosterLMS\Admin\Request::key( 'tab' ) ) { foreach ( \WP_Block_Type_Registry::get_instance()->get_registered( 'rooster-lms/org-registration-fields' )?->style_handles ?? array() as $handle ) { wp_enqueue_style( $handle ); } return; } if ( 'edit' !== \RoosterLMS\Admin\Request::action() || \RoosterLMS\Admin\Request::org() <= 0 || ! in_array( \RoosterLMS\Admin\Request::key( 'tab' ), array( 'structure', 'locations' ), true ) ) { return; } if ( '' !== \RoosterLMS\Admin\Builder::register_script() && wp_style_is( \RoosterLMS\Admin\Builder::HANDLE, 'registered' ) ) { wp_enqueue_style( \RoosterLMS\Admin\Builder::HANDLE ); } if ( function_exists( 'rooster_admin_table_assets' ) ) { rooster_admin_table_assets( array( 'twin', 'cards' ) ); } if ( '' !== \RoosterLMS\Persons\BuilderPanel::register_script() && wp_style_is( \RoosterLMS\Persons\BuilderPanel::HANDLE, 'registered' ) && current_user_can( \RoosterLMS\Admin\Persons\PersonsPage::CAP ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown -- PE meta capability (Persons\PersonCaps). wp_enqueue_style( \RoosterLMS\Persons\BuilderPanel::HANDLE ); } } public static function persons_editor_styles(): void { if ( in_array( \RoosterLMS\Admin\Request::action(), array( 'new', 'edit' ), true ) && in_array( \RoosterLMS\Admin\Persons\PersonsPage::tab(), array( 'persons', 'committees' ), true ) ) { wp_enqueue_style( 'editor-buttons' ); } } public static function use_bundle(): void { if ( true !== (bool) apply_filters( 'rooster_lms_admin_asset_bundle', Chrome::is_ours() ) ) { return; } $bundle = self::bundle(); if ( null === $bundle ) { return; } self::ensure_registered(); $styles = wp_styles(); if ( ! wp_style_is( self::BUNDLE, 'registered' ) ) { wp_register_style( self::BUNDLE, $bundle['url'], array(), $bundle['hash'] ); } foreach ( array_keys( self::bundle_sources() ) as $handle ) { $after = $styles->get_data( $handle, 'after' ); $deps = isset( $styles->registered[ $handle ] ) ? (array) $styles->registered[ $handle ]->deps : array(); wp_deregister_style( $handle ); wp_register_style( $handle, false, array_values( array_unique( array_merge( array( self::BUNDLE ), $deps ) ) ), null ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- a source-less alias: nothing to version. if ( is_array( $after ) && array() !== $after ) { $styles->add_data( $handle, 'after', $after ); } } } private static function bundle(): ?array { if ( self::$bundle_checked ) { return self::$bundle; } self::$bundle_checked = true; self::$bundle = null; if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) { return null; } $dir = self::dist_dir(); $name = Ui::chrome() ? self::CHROME_MANIFEST : self::MANIFEST; $json = is_readable( $dir . $name ) ? file_get_contents( $dir . $name ) : false; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- the plugin's own manifest file. $data = is_string( $json ) ? json_decode( $json, true ) : null; if ( ! is_array( $data ) || 1 !== ( $data['version'] ?? null ) || ! is_string( $data['file'] ?? null ) || ! is_string( $data['hash'] ?? null ) || ! is_array( $data['sources'] ?? null ) ) { return null; } $file = $data['file']; if ( 1 !== preg_match( '/^[a-z0-9.-]+\.css$/', $file ) || 1 !== preg_match( '/^[0-9a-f]{16}$/', $data['hash'] ) || ! is_readable( $dir . $file ) ) { return null; } $sources = array_values( $data['sources'] ); $paths = array_values( self::bundle_sources() ); if ( count( $sources ) !== count( $paths ) ) { return null; } foreach ( $paths as $i => $path ) { $source = $sources[ $i ]; $size = @filesize( ROOSTER_LMS_DIR . $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a missing source means a stale bundle. if ( ! is_array( $source ) || ( $source['path'] ?? null ) !== $path || ! is_int( $source['bytes'] ?? null ) || $size !== $source['bytes'] ) { return null; } } self::$bundle = array( 'url' => self::dist_url() . $file, 'hash' => $data['hash'], ); return self::$bundle; } private static function dist_dir(): string { return '' !== self::$dist[0] ? trailingslashit( self::$dist[0] ) : ROOSTER_LMS_DIR . 'assets/dist/'; } private static function dist_url(): string { return '' !== self::$dist[1] ? trailingslashit( self::$dist[1] ) : plugins_url( 'assets/dist/', ROOSTER_LMS_FILE ); } public static function use_dist( string $dir, string $url ): void { self::$dist = array( $dir, $url ); self::$bundle = null; self::$bundle_checked = false; } public static function enqueue_row_actions(): void { self::ensure_registered(); wp_enqueue_style( self::ROW_ACTIONS ); wp_enqueue_script( self::CONFIRM ); } public static function enqueue_table( string $mode, bool $search_live = false ): void { self::enqueue_styles(); wp_enqueue_script( self::TABLE ); if ( 'rest' === $mode || $search_live ) { wp_enqueue_script( 'wp-api-fetch' ); } if ( 'router' === $mode ) { wp_enqueue_script( self::TABLE_LIVE ); wp_enqueue_script_module( self::ROUTER ); } } public static function enqueue_table_search(): void { self::ensure_registered(); if ( ! is_admin() ) { $lib = dirname( ROOSTER_LMS_FILE ) . '/lib/rooster-admin'; require_once $lib . '/table-engine.php'; $engine = rooster_table_engine( $lib, plugins_url( 'lib/rooster-admin', ROOSTER_LMS_FILE ) ); $search = wp_scripts()->query( self::TABLE_SEARCH, 'registered' ); if ( $search && ! in_array( $engine, $search->deps, true ) ) { $search->deps[] = $engine; } } wp_enqueue_script( self::TABLE_SEARCH ); } public static function enqueue_table_library(): void { self::ensure_registered(); wp_enqueue_script( self::TABLE_LIBRARY ); } private const KIT_AFTER_OWN = array( 'rooster-lms/my-courses', 'rooster-lms/org-portal' ); public static function order_block_styles(): void { foreach ( \WP_Block_Type_Registry::get_instance()->get_all_registered() as $name => $type ) { if ( ! str_starts_with( (string) $name, 'rooster-lms/' ) || in_array( $name, self::KIT_AFTER_OWN, true ) ) { continue; } $handles = array_values( array_map( 'strval', (array) $type->style_handles ) ); $own = $handles[0] ?? ''; if ( '' === $own || generate_block_asset_handle( (string) $name, 'style' ) !== $own || ! isset( wp_styles()->registered[ $own ] ) ) { continue; } $deps = array_values( array_unique( array_merge( wp_styles()->registered[ $own ]->deps, array_slice( $handles, 1 ) ) ) ); wp_styles()->registered[ $own ]->deps = $deps; } } public static function enqueue_tip(): void { self::ensure_registered(); if ( Ui::chrome() ) { return; } wp_enqueue_script( self::TIP ); if ( is_admin() ) { wp_enqueue_style( self::UI_TIP ); } } public static function enqueue_front_part( string $part ): void { self::ensure_registered(); $handles = array( 'menu' => self::FRONT_MENU, 'search' => self::FRONT_SEARCH, 'account' => self::FRONT_ACCOUNT, ); wp_enqueue_style( $handles[ $part ] ?? self::FRONT ); if ( 'account' === $part ) { self::$account = true; } } private static bool $account = false; public static function enqueue_part( string $handle ): void { self::ensure_registered(); self::chrome_takes_the_kit(); wp_enqueue_style( $handle ); } private static bool $kit_taken = false; private static function chrome_takes_the_kit(): void { if ( self::$kit_taken || ! Ui::chrome() ) { return; } self::$kit_taken = true; $styles = wp_styles(); $handles = array_merge( array( self::UI, self::UI_TIP, self::COMBO_SHELL, self::COMBO ), array_map( static fn( string $part ): string => 'rooster-lms-ui-' . $part, self::KIT ) ); foreach ( array_unique( $handles ) as $handle ) { if ( ! isset( $styles->registered[ $handle ] ) ) { continue; } $deps = array_values( array_diff( (array) $styles->registered[ $handle ]->deps, $handles ) ); wp_deregister_style( $handle ); wp_register_style( $handle, false, $deps, null ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- a source-less alias: nothing to version. } } public static function reset_kit_taken(): void { self::$kit_taken = false; } public static function restore_kit(): void { self::$kit_taken = false; foreach ( array_unique( array_merge( array( self::UI, self::UI_TIP, self::COMBO_SHELL, self::COMBO ), array_map( static fn( string $part ): string => 'rooster-lms-ui-' . $part, self::KIT ) ) ) as $handle ) { wp_deregister_style( $handle ); } self::register_assets(); } public static function enqueue_select(): void { if ( ! wp_style_is( self::UI, 'registered' ) || ! ( wp_style_is( self::UI, 'enqueued' ) || wp_style_is( self::UI, 'done' ) ) ) { return; } if ( Ui::chrome() ) { return; } self::enqueue_tip(); if ( ! is_admin() && ! self::$select_now && ! wp_style_is( self::BUNDLE, 'enqueued' ) ) { self::enqueue_boot(); return; } if ( wp_script_is( self::SELECT, 'enqueued' ) ) { return; } wp_enqueue_style( self::COMBO ); wp_enqueue_script( self::SELECT ); wp_localize_script( self::SELECT, 'roosterLmsSelectL10n', self::select_l10n() ); } public static function enqueue_combo_style(): void { self::ensure_registered(); wp_enqueue_style( self::COMBO_SHELL ); } private static bool $select_now = false; public static function select_now(): void { self::$select_now = true; } public static function select_l10n(): array { return array( 'search' => __( 'Search…', 'rooster-lms' ), 'choose' => __( 'Choose…', 'rooster-lms' ), 'noResults' => __( 'No results', 'rooster-lms' ), 'searching' => __( 'Searching…', 'rooster-lms' ), /* translators: %d: minimum number of characters. */ 'typeMore' => __( 'Type at least %d characters to search', 'rooster-lms' ), /* translators: %d: number of options not shown. */ 'more' => __( '%d more: type to narrow the list', 'rooster-lms' ), /* translators: %s: option label. */ 'remove' => __( 'Remove %s', 'rooster-lms' ), 'clear' => __( 'Clear', 'rooster-lms' ), /* translators: %d: number of options shown. */ 'selectAll' => __( 'Select all shown (%d)', 'rooster-lms' ), /* translators: %d: number of selected options. */ 'selected' => __( '%d selected', 'rooster-lms' ), 'separator' => _x( ', ', 'between the chosen options of a filter', 'rooster-lms' ), 'error' => __( 'The search failed. Try again.', 'rooster-lms' ), ); } private static function ensure_registered(): void { if ( ! wp_style_is( self::TOKENS, 'registered' ) ) { self::register_assets(); } } }