-1;}).map(function(c){return c.index;}).join(" "));}}catch(e){}})(document.getElementById(%s));'; private static array $scripts = array(); public static function render( array $args ): string { Assets::enqueue_tree_table(); $id = isset( $args['id'] ) && '' !== $args['id'] ? sanitize_html_class( (string) $args['id'] ) : 'rooster-lms-tree-' . ( ++self::$count ); $columns = self::columns( $args['columns'] ?? array() ); $select = ! empty( $args['select'] ); $menu = $args['menu'] ?? true; $rows = self::normalise( $args['rows'] ?? array(), $columns ); $facets = array_filter( is_array( $args['facets'] ?? null ) ? $args['facets'] : array(), static fn( $v ): bool => null !== $v && '' !== $v && array() !== $v ); $allowed = null; if ( array() !== $facets ) { list( $allowed, $opened ) = self::facet_rows( $rows, $facets ); foreach ( $rows as $i => $r ) { if ( isset( $opened[ $r['id'] ] ) ) { $rows[ $i ]['open'] = true; } } } $tree = self::tree( $rows ); $count = array_values( array_filter( array_map( 'strval', (array) ( $args['count_kind'] ?? array() ) ), 'strlen' ) ); $pages = is_array( $args['pages'] ?? null ) && (int) ( $args['pages']['size'] ?? 0 ) > 0 && array() !== $count && Ui::kit_markup() ? $args['pages'] : null; $paging = null; if ( null !== $pages ) { $paging = self::page_rows( $tree, $rows, $allowed, $count, (int) $pages['size'], (int) ( $pages['paged'] ?? 1 ) ); $allowed = $paging['allowed']; } $kinds = $args['kinds'] ?? array(); $l10n = array_merge( self::l10n(), array_map( 'strval', $args['l10n'] ?? array() ) ); $counted = self::counted( $rows, $count ); $offset = $select ? 1 : 0; $config = array( 'id' => $id, 'columns' => array_map( static fn( array $c, int $i ): array => array( 'key' => $c['key'], 'sticky' => $c['sticky'], 'edit' => $c['edit'], 'search' => $c['search'], 'class' => $c['class'], 'hideable' => $c['hideable'], 'index' => $i + 1 + $offset, 'sort' => $c['sort'], ), $columns, array_keys( $columns ) ), 'kinds' => self::kinds( $kinds ), 'actions' => self::actions_config( $args['actions'] ?? array() ), 'rules' => self::rules( $args['rules'] ?? array() ), 'select' => $select, 'menu' => (bool) $menu, 'count' => 1 < count( $count ) ? $count : (string) ( $count[0] ?? '' ), 'kit' => Ui::kit_markup(), 'l10n' => $l10n, ); if ( array() !== $facets ) { $config['facets'] = (object) $facets; } if ( null !== $paging ) { $config['pages'] = array( 'size' => (int) $pages['size'], 'paged' => $paging['paged'], 'chevron' => Pager::chevron(), 'l10n' => array( /* translators: 1: first row number, 2: last row number, 3: number of rows. */ 'summary' => __( 'Showing %1$s–%2$s of %3$s', 'rooster-lms' ), 'label' => __( 'Pages', 'rooster-lms' ), 'prev' => __( 'Previous page', 'rooster-lms' ), 'next' => __( 'Next page', 'rooster-lms' ), /* translators: %s: page number. */ 'page' => __( 'Page %s', 'rooster-lms' ), ), ); } $sort = self::sort_args( $args['sort'] ?? null ); if ( null !== $sort ) { $config['sort'] = array( 'mode' => $sort['mode'], 'kinds' => $sort['kinds'], ); if ( $sort['address'] ) { $config['sort'] += array( 'address' => true, 'key' => $sort['key'], 'dir' => $sort['dir'], 'none' => $sort['none'], ); } } if ( ! empty( $args['save'] ) ) { $config['save'] = self::user_locale( esc_url_raw( (string) $args['save'] ) ); } if ( ! empty( $args['children'] ) ) { $marks = array( '{id}' => 'rl-tree-id-mark', '{kind}' => 'rl-tree-kind-mark', ); $config['children'] = self::user_locale( strtr( esc_url_raw( strtr( (string) $args['children'], $marks ) ), array_flip( $marks ) ) ); } if ( ! empty( $args['batch'] ) ) { $config['childrenMany'] = self::user_locale( esc_url_raw( (string) $args['batch'] ) ); } if ( isset( $config['save'] ) || isset( $config['children'] ) || isset( $config['childrenMany'] ) ) { $config['nonce'] = is_user_logged_in() ? wp_create_nonce( 'wp_rest' ) : ''; } $search = $args['search'] ?? true; $html = '
'; $html .= '
'; $html .= '
'; if ( false !== $search ) { $html .= self::search_field( $id, self::BLOCK, 'data-rl-tree', $l10n['searchLabel'], is_string( $search ) ? $search : $l10n['searchPlaceholder'] ); } /* translators: %s: number of rows. */ $html .= '' . esc_html( self::fill( 1 === $counted ? $l10n['countOne'] : $l10n['countMany'], array( Format::number( $counted ) ) ) ) . ''; if ( $select ) { $html .= ''; } $scaled = ! empty( $args['text_size'] ) && ! empty( $args['id'] ) && Ui::kit_markup() && method_exists( '\Rooster\Admin\Ui', 'text_size_button' ); $sizer = $scaled ? \Rooster\Admin\Ui::text_size_button( $id . '-rows', array( 'key' => $id, 'product' => 'rooster-lms', 'size' => 'md', ) ) : ''; if ( $args['tools'] ?? true ) { $html .= self::expand_tools( self::BLOCK, 'data-rl-tree', $sizer ); } elseif ( $scaled ) { $html .= '' . $sizer . ''; } $hideable = array(); foreach ( $columns as $i => $column ) { if ( $column['hideable'] ) { $hideable[] = array( 'key' => $column['key'], 'label' => $column['label'], 'index' => $i + 1 + $offset, 'visible' => $column['visible'], ); } } if ( array() !== $hideable ) { $html .= '
' . DataTable::columns_tool( $id, $hideable, count( array_filter( $columns, static fn( array $c ): bool => '' !== (string) $c['label'] ) ) - count( $hideable ) ) . '
'; self::$scripts[ $id ] = $id; } $html .= '
'; $editable = array() !== array_filter( $rows, static fn( array $r ): bool => array() !== $r['edit'] ); $hidden = array(); foreach ( $columns as $i => $column ) { if ( $column['hideable'] && ! $column['visible'] ) { $hidden[] = $i + 1 + $offset; } } $html .= '' . ''; $html .= self::head( $columns, $select, (bool) $menu, $l10n, $sort ) . ''; $first = true; self::walk( $tree, '', 1, true, static function ( array $row, int $level, int $pos, int $size, bool $shown, bool $loaded, int $kids ) use ( &$html, &$first, $columns, $kinds, $select, $menu, $l10n, $editable, $allowed ): void { $shown = $shown && ( null === $allowed || isset( $allowed[ $row['id'] ] ) ); $at = array( 'level' => $level, 'pos' => $pos, 'size' => $size, 'shown' => $shown, 'loaded' => $loaded, 'kids' => $kids, 'focus' => $first && $shown, 'grid' => $editable, ); $html .= self::row( $row, $at, $columns, $kinds, $select, (bool) $menu, $l10n ); if ( $shown ) { $first = false; } } ); $html .= '
' . esc_html( (string) ( $args['caption'] ?? '' ) ) . '
'; if ( false === strpos( $html, self::BLOCK . '__grip"' ) ) { $html = str_replace( '' . esc_html( array() === $rows ? $l10n['empty'] : $l10n['noMatch'] ) . '

'; $html .= ''; if ( null !== $paging ) { $html .= '
' . Pager::with_summary( $paging['paged'], (int) $pages['size'], $paging['on_page'], $paging['matched'], $pages['url'] ?? static fn( int $n ): string => (string) add_query_arg( 'paged', $n > 1 ? $n : false ) ) . '
'; } $html .= ''; $html .= '

'; $html .= '
'; return $html . ''; } public static function print_scripts(): void { $ids = self::$scripts; self::$scripts = array(); foreach ( $ids as $id ) { wp_print_inline_script_tag( sprintf( self::PREFS_JS, wp_json_encode( $id ) ) ); } } public static function rows_json( array $rows, array $columns ): array { $columns = self::columns( $columns ); $out = array(); foreach ( self::normalise( $rows, $columns, false ) as $row ) { $cells = array(); foreach ( $row['cells'] as $key => $cell ) { $cells[ $key ] = is_array( $cell ) ? array_filter( array( 'html' => isset( $cell['html'] ) ? wp_kses( (string) $cell['html'], is_admin() ? Kses::admin() : Kses::front() ) : esc_html( (string) ( $cell['text'] ?? '' ) ), 'sort' => isset( $cell['sort'] ) && is_scalar( $cell['sort'] ) ? (string) $cell['sort'] : null, ), static fn( $v ): bool => null !== $v ) : $cell; } $item = array( 'id' => $row['id'], 'kind' => $row['kind'], 'title' => $row['title'], 'cells' => $cells, 'movable' => $row['movable'], 'lazy' => $row['lazy'], 'count' => $row['count'], ); if ( null !== $row['parent'] ) { $item['parent'] = $row['parent']; } foreach ( array( 'href', 'search', 'class' ) as $key ) { if ( '' !== $row[ $key ] ) { $item[ $key ] = 'href' === $key ? esc_url_raw( $row[ $key ] ) : $row[ $key ]; } } if ( array() !== $row['edit'] ) { $item['edit'] = $row['edit']; } if ( array() !== $row['actions'] ) { $item['actions'] = $row['actions']; } if ( ! $row['selectable'] ) { $item['selectable'] = false; } if ( $row['open'] ) { $item['open'] = true; } if ( array() !== $row['data'] ) { $item['data'] = $row['data']; } if ( null !== $row['lang'] ) { $item['lang'] = $row['lang']; } if ( array() !== $row['facets'] ) { $item['facets'] = $row['facets']; } if ( array() !== $row['sort'] ) { $item['sort'] = $row['sort']; } if ( '' !== $row['icon'] ) { $item['icon'] = array( 'kind' => $row['icon'], 'html' => KindIcon::svg( $row['icon'] ), ); } $out[] = $item; } return $out; } public static function user_locale( string $url ): string { if ( '' === $url || ! is_admin() || str_contains( $url, '_locale=' ) ) { return $url; } return $url . ( str_contains( $url, '?' ) ? '&' : '?' ) . '_locale=user'; } public static function search_field( string $id, string $block, string $attr, string $label, string $placeholder ): string { if ( Ui::kit_markup() ) { return ''; } return ''; } public static function expand_tools( string $block, string $attr, string $lead = '' ): string { if ( Ui::kit_markup() ) { return '' . $lead . Ui::button( __( 'Expand all', 'rooster-lms' ), 'outline', array( $attr . '-expand' => '1' ) ) . Ui::button( __( 'Collapse all', 'rooster-lms' ), 'outline', array( $attr . '-expand' => '0' ) ) . ''; } return '' . '' . '' . ''; } public static function chevron( string $css_class ): string { return ''; } public static function l10n(): array { return array( /* translators: 1: column name, 2: "ascending" or "descending". */ 'sortedBy' => __( 'Sorted by %1$s, %2$s.', 'rooster-lms' ), 'ascending' => __( 'ascending', 'rooster-lms' ), 'descending' => __( 'descending', 'rooster-lms' ), 'manualOrder' => __( 'The order you set is back.', 'rooster-lms' ), 'sortedNoMove' => __( 'Click the sorted column until the order you set is back to move rows.', 'rooster-lms' ), 'searchLabel' => __( 'Search the rows', 'rooster-lms' ), 'searchPlaceholder' => __( 'Search…', 'rooster-lms' ), /* translators: %s: number of rows. */ 'countOne' => _n( '%s row', '%s rows', 1, 'rooster-lms' ), /* translators: %s: number of rows. */ 'countMany' => _n( '%s row', '%s rows', 2, 'rooster-lms' ), /* translators: 1: rows found, 2: rows in all. */ 'countSome' => __( '%1$s of %2$s rows', 'rooster-lms' ), 'empty' => __( 'Nothing here yet.', 'rooster-lms' ), 'noMatch' => __( 'Nothing matches the search.', 'rooster-lms' ), 'actions' => __( 'Actions', 'rooster-lms' ), /* translators: %s: row title. */ 'actionsFor' => __( 'Actions for %s', 'rooster-lms' ), 'newTab' => __( '(opens in a new tab)', 'rooster-lms' ), /* translators: %s: row title. */ 'rowsUnder' => __( 'Rows under %s', 'rooster-lms' ), /* translators: %s: row title. */ 'grip' => __( 'Move %s: drag, or press Alt with the up or down arrow', 'rooster-lms' ), /* translators: %s: row title. */ 'select' => __( 'Select %s', 'rooster-lms' ), 'selectAll' => __( 'Select all rows shown', 'rooster-lms' ), /* translators: %s: number of selected rows. */ 'selectedOne' => _n( '%s selected', '%s selected', 1, 'rooster-lms' ), /* translators: %s: number of selected rows. */ 'selectedMany' => _n( '%s selected', '%s selected', 2, 'rooster-lms' ), 'clear' => __( 'Clear selection', 'rooster-lms' ), 'rename' => __( 'Rename', 'rooster-lms' ), 'moveUp' => __( 'Move up', 'rooster-lms' ), 'moveDown' => __( 'Move down', 'rooster-lms' ), 'moveTo' => __( 'Move to…', 'rooster-lms' ), /* translators: %s: row title. */ 'moveTitle' => __( 'Move “%s”', 'rooster-lms' ), 'destination' => __( 'Destination', 'rooster-lms' ), 'position' => __( 'Position', 'rooster-lms' ), 'topLevel' => __( 'Top level', 'rooster-lms' ), 'move' => __( 'Move', 'rooster-lms' ), 'cancel' => __( 'Cancel', 'rooster-lms' ), 'nowhere' => __( 'There is nowhere else this row can go.', 'rooster-lms' ), /* translators: 1: row title, 2: new position, 3: rows in that place, 4: the parent row's title. */ 'moved' => __( 'Moved “%1$s” to position %2$s of %3$s in “%4$s”.', 'rooster-lms' ), /* translators: 1: row title, 2: new position, 3: rows at the top level. */ 'movedTop' => __( 'Moved “%1$s” to position %2$s of %3$s.', 'rooster-lms' ), /* translators: 1: old text, 2: new text. */ 'renamed' => __( 'Renamed “%1$s” to “%2$s”.', 'rooster-lms' ), /* translators: %s: row title. */ 'editLabel' => __( 'Edit “%s”', 'rooster-lms' ), 'undo' => __( 'Undo', 'rooster-lms' ), 'undone' => __( 'Undone.', 'rooster-lms' ), 'close' => __( 'Close', 'rooster-lms' ), 'saving' => __( 'Saving…', 'rooster-lms' ), /* translators: %s: the reason the server gave. */ 'failedMove' => __( 'Not saved: %s The row is back where it was.', 'rooster-lms' ), /* translators: %s: the reason the server gave. */ 'failedEdit' => __( 'Not saved: %s The previous text is back.', 'rooster-lms' ), 'noAnswer' => __( 'The site did not answer.', 'rooster-lms' ), 'loading' => __( 'Loading…', 'rooster-lms' ), /* translators: 1: row title, 2: the reason. */ 'loadFailed' => __( 'The rows under “%1$s” did not load: %2$s', 'rooster-lms' ), 'retry' => __( 'Try again', 'rooster-lms' ), /* translators: %s: row title. */ 'locked' => __( '“%s” cannot be moved.', 'rooster-lms' ), 'cycle' => __( 'A row cannot go inside itself.', 'rooster-lms' ), /* translators: %s: row title. */ 'kind' => __( '“%s” cannot go there.', 'rooster-lms' ), /* translators: %s: row title. */ 'parent' => __( '“%s” can only change places within its group.', 'rooster-lms' ), /* translators: %s: row title. */ 'unloaded' => __( 'Open “%s” first.', 'rooster-lms' ), /* translators: %s: row title. */ 'first' => __( '“%s” is already first.', 'rooster-lms' ), /* translators: %s: row title. */ 'last' => __( '“%s” is already last.', 'rooster-lms' ), ); } private static function fill( string $template, array $values ): string { $next = 0; return (string) preg_replace_callback( '/%(?:(\d+)\$)?[sd]/', static function ( array $m ) use ( $values, &$next ): string { $value = isset( $m[1] ) && '' !== $m[1] ? ( $values[ (int) $m[1] - 1 ] ?? null ) : ( $values[ $next++ ] ?? null ); return null === $value ? $m[0] : (string) $value; }, $template ); } private static function columns( array $columns ): array { $out = array(); foreach ( $columns as $column ) { if ( ! is_array( $column ) || ! isset( $column['key'] ) ) { continue; } $hideable = array() !== $out && ! empty( $column['hideable'] ) && '' !== (string) ( $column['label'] ?? '' ); $sort = in_array( $column['sort'] ?? '', array( 'text', 'number', 'date' ), true ) ? (string) $column['sort'] : ''; $class = isset( $column['class'] ) ? sanitize_html_class( (string) $column['class'] ) : ''; if ( 'number' === $sort && 'is-number' !== $class ) { $class = trim( $class . ' is-number' ); } $out[] = array( 'key' => sanitize_key( (string) $column['key'] ), 'label' => (string) ( $column['label'] ?? '' ), 'sticky' => ! empty( $column['sticky'] ), 'edit' => ! empty( $column['edit'] ), 'search' => array() === $out || ! empty( $column['search'] ), 'class' => $class, 'hideable' => $hideable, 'visible' => ! $hideable || ! isset( $column['visible'] ) || (bool) $column['visible'], 'sort' => $sort, ); } if ( array() === $out ) { $out[] = array( 'key' => 'title', 'label' => '', 'sticky' => false, 'edit' => false, 'search' => true, 'class' => '', 'hideable' => false, 'visible' => true, 'sort' => '', ); } return $out; } private static function sort_args( $sort ): ?array { if ( ! is_array( $sort ) ) { return null; } return array( 'mode' => 'server' === ( $sort['mode'] ?? '' ) ? 'server' : 'client', 'kinds' => array_values( array_map( 'sanitize_key', array_filter( (array) ( $sort['kinds'] ?? array() ), 'is_string' ) ) ), 'key' => sanitize_key( (string) ( $sort['key'] ?? '' ) ), 'dir' => 'desc' === ( $sort['dir'] ?? '' ) ? 'desc' : 'asc', 'url' => (string) ( $sort['url'] ?? '' ), 'address' => 'server' !== ( $sort['mode'] ?? '' ) && ! empty( $sort['address'] ), 'none' => ! empty( $sort['none'] ), ); } private static function normalise( array $rows, array $columns, bool $root = true ): array { $edit = array(); foreach ( $columns as $column ) { if ( $column['edit'] ) { $edit[] = $column['key']; } } $out = array(); foreach ( $rows as $row ) { if ( ! is_array( $row ) || ! isset( $row['id'] ) || '' === (string) $row['id'] ) { continue; } $fields = isset( $row['edit'] ) ? ( false === $row['edit'] ? array() : array_values( array_intersect( array_map( 'strval', (array) $row['edit'] ), array_column( $columns, 'key' ) ) ) ) : $edit; $data = array(); foreach ( (array) ( $row['data'] ?? array() ) as $key => $value ) { $key = sanitize_key( (string) $key ); if ( '' !== $key && is_scalar( $value ) ) { $data[ $key ] = (string) $value; } } $actions = array(); foreach ( (array) ( $row['actions'] ?? array() ) as $action ) { if ( is_array( $action ) && isset( $action['id'], $action['label'] ) ) { $actions[] = self::action( $action ); } } $out[] = array( 'id' => (string) $row['id'], 'parent' => isset( $row['parent'] ) && '' !== (string) $row['parent'] && '0' !== (string) $row['parent'] ? (string) $row['parent'] : ( $root ? '' : null ), 'kind' => sanitize_key( (string) ( $row['kind'] ?? 'row' ) ), 'title' => (string) ( $row['title'] ?? '' ), 'href' => (string) ( $row['href'] ?? '' ), 'cells' => (array) ( $row['cells'] ?? array() ), 'movable' => ! empty( $row['movable'] ), 'lazy' => ! empty( $row['lazy'] ), 'count' => max( 0, (int) ( $row['count'] ?? 0 ) ), 'open' => ! empty( $row['open'] ), 'edit' => $fields, 'actions' => $actions, 'selectable' => ! isset( $row['selectable'] ) || (bool) $row['selectable'], 'search' => (string) ( $row['search'] ?? '' ), 'class' => isset( $row['class'] ) ? implode( ' ', array_map( 'sanitize_html_class', (array) preg_split( '/\s+/', (string) $row['class'], -1, PREG_SPLIT_NO_EMPTY ) ) ) : '', 'data' => $data, 'facets' => is_array( $row['facets'] ?? null ) ? array_map( static fn( $v ): array => array_values( array_map( 'strval', (array) $v ) ), $row['facets'] ) : array(), 'sort' => is_array( $row['sort'] ?? null ) ? array_filter( $row['sort'], 'is_scalar' ) : array(), 'lang' => is_array( $row['lang'] ?? null ) && '' !== (string) ( $row['lang']['code'] ?? '' ) ? array( 'code' => (string) $row['lang']['code'], 'tip' => (string) ( $row['lang']['tip'] ?? '' ), ) : null, 'icon' => is_string( $row['icon'] ?? null ) && '' !== $row['icon'] ? KindIcon::kind( sanitize_key( $row['icon'] ) ) : '', ); } return $out; } private static function action( array $action ): array { $out = array( 'id' => sanitize_key( (string) $action['id'] ), 'label' => (string) $action['label'], ); if ( ! empty( $action['href'] ) ) { $out['href'] = esc_url_raw( (string) $action['href'] ); if ( 'view' === $out['id'] || ! empty( $action['blank'] ) ) { $out['blank'] = true; } } if ( ! empty( $action['danger'] ) ) { $out['danger'] = true; } return $out; } private static function actions_config( array $actions ): array { $out = array(); foreach ( $actions as $kind => $list ) { foreach ( (array) $list as $action ) { if ( is_array( $action ) && isset( $action['id'], $action['label'] ) ) { $out[ sanitize_key( (string) $kind ) ][] = self::action( $action ); } } } return $out; } private static function rules( array $rules ): array { $out = array(); foreach ( $rules as $kind => $rule ) { $rule = (array) $rule; $out[ sanitize_key( (string) $kind ) ] = array( 'parents' => array_values( array_map( static fn( $k ): string => sanitize_key( (string) $k ), (array) ( $rule['parents'] ?? array() ) ) ), 'across' => ! empty( $rule['across'] ), ); } return $out; } private static function kinds( array $kinds ): array { $out = array(); foreach ( $kinds as $kind => $def ) { $def = (array) $def; if ( isset( $def['count'][0], $def['count'][1] ) ) { $out[ sanitize_key( (string) $kind ) ] = array( 'count' => array( (string) $def['count'][0], (string) $def['count'][1] ) ); } } return $out; } private static function counted( array $rows, array $kinds ): int { if ( array() === $kinds ) { return count( $rows ); } return count( array_filter( $rows, static fn( array $r ): bool => in_array( $r['kind'], $kinds, true ) ) ); } private static function tree( array $rows ): array { $ids = array(); foreach ( $rows as $row ) { $ids[ $row['id'] ] = $row['parent']; } $out = array( '' => array() ); foreach ( $rows as $row ) { $parent = $row['parent']; if ( '' !== $parent && ( ! isset( $ids[ $parent ] ) || self::loops( $row['id'], $ids ) ) ) { $parent = ''; $row['parent'] = ''; } $out[ $parent ][] = $row; } return $out; } private static function loops( string $id, array $ids ): bool { $seen = array( $id => true ); $at = $ids[ $id ] ?? ''; while ( '' !== $at && isset( $ids[ $at ] ) ) { if ( isset( $seen[ $at ] ) ) { return true; } $seen[ $at ] = true; $at = $ids[ $at ]; } return false; } private static function walk( array $tree, string $parent_id, int $level, bool $shown, callable $visit, array &$seen = array() ): void { $list = $tree[ $parent_id ] ?? array(); $size = count( $list ); foreach ( $list as $i => $row ) { if ( isset( $seen[ $row['id'] ] ) ) { continue; } $seen[ $row['id'] ] = true; $kids = count( $tree[ $row['id'] ] ?? array() ); $visit( $row, $level, $i + 1, $size, $shown, $kids > 0 || ! $row['lazy'], $kids ); if ( $kids > 0 ) { self::walk( $tree, $row['id'], $level + 1, $shown && $row['open'], $visit, $seen ); } } } private static function head( array $columns, bool $select, bool $menu, array $l10n, ?array $sort = null ): string { $sticky = self::sticky_classes( $columns, $select ); $kit = Ui::kit_markup(); $th = $kit ? ' rooster-admin-table__th' : ''; $html = ''; if ( $select ) { $html .= ''; } foreach ( $columns as $i => $column ) { $label = esc_html( $column['label'] ); $aria = ''; if ( null !== $sort && '' !== $column['sort'] && '' !== $column['label'] ) { $current = ( 'server' === $sort['mode'] || $sort['address'] ) && $sort['key'] === $column['key']; $aria = ' aria-sort="' . ( $current ? ( 'desc' === $sort['dir'] ? 'descending' : 'ascending' ) : 'none' ) . '"'; $icon = $kit ? DataTable::sort_chevron() : ''; $link = $kit ? 'rooster-admin-table__sort' : self::BLOCK . '__sort'; if ( 'server' === $sort['mode'] ) { $href = add_query_arg( array( 'orderby' => $column['key'], 'order' => $current && 'asc' === $sort['dir'] ? 'desc' : 'asc', ), '' !== $sort['url'] ? $sort['url'] : remove_query_arg( array( 'orderby', 'order', 'paged' ) ) ); $label = '' . $label . $icon . ''; } else { $label = ''; } } $html .= ''; } if ( $menu ) { $html .= ''; } return $html . ''; } private static function sticky_classes( array $columns, bool $select ): array { $out = array( 'select' => '' ); $n = 0; $last = null; $still = true; foreach ( $columns as $i => $column ) { $out[ $i ] = ''; if ( ! $still || ! $column['sticky'] ) { $still = false; continue; } if ( 0 === $i && $select ) { $out['select'] = 'is-sticky is-sticky-' . ( ++$n ); $last = 'select'; } $out[ $i ] = 'is-sticky is-sticky-' . ( ++$n ); $last = $i; } if ( null !== $last ) { $out[ $last ] .= ' is-sticky-last'; } return $out; } private static function row( array $row, array $at, array $columns, array $kinds, bool $select, bool $menu, array $l10n ): string { $b = self::BLOCK; $sticky = self::sticky_classes( $columns, $select ); $children = $at['loaded'] ? $at['kids'] : $row['count']; $expandable = $children > 0; $open = $expandable && $row['open'] && $at['loaded']; $title = $row['title']; $attr = ' data-rl-tree-row data-id="' . esc_attr( $row['id'] ) . '" data-parent="' . esc_attr( (string) $row['parent'] ) . '" data-kind="' . esc_attr( $row['kind'] ) . '"'; if ( $row['movable'] ) { $attr .= ' data-movable="1"'; } if ( ! $at['loaded'] ) { $attr .= ' data-lazy="1"'; } if ( $row['count'] > 0 ) { $attr .= ' data-count="' . (int) $row['count'] . '"'; } if ( array() !== $row['edit'] ) { $attr .= ' data-edit="' . esc_attr( implode( ',', $row['edit'] ) ) . '"'; } if ( array() !== $row['actions'] ) { $attr .= ' data-actions="' . esc_attr( (string) wp_json_encode( $row['actions'] ) ) . '"'; } if ( '' !== $row['search'] ) { $attr .= ' data-search="' . esc_attr( $row['search'] ) . '"'; } foreach ( $row['data'] as $key => $value ) { $attr .= ' data-x-' . esc_attr( $key ) . '="' . esc_attr( $value ) . '"'; } if ( array() !== ( $row['facets'] ?? array() ) ) { $attr .= ' data-ra-f="' . esc_attr( (string) wp_json_encode( (object) $row['facets'], JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG ) ) . '"'; } if ( array() !== ( $row['sort'] ?? array() ) ) { $attr .= ' data-ra-s="' . esc_attr( (string) wp_json_encode( (object) $row['sort'], JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG ) ) . '"'; } $level = max( 1, $at['level'] ); $html = ''; if ( $select ) { $html .= ''; } foreach ( $columns as $i => $column ) { $classes = self::classes( $b . '__' . ( 0 === $i ? 'title' : 'cell' ), $column['class'], $sticky[ $i ] ); $fixed = ! empty( $at['grid'] ) && ! in_array( $column['key'], $row['edit'], true ) ? ' aria-readonly="true"' : ''; if ( 0 === $i ) { $html .= ''; continue; } $cell = $row['cells'][ $column['key'] ] ?? ''; $by = is_array( $cell ) && isset( $cell['sort'] ) && is_scalar( $cell['sort'] ) ? ' data-sort="' . esc_attr( (string) $cell['sort'] ) . '"' : ''; $html .= ''; } if ( $menu ) { $html .= ''; } return $html . ''; } private static function classes( string ...$names ): string { return implode( ' ', array_filter( array_map( 'trim', $names ), static fn( string $n ): bool => '' !== $n ) ); } public static function grip(): string { return ''; } public static function dots(): string { return ''; } private static function page_rows( array $tree, array $rows, ?array $allowed, array $kinds, int $size, int $paged ): array { $parent = array(); $kind_of = array(); foreach ( $rows as $r ) { $parent[ $r['id'] ] = (string) ( $r['parent'] ?? '' ); $kind_of[ $r['id'] ] = (string) ( $r['kind'] ?? '' ); } $order = array(); self::walk( $tree, '', 1, true, static function ( array $row ) use ( &$order ): void { $order[] = (string) $row['id']; } ); $items = array_values( array_filter( $order, static fn( string $id ): bool => in_array( $kind_of[ $id ] ?? '', $kinds, true ) && ( null === $allowed || isset( $allowed[ $id ] ) ) ) ); $last = max( 1, (int) ceil( count( $items ) / max( 1, $size ) ) ); $paged = max( 1, min( $paged, $last ) ); $keep = array_fill_keys( array_slice( $items, ( $paged - 1 ) * $size, $size ), true ); $out = array(); foreach ( $order as $id ) { if ( null !== $allowed && ! isset( $allowed[ $id ] ) ) { continue; } for ( $a = $id; '' !== $a; $a = $parent[ $a ] ?? '' ) { if ( isset( $keep[ $a ] ) ) { $out[ $id ] = true; break; } } } foreach ( array_keys( $keep ) as $id ) { for ( $a = $parent[ $id ] ?? ''; '' !== $a; $a = $parent[ $a ] ?? '' ) { $out[ $a ] = true; } } return array( 'allowed' => $out, 'matched' => count( $items ), 'on_page' => count( $keep ), 'paged' => $paged, ); } private static function facet_rows( array $rows, array $facets ): array { $parent = array(); $children = array(); foreach ( $rows as $r ) { $p = (string) ( $r['parent'] ?? '' ); $parent[ $r['id'] ] = $p; $children[ $p ][] = $r['id']; } $allowed = array(); $opened = array(); foreach ( $rows as $r ) { $values = $r['facets'] ?? array(); if ( array() === $values ) { continue; } $ok = true; foreach ( $facets as $key => $want ) { $asked = array_map( 'strval', (array) $want ); if ( array() === array_intersect( $asked, array_map( 'strval', (array) ( $values[ $key ] ?? array() ) ) ) ) { $ok = false; break; } } if ( ! $ok ) { continue; } $allowed[ $r['id'] ] = true; for ( $a = $parent[ $r['id'] ] ?? ''; '' !== $a && ! isset( $opened[ $a ] ); $a = $parent[ $a ] ?? '' ) { $allowed[ $a ] = true; $opened[ $a ] = true; } $stack = $children[ $r['id'] ] ?? array(); while ( array() !== $stack ) { $c = array_pop( $stack ); $allowed[ $c ] = true; array_push( $stack, ...( $children[ $c ] ?? array() ) ); } } return array( $allowed, $opened ); } }
' . $label . '' . esc_html( $l10n['actions'] ) . '
' . ( $row['selectable'] ? '' : '' ) . ''; $html .= $row['movable'] ? '' : ''; $html .= $expandable ? '' : ''; $label = '' !== $row['href'] ? '' . esc_html( $title ) . '' : esc_html( $title ); $lang = $row['lang']; if ( '' !== $row['icon'] ) { $html .= KindIcon::html( $row['icon'] ); } $html .= '' . $label . ''; $badge = Ui::kit_markup() ? 'rooster-admin-badge is-mono is-sm ' : 'rooster-lms-badge rooster-lms-badge--sm '; if ( null !== $lang ) { $code = esc_html( strtoupper( Language::primary( $lang['code'] ) ) ); $html .= Ui::kit_markup() ? '' . esc_html( $lang['tip'] ) . '' : '' . $code . ''; } $noun = $kinds[ $row['kind'] ]['count'] ?? null; if ( $expandable && is_array( $noun ) && isset( $noun[0], $noun[1] ) ) { $html .= '' . esc_html( self::fill( 1 === $children ? (string) $noun[0] : (string) $noun[1], array( Format::number( $children ) ) ) ) . ''; } $html .= ''; if ( is_array( $cell ) && ! isset( $cell['html'] ) ) { $html .= esc_html( (string) ( $cell['text'] ?? '' ) ); } elseif ( is_array( $cell ) ) { $html .= wp_kses( (string) ( $cell['html'] ?? '' ), is_admin() ? Kses::admin() : Kses::front() ); } elseif ( in_array( $column['key'], $row['edit'], true ) ) { $html .= '' . esc_html( (string) $cell ) . ''; } else { $html .= esc_html( (string) $cell ); } $html .= '