Skip to content

Instantly share code, notes, and snippets.

@Junex123
Created September 23, 2025 06:19
Show Gist options
  • Select an option

  • Save Junex123/da721e81a4b97d4f58c14ac5dc63528f to your computer and use it in GitHub Desktop.

Select an option

Save Junex123/da721e81a4b97d4f58c14ac5dc63528f to your computer and use it in GitHub Desktop.
<?php
/**
* List Table API: WP_Posts_List_Table class
*
* @package WordPress
* @subpackage Administration
* @since 3.1.0
*/
/**
* Core class used to implement displaying posts in a list table.
*
* @since 3.1.0
*
* @see WP_List_Table
*/
class WP_Posts_List_Table extends WP_List_Table {
/**
* Whether the items should be displayed hierarchically or linearly.
*
* @since 3.1.0
* @var bool
*/
protected $hierarchical_display;
/**
* Holds the number of pending comments for each post.
*
* @since 3.1.0
* @var array
*/
protected $comment_pending_count;
/**
* Holds the number of posts for this user.
*
* @since 3.1.0
* @var int
*/
private $user_posts_count;
/**
* Holds the number of posts which are sticky.
*
* @since 3.1.0
* @var int
*/
private $sticky_posts_count = 0;
private $is_trash;
/**
* Current level for output.
*
* @since 4.3.0
* @var int
*/
protected $current_level = 0;
/**
* Constructor.
*
* @since 3.1.0
*
* @see WP_List_Table::__construct() for more information on default arguments.
*
* @global WP_Post_Type $post_type_object Global post type object.
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param array $args An associative array of arguments.
*/
public function __construct( $args = array() ) {
global $post_type_object, $wpdb;
parent::__construct(
array(
'plural' => 'posts',
'screen' => isset( $args['screen'] ) ? $args['screen'] : null,
)
);
$post_type = $this->screen->post_type;
$post_type_object = get_post_type_object( $post_type );
$exclude_states = get_post_stati(
array(
'show_in_admin_all_list' => false,
)
);
$this->user_posts_count = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT( 1 )
FROM $wpdb->posts
WHERE post_type = %s
AND post_status NOT IN ( '" . implode( "','", $exclude_states ) . "' )
AND post_author = %d",
$post_type,
get_current_user_id()
)
);
if ( $this->user_posts_count
&& ! current_user_can( $post_type_object->cap->edit_others_posts )
&& empty( $_REQUEST['post_status'] ) && empty( $_REQUEST['all_posts'] )
&& empty( $_REQUEST['author'] ) && empty( $_REQUEST['show_sticky'] )
) {
$_GET['author'] = get_current_user_id();
}
$sticky_posts = get_option( 'sticky_posts' );
if ( 'post' === $post_type && $sticky_posts ) {
$sticky_posts = implode( ', ', array_map( 'absint', (array) $sticky_posts ) );
$this->sticky_posts_count = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT( 1 )
FROM $wpdb->posts
WHERE post_type = %s
AND post_status NOT IN ('trash', 'auto-draft')
AND ID IN ($sticky_posts)",
$post_type
)
);
}
}
/**
* Sets whether the table layout should be hierarchical or not.
*
* @since 4.2.0
*
* @param bool $display Whether the table layout should be hierarchical.
*/
public function set_hierarchical_display( $display ) {
$this->hierarchical_display = $display;
}
/**
* @return bool
*/
public function ajax_user_can() {
return current_user_can( get_post_type_object( $this->screen->post_type )->cap->edit_posts );
}
/**
* @global string $mode List table view mode.
* @global array $avail_post_stati
* @global WP_Query $wp_query WordPress Query object.
* @global int $per_page
*/
public function prepare_items() {
global $mode, $avail_post_stati, $wp_query, $per_page;
if ( ! empty( $_REQUEST['mode'] ) ) {
$mode = 'excerpt' === $_REQUEST['mode'] ? 'excerpt' : 'list';
set_user_setting( 'posts_list_mode', $mode );
} else {
$mode = get_user_setting( 'posts_list_mode', 'list' );
}
// Is going to call wp().
$avail_post_stati = wp_edit_posts_query();
$this->set_hierarchical_display(
is_post_type_hierarchical( $this->screen->post_type )
&& 'menu_order title' === $wp_query->query['orderby']
);
$post_type = $this->screen->post_type;
$per_page = $this->get_items_per_page( 'edit_' . $post_type . '_per_page' );
/** This filter is documented in wp-admin/includes/post.php */
$per_page = apply_filters( 'edit_posts_per_page', $per_page, $post_type );
if ( $this->hierarchical_display ) {
$total_items = $wp_query->post_count;
} elseif ( $wp_query->found_posts || $this->get_pagenum() === 1 ) {
$total_items = $wp_query->found_posts;
} else {
$post_counts = (array) wp_count_posts( $post_type, 'readable' );
if ( isset( $_REQUEST['post_status'] ) && in_array( $_REQUEST['post_status'], $avail_post_stati, true ) ) {
$total_items = $post_counts[ $_REQUEST['post_status'] ];
} elseif ( isset( $_REQUEST['show_sticky'] ) && $_REQUEST['show_sticky'] ) {
$total_items = $this->sticky_posts_count;
} elseif ( isset( $_GET['author'] ) && get_current_user_id() === (int) $_GET['author'] ) {
$total_items = $this->user_posts_count;
} else {
$total_items = array_sum( $post_counts );
// Subtract post types that are not included in the admin all list.
foreach ( get_post_stati( array( 'show_in_admin_all_list' => false ) ) as $state ) {
$total_items -= $post_counts[ $state ];
}
}
}
$this->is_trash = isset( $_REQUEST['post_status'] ) && 'trash' === $_REQUEST['post_status'];
$this->set_pagination_args(
array(
'total_items' => $total_items,
'per_page' => $per_page,
)
);
}
/**
* @return bool
*/
public function has_items() {
return have_posts();
}
/**
*/
public function no_items() {
if ( isset( $_REQUEST['post_status'] ) && 'trash' === $_REQUEST['post_status'] ) {
echo get_post_type_object( $this->screen->post_type )->labels->not_found_in_trash;
} else {
echo get_post_type_object( $this->screen->post_type )->labels->not_found;
}
}
/**
* Determines if the current view is the "All" view.
*
* @since 4.2.0
*
* @return bool Whether the current view is the "All" view.
*/
protected function is_base_request() {
$vars = $_GET;
unset( $vars['paged'] );
if ( empty( $vars ) ) {
return true;
} elseif ( 1 === count( $vars ) && ! empty( $vars['post_type'] ) ) {
return $this->screen->post_type === $vars['post_type'];
}
return 1 === count( $vars ) && ! empty( $vars['mode'] );
}
/**
* Creates a link to edit.php with params.
*
* @since 4.4.0
*
* @param string[] $args Associative array of URL parameters for the link.
* @param string $link_text Link text.
* @param string $css_class Optional. Class attribute. Default empty string.
* @return string The formatted link string.
*/
protected function get_edit_link( $args, $link_text, $css_class = '' ) {
$url = add_query_arg( $args, 'edit.php' );
$class_html = '';
$aria_current = '';
if ( ! empty( $css_class ) ) {
$class_html = sprintf(
' class="%s"',
esc_attr( $css_class )
);
if ( 'current' === $css_class ) {
$aria_current = ' aria-current="page"';
}
}
return sprintf(
'<a href="%s"%s%s>%s</a>',
esc_url( $url ),
$class_html,
$aria_current,
$link_text
);
}
/**
* @global array $locked_post_status This seems to be deprecated.
* @global array $avail_post_stati
* @return array
*/
protected function get_views() {
global $locked_post_status, $avail_post_stati;
$post_type = $this->screen->post_type;
if ( ! empty( $locked_post_status ) ) {
return array();
}
$status_links = array();
$num_posts = wp_count_posts( $post_type, 'readable' );
$total_posts = array_sum( (array) $num_posts );
$class = '';
$current_user_id = get_current_user_id();
$all_args = array( 'post_type' => $post_type );
$mine = '';
// Subtract post types that are not included in the admin all list.
foreach ( get_post_stati( array( 'show_in_admin_all_list' => false ) ) as $state ) {
$total_posts -= $num_posts->$state;
}
if ( $this->user_posts_count && $this->user_posts_count !== $total_posts ) {
if ( isset( $_GET['author'] ) && ( $current_user_id === (int) $_GET['author'] ) ) {
$class = 'current';
}
$mine_args = array(
'post_type' => $post_type,
'author' => $current_user_id,
);
$mine_inner_html = sprintf(
/* translators: %s: Number of posts. */
_nx(
'Mine <span class="count">(%s)</span>',
'Mine <span class="count">(%s)</span>',
$this->user_posts_count,
'posts'
),
number_format_i18n( $this->user_posts_count )
);
$mine = array(
'url' => esc_url( add_query_arg( $mine_args, 'edit.php' ) ),
'label' => $mine_inner_html,
'current' => isset( $_GET['author'] ) && ( $current_user_id === (int) $_GET['author'] ),
);
$all_args['all_posts'] = 1;
$class = '';
}
$all_inner_html = sprintf(
/* translators: %s: Number of posts. */
_nx(
'All <span class="count">(%s)</span>',
'All <span class="count">(%s)</span>',
$total_posts,
'posts'
),
number_format_i18n( $total_posts )
);
$status_links['all'] = array(
'url' => esc_url( add_query_arg( $all_args, 'edit.php' ) ),
'label' => $all_inner_html,
'current' => empty( $class ) && ( $this->is_base_request() || isset( $_REQUEST['all_posts'] ) ),
);
if ( $mine ) {
$status_links['mine'] = $mine;
}
foreach ( get_post_stati( array( 'show_in_admin_status_list' => true ), 'objects' ) as $status ) {
$class = '';
$status_name = $status->name;
if ( ! in_array( $status_name, $avail_post_stati, true ) || empty( $num_posts->$status_name ) ) {
continue;
}
if ( isset( $_REQUEST['post_status'] ) && $status_name === $_REQUEST['post_status'] ) {
$class = 'current';
}
$status_args = array(
'post_status' => $status_name,
'post_type' => $post_type,
);
$status_label = sprintf(
translate_nooped_plural( $status->label_count, $num_posts->$status_name ),
number_format_i18n( $num_posts->$status_name )
);
$status_links[ $status_name ] = array(
'url' => esc_url( add_query_arg( $status_args, 'edit.php' ) ),
'label' => $status_label,
'current' => isset( $_REQUEST['post_status'] ) && $status_name === $_REQUEST['post_status'],
);
}
if ( ! empty( $this->sticky_posts_count ) ) {
$class = ! empty( $_REQUEST['show_sticky'] ) ? 'current' : '';
$sticky_args = array(
'post_type' => $post_type,
'show_sticky' => 1,
);
$sticky_inner_html = sprintf(
/* translators: %s: Number of posts. */
_nx(
'Sticky <span class="count">(%s)</span>',
'Sticky <span class="count">(%s)</span>',
$this->sticky_posts_count,
'posts'
),
number_format_i18n( $this->sticky_posts_count )
);
$sticky_link = array(
'sticky' => array(
'url' => esc_url( add_query_arg( $sticky_args, 'edit.php' ) ),
'label' => $sticky_inner_html,
'current' => ! empty( $_REQUEST['show_sticky'] ),
),
);
// Sticky comes after Publish, or if not listed, after All.
$split = 1 + array_search( ( isset( $status_links['publish'] ) ? 'publish' : 'all' ), array_keys( $status_links ), true );
$status_links = array_merge( array_slice( $status_links, 0, $split ), $sticky_link, array_slice( $status_links, $split ) );
}
return $this->get_views_links( $status_links );
}
/**
* @return array
*/
protected function get_bulk_actions() {
$actions = array();
$post_type_obj = get_post_type_object( $this->screen->post_type );
if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
if ( $this->is_trash ) {
$actions['untrash'] = __( 'Restore' );
} else {
$actions['edit'] = __( 'Edit' );
}
}
if ( current_user_can( $post_type_obj->cap->delete_posts ) ) {
if ( $this->is_trash || ! EMPTY_TRASH_DAYS ) {
$actions['delete'] = __( 'Delete permanently' );
} else {
$actions['trash'] = __( 'Move to Trash' );
}
}
return $actions;
}
/**
* Displays a categories drop-down for filtering on the Posts list table.
*
* @since 4.6.0
*
* @global int $cat Currently selected category.
*
* @param string $post_type Post type slug.
*/
protected function categories_dropdown( $post_type ) {
global $cat;
/**
* Filters whether to remove the 'Categories' drop-down from the post list table.
*
* @since 4.6.0
*
* @param bool $disable Whether to disable the categories drop-down. Default false.
* @param string $post_type Post type slug.
*/
if ( false !== apply_filters( 'disable_categories_dropdown', false, $post_type ) ) {
return;
}
if ( is_object_in_taxonomy( $post_type, 'category' ) ) {
$dropdown_options = array(
'show_option_all' => get_taxonomy( 'category' )->labels->all_items,
'hide_empty' => 0,
'hierarchical' => 1,
'show_count' => 0,
'orderby' => 'name',
'selected' => $cat,
);
echo '<label class="screen-reader-text" for="cat">' . get_taxonomy( 'category' )->labels->filter_by_item . '</label>';
wp_dropdown_categories( $dropdown_options );
}
}
/**
* Displays a formats drop-down for filtering items.
*
* @since 5.2.0
* @access protected
*
* @param string $post_type Post type slug.
*/
protected function formats_dropdown( $post_type ) {
/**
* Filters whether to remove the 'Formats' drop-down from the post list table.
*
* @since 5.2.0
* @since 5.5.0 The `$post_type` parameter was added.
*
* @param bool $disable Whether to disable the drop-down. Default false.
* @param string $post_type Post type slug.
*/
if ( apply_filters( 'disable_formats_dropdown', false, $post_type ) ) {
return;
}
// Return if the post type doesn't have post formats or if we're in the Trash.
if ( ! is_object_in_taxonomy( $post_type, 'post_format' ) || $this->is_trash ) {
return;
}
// Make sure the dropdown shows only formats with a post count greater than 0.
$used_post_formats = get_terms(
array(
'taxonomy' => 'post_format',
'hide_empty' => true,
)
);
// Return if there are no posts using formats.
if ( ! $used_post_formats ) {
return;
}
$displayed_post_format = isset( $_GET['post_format'] ) ? $_GET['post_format'] : '';
?>
<label for="filter-by-format" class="screen-reader-text">
<?php
/* translators: Hidden accessibility text. */
_e( 'Filter by post format' );
?>
</label>
<select name="post_format" id="filter-by-format">
<option<?php selected( $displayed_post_format, '' ); ?> value=""><?php _e( 'All formats' ); ?></option>
<?php
foreach ( $used_post_formats as $used_post_format ) {
// Post format slug.
$slug = str_replace( 'post-format-', '', $used_post_format->slug );
// Pretty, translated version of the post format slug.
$pretty_name = get_post_format_string( $slug );
// Skip the standard post format.
if ( 'standard' === $slug ) {
continue;
}
?>
<option<?php selected( $displayed_post_format, $slug ); ?> value="<?php echo esc_attr( $slug ); ?>"><?php echo esc_html( $pretty_name ); ?></option>
<?php
}
?>
</select>
<?php
}
/**
* @param string $which
*/
protected function extra_tablenav( $which ) {
?>
<div class="alignleft actions">
<?php
if ( 'top' === $which ) {
ob_start();
$this->months_dropdown( $this->screen->post_type );
$this->categories_dropdown( $this->screen->post_type );
$this->formats_dropdown( $this->screen->post_type );
/**
* Fires before the Filter button on the Posts and Pages list tables.
*
* The Filter button allows sorting by date and/or category on the
* Posts list table, and sorting by date on the Pages list table.
*
* @since 2.1.0
* @since 4.4.0 The `$post_type` parameter was added.
* @since 4.6.0 The `$which` parameter was added.
*
* @param string $post_type The post type slug.
* @param string $which The location of the extra table nav markup:
* 'top' or 'bottom' for WP_Posts_List_Table,
* 'bar' for WP_Media_List_Table.
*/
do_action( 'restrict_manage_posts', $this->screen->post_type, $which );
$output = ob_get_clean();
if ( ! empty( $output ) ) {
echo $output;
submit_button( __( 'Filter' ), '', 'filter_action', false, array( 'id' => 'post-query-submit' ) );
}
}
if ( $this->is_trash && $this->has_items()
&& current_user_can( get_post_type_object( $this->screen->post_type )->cap->edit_others_posts )
) {
submit_button( __( 'Empty Trash' ), 'apply', 'delete_all', false );
}
?>
</div>
<?php
/**
* Fires immediately following the closing "actions" div in the tablenav for the posts
* list table.
*
* @since 4.4.0
*
* @param string $which The location of the extra table nav markup: 'top' or 'bottom'.
*/
do_action( 'manage_posts_extra_tablenav', $which );
}
/**
* @return string
*/
public function current_action() {
if ( isset( $_REQUEST['delete_all'] ) || isset( $_REQUEST['delete_all2'] ) ) {
return 'delete_all';
}
return parent::current_action();
}
/**
* @global string $mode List table view mode.
*
* @return array
*/
protected function get_table_classes() {
global $mode;
$mode_class = esc_attr( 'table-view-' . $mode );
return array(
'widefat',
'fixed',
'striped',
$mode_class,
is_post_type_hierarchical( $this->screen->post_type ) ? 'pages' : 'posts',
);
}
/**
* @return string[] Array of column titles keyed by their column name.
*/
public function get_columns() {
$post_type = $this->screen->post_type;
$posts_columns = array();
$posts_columns['cb'] = '<input type="checkbox" />';
/* translators: Posts screen column name. */
$posts_columns['title'] = _x( 'Title', 'column name' );
if ( post_type_supports( $post_type, 'author' ) ) {
$posts_columns['author'] = __( 'Author' );
}
$taxonomies = get_object_taxonomies( $post_type, 'objects' );
$taxonomies = wp_filter_object_list( $taxonomies, array( 'show_admin_column' => true ), 'and', 'name' );
/**
* Filters the taxonomy columns in the Posts list table.
*
* The dynamic portion of the hook name, `$post_type`, refers to the post
* type slug.
*
* Possible hook names include:
*
* - `manage_taxonomies_for_post_columns`
* - `manage_taxonomies_for_page_columns`
*
* @since 3.5.0
*
* @param string[] $taxonomies Array of taxonomy names to show columns for.
* @param string $post_type The post type.
*/
$taxonomies = apply_filters( "manage_taxonomies_for_{$post_type}_columns", $taxonomies, $post_type );
$taxonomies = array_filter( $taxonomies, 'taxonomy_exists' );
foreach ( $taxonomies as $taxonomy ) {
if ( 'category' === $taxonomy ) {
$column_key = 'categories';
} elseif ( 'post_tag' === $taxonomy ) {
$column_key = 'tags';
} else {
$column_key = 'taxonomy-' . $taxonomy;
}
$posts_columns[ $column_key ] = get_taxonomy( $taxonomy )->labels->name;
}
$post_status = ! empty( $_REQUEST['post_status'] ) ? $_REQUEST['post_status'] : 'all';
if ( post_type_supports( $post_type, 'comments' )
&& ! in_array( $post_status, array( 'pending', 'draft', 'future' ), true )
) {
$posts_columns['comments'] = sprintf(
'<span class="vers comment-grey-bubble" title="%1$s" aria-hidden="true"></span><span class="screen-reader-text">%2$s</span>',
esc_attr__( 'Comments' ),
/* translators: Hidden accessibility text. */
__( 'Comments' )
);
}
$posts_columns['date'] = __( 'Date' );
if ( 'page' === $post_type ) {
/**
* Filters the columns displayed in the Pages list table.
*
* @since 2.5.0
*
* @param string[] $posts_columns An associative array of column headings.
*/
$posts_columns = apply_filters( 'manage_pages_columns', $posts_columns );
} else {
/**
* Filters the columns displayed in the Posts list table.
*
* @since 1.5.0
*
* @param string[] $posts_columns An associative array of column headings.
* @param string $post_type The post type slug.
*/
$posts_columns = apply_filters( 'manage_posts_columns', $posts_columns, $post_type );
}
/**
* Filters the columns displayed in the Posts list table for a specific post type.
*
* The dynamic portion of the hook name, `$post_type`, refers to the post type slug.
*
* Possible hook names include:
*
* - `manage_post_posts_columns`
* - `manage_page_posts_columns`
*
* @since 3.0.0
*
* @param string[] $posts_columns An associative array of column headings.
*/
return apply_filters( "manage_{$post_type}_posts_columns", $posts_columns );
}
/**
* @return array
*/
protected function get_sortable_columns() {
$post_type = $this->screen->post_type;
if ( 'page' === $post_type ) {
if ( isset( $_GET['orderby'] ) ) {
$title_orderby_text = __( 'Table ordered by Title.' );
} else {
$title_orderby_text = __( 'Table ordered by Hierarchical Menu Order and Title.' );
}
$sortables = array(
'title' => array( 'title', false, __( 'Title' ), $title_orderby_text, 'asc' ),
'parent' => array( 'parent', false ),
'comments' => array( 'comment_count', false, __( 'Comments' ), __( 'Table ordered by Comments.' ) ),
'date' => array( 'date', true, __( 'Date' ), __( 'Table ordered by Date.' ) ),
);
} else {
$sortables = array(
'title' => array( 'title', false, __( 'Title' ), __( 'Table ordered by Title.' ) ),
'parent' => array( 'parent', false ),
'comments' => array( 'comment_count', false, __( 'Comments' ), __( 'Table ordered by Comments.' ) ),
'date' => array( 'date', true, __( 'Date' ), __( 'Table ordered by Date.' ), 'desc' ),
);
}
// Custom Post Types: there's a filter for that, see get_column_info().
return $sortables;
}
/**
* Generates the list table rows.
*
* @since 3.1.0
*
* @global WP_Query $wp_query WordPress Query object.
* @global int $per_page
*
* @param array $posts
* @param int $level
*/
public function display_rows( $posts = array(), $level = 0 ) {
global $wp_query, $per_page;
if ( empty( $posts ) ) {
$posts = $wp_query->posts;
}
add_filter( 'the_title', 'esc_html' );
if ( $this->hierarchical_display ) {
$this->_display_rows_hierarchical( $posts, $this->get_pagenum(), $per_page );
} else {
$this->_display_rows( $posts, $level );
}
}
/**
* @param array $posts
* @param int $level
*/
private function _display_rows( $posts, $level = 0 ) {
$post_type = $this->screen->post_type;
// Create array of post IDs.
$post_ids = array();
foreach ( $posts as $a_post ) {
$post_ids[] = $a_post->ID;
}
if ( post_type_supports( $post_type, 'comments' ) ) {
$this->comment_pending_count = get_pending_comments_num( $post_ids );
}
update_post_author_caches( $posts );
foreach ( $posts as $post ) {
$this->single_row( $post, $level );
}
}
/**
* @global wpdb $wpdb WordPress database abstraction object.
* @global WP_Post $post Global post object.
* @param array $pages
* @param int $pagenum
* @param int $per_page
*/
private function _display_rows_hierarchical( $pages, $pagenum = 1, $per_page = 20 ) {
global $wpdb;
$level = 0;
if ( ! $pages ) {
$pages = get_pages( array( 'sort_column' => 'menu_order' ) );
if ( ! $pages ) {
return;
}
}
/*
* Arrange pages into two parts: top level pages and children_pages.
* children_pages is two dimensional array. Example:
* children_pages[10][] contains all sub-pages whose parent is 10.
* It only takes O( N ) to arrange this and it takes O( 1 ) for subsequent lookup operations
* If searching, ignore hierarchy and treat everything as top level
*/
if ( empty( $_REQUEST['s'] ) ) {
$top_level_pages = array();
$children_pages = array();
foreach ( $pages as $page ) {
// Catch and repair bad pages.
if ( $page->post_parent === $page->ID ) {
$page->post_parent = 0;
$wpdb->update( $wpdb->posts, array( 'post_parent' => 0 ), array( 'ID' => $page->ID ) );
clean_post_cache( $page );
}
if ( $page->post_parent > 0 ) {
$children_pages[ $page->post_parent ][] = $page;
} else {
$top_level_pages[] = $page;
}
}
$pages = &$top_level_pages;
}
$count = 0;
$start = ( $pagenum - 1 ) * $per_page;
$end = $start + $per_page;
$to_display = array();
foreach ( $pages as $page ) {
if ( $count >= $end ) {
break;
}
if ( $count >= $start ) {
$to_display[ $page->ID ] = $level;
}
++$count;
if ( isset( $children_pages ) ) {
$this->_page_rows( $children_pages, $count, $page->ID, $level + 1, $pagenum, $per_page, $to_display );
}
}
// If it is the last pagenum and there are orphaned pages, display them with paging as well.
if ( isset( $children_pages ) && $count < $end ) {
foreach ( $children_pages as $orphans ) {
foreach ( $orphans as $op ) {
if ( $count >= $end ) {
break;
}
if ( $count >= $start ) {
$to_display[ $op->ID ] = 0;
}
++$count;
}
}
}
$ids = array_keys( $to_display );
_prime_post_caches( $ids );
$_posts = array_map( 'get_post', $ids );
update_post_author_caches( $_posts );
if ( ! isset( $GLOBALS['post'] ) ) {
$GLOBALS['post'] = reset( $ids );
}
foreach ( $to_display as $page_id => $level ) {
echo "\t";
$this->single_row( $page_id, $level );
}
}
/**
* Displays the nested hierarchy of sub-pages together with paging
* support, based on a top level page ID.
*
* @since 3.1.0 (Standalone function exists since 2.6.0)
* @since 4.2.0 Added the `$to_display` parameter.
*
* @param array $children_pages
* @param int $count
* @param int $parent_page
* @param int $level
* @param int $pagenum
* @param int $per_page
* @param array $to_display List of pages to be displayed. Passed by reference.
*/
private function _page_rows( &$children_pages, &$count, $parent_page, $level, $pagenum, $per_page, &$to_display ) {
if ( ! isset( $children_pages[ $parent_page ] ) ) {
return;
}
$start = ( $pagenum - 1 ) * $per_page;
$end = $start + $per_page;
foreach ( $children_pages[ $parent_page ] as $page ) {
if ( $count >= $end ) {
break;
}
// If the page starts in a subtree, print the parents.
if ( $count === $start && $page->post_parent > 0 ) {
$my_parents = array();
$my_parent = $page->post_parent;
while ( $my_parent ) {
// Get the ID from the list or the attribute if my_parent is an object.
$parent_id = $my_parent;
if ( is_object( $my_parent ) ) {
$parent_id = $my_parent->ID;
}
$my_parent = get_post( $parent_id );
$my_parents[] = $my_parent;
if ( ! $my_parent->post_parent ) {
break;
}
$my_parent = $my_parent->post_parent;
}
$num_parents = count( $my_parents );
while ( $my_parent = array_pop( $my_parents ) ) {
$to_display[ $my_parent->ID ] = $level - $num_parents;
--$num_parents;
}
}
if ( $count >= $start ) {
$to_display[ $page->ID ] = $level;
}
++$count;
$this->_page_rows( $children_pages, $count, $page->ID, $level + 1, $pagenum, $per_page, $to_display );
}
unset( $children_pages[ $parent_page ] ); // Required in order to keep track of orphans.
}
/**
* Handles the checkbox column output.
*
* @since 4.3.0
* @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
*
* @param WP_Post $item The current WP_Post object.
*/
public function column_cb( $item ) {
// Restores the more descriptive, specific name for use within this method.
$post = $item;
$show = current_user_can( 'edit_post', $post->ID );
/**
* Filters whether to show the bulk edit checkbox for a post in its list table.
*
* By default the checkbox is only shown if the current user can edit the post.
*
* @since 5.7.0
*
* @param bool $show Whether to show the checkbox.
* @param WP_Post $post The current WP_Post object.
*/
if ( apply_filters( 'wp_list_table_show_post_checkbox', $show, $post ) ) :
?>
<input id="cb-select-<?php the_ID(); ?>" type="checkbox" name="post[]" value="<?php the_ID(); ?>" />
<label for="cb-select-<?php the_ID(); ?>">
<span class="screen-reader-text">
<?php
/* translators: %s: Post title. */
printf( __( 'Select %s' ), _draft_or_post_title() );
?>
</span>
</label>
<div class="locked-indicator">
<span class="locked-indicator-icon" aria-hidden="true"></span>
<span class="screen-reader-text">
<?php
printf(
/* translators: Hidden accessibility text. %s: Post title. */
__( '&#8220;%s&#8221; is locked' ),
_draft_or_post_title()
);
?>
</span>
</div>
<?php
endif;
}
/**
* @since 4.3.0
*
* @param WP_Post $post
* @param string $classes
* @param string $data
* @param string $primary
*/
protected function _column_title( $post, $classes, $data, $primary ) {
echo '<td class="' . $classes . ' page-title" ', $data, '>';
echo $this->column_title( $post );
echo $this->handle_row_actions( $post, 'title', $primary );
echo '</td>';
}
/**
* Handles the title column output.
*
* @since 4.3.0
*
* @global string $mode List table view mode.
*
* @param WP_Post $post The current WP_Post object.
*/
public function column_title( $post ) {
global $mode;
if ( $this->hierarchical_display ) {
if ( 0 === $this->current_level && (int) $post->post_parent > 0 ) {
// Sent level 0 by accident, by default, or because we don't know the actual level.
$find_main_page = (int) $post->post_parent;
while ( $find_main_page > 0 ) {
$parent = get_post( $find_main_page );
if ( is_null( $parent ) ) {
break;
}
++$this->current_level;
$find_main_page = (int) $parent->post_parent;
if ( ! isset( $parent_name ) ) {
/** This filter is documented in wp-includes/post-template.php */
$parent_name = apply_filters( 'the_title', $parent->post_title, $parent->ID );
}
}
}
}
$can_edit_post = current_user_can( 'edit_post', $post->ID );
if ( $can_edit_post && 'trash' !== $post->post_status ) {
$lock_holder = wp_check_post_lock( $post->ID );
if ( $lock_holder ) {
$lock_holder = get_userdata( $lock_holder );
$locked_avatar = get_avatar( $lock_holder->ID, 18 );
/* translators: %s: User's display name. */
$locked_text = esc_html( sprintf( __( '%s is currently editing' ), $lock_holder->display_name ) );
} else {
$locked_avatar = '';
$locked_text = '';
}
echo '<div class="locked-info"><span class="locked-avatar">' . $locked_avatar . '</span> <span class="locked-text">' . $locked_text . "</span></div>\n";
}
$pad = str_repeat( '&#8212; ', $this->current_level );
echo '<strong>';
$title = _draft_or_post_title();
if ( $can_edit_post && 'trash' !== $post->post_status ) {
printf(
'<a class="row-title" href="%s" aria-label="%s">%s%s</a>',
get_edit_post_link( $post->ID ),
/* translators: %s: Post title. */
esc_attr( sprintf( __( '&#8220;%s&#8221; (Edit)' ), $title ) ),
$pad,
$title
);
} else {
printf(
'<span>%s%s</span>',
$pad,
$title
);
}
_post_states( $post );
if ( isset( $parent_name ) ) {
$post_type_object = get_post_type_object( $post->post_type );
echo ' | ' . $post_type_object->labels->parent_item_colon . ' ' . esc_html( $parent_name );
}
echo "</strong>\n";
if ( 'excerpt' === $mode
&& ! is_post_type_hierarchical( $this->screen->post_type )
&& current_user_can( 'read_post', $post->ID )
) {
if ( post_password_required( $post ) ) {
echo '<span class="protected-post-excerpt">' . esc_html( get_the_excerpt() ) . '</span>';
} else {
echo esc_html( get_the_excerpt() );
}
}
/** This filter is documented in wp-admin/includes/class-wp-posts-list-table.php */
$quick_edit_enabled = apply_filters( 'quick_edit_enabled_for_post_type', true, $post->post_type );
if ( $quick_edit_enabled ) {
get_inline_data( $post );
}
}
/**
* Handles the post date column output.
*
* @since 4.3.0
*
* @global string $mode List table view mode.
*
* @param WP_Post $post The current WP_Post object.
*/
public function column_date( $post ) {
global $mode;
if ( '0000-00-00 00:00:00' === $post->post_date ) {
$t_time = __( 'Unpublished' );
$time_diff = 0;
} else {
$t_time = sprintf(
/* translators: 1: Post date, 2: Post time. */
__( '%1$s at %2$s' ),
/* translators: Post date format. See https://www.php.net/manual/datetime.format.php */
get_the_time( __( 'Y/m/d' ), $post ),
/* translators: Post time format. See https://www.php.net/manual/datetime.format.php */
get_the_time( __( 'g:i a' ), $post )
);
$time = get_post_timestamp( $post );
$time_diff = time() - $time;
}
if ( 'publish' === $post->post_status ) {
$status = __( 'Published' );
} elseif ( 'future' === $post->post_status ) {
if ( $time_diff > 0 ) {
$status = '<strong class="error-message">' . __( 'Missed schedule' ) . '</strong>';
} else {
$status = __( 'Scheduled' );
}
} else {
$status = __( 'Last Modified' );
}
/**
* Filters the status text of the post.
*
* @since 4.8.0
*
* @param string $status The status text.
* @param WP_Post $post Post object.
* @param string $column_name The column name.
* @param string $mode The list display mode ('excerpt' or 'list').
*/
$status = apply_filters( 'post_date_column_status', $status, $post, 'date', $mode );
if ( $status ) {
echo $status . '<br />';
}
/**
* Filters the published, scheduled, or unpublished time of the post.
*
* @since 2.5.1
* @since 5.5.0 Removed the difference between 'excerpt' and 'list' modes.
* The published time and date are both displayed now,
* which is equivalent to the previous 'excerpt' mode.
*
* @param string $t_time The published time.
* @param WP_Post $post Post object.
* @param string $column_name The column name.
* @param string $mode The list display mode ('excerpt' or 'list').
*/
echo apply_filters( 'post_date_column_time', $t_time, $post, 'date', $mode );
}
/**
* Handles the comments column output.
*
* @since 4.3.0
*
* @param WP_Post $post The current WP_Post object.
*/
public function column_comments( $post ) {
?>
<div class="post-com-count-wrapper">
<?php
$pending_comments = isset( $this->comment_pending_count[ $post->ID ] ) ? $this->comment_pending_count[ $post->ID ] : 0;
$this->comments_bubble( $post->ID, $pending_comments );
?>
</div>
<?php
}
/**
* Handles the post author column output.
*
* @since 4.3.0
* @since 6.8.0 Added fallback text when author's name is unknown.
*
* @param WP_Post $post The current WP_Post object.
*/
public function column_author( $post ) {
$author = get_the_author();
if ( ! empty( $author ) ) {
$args = array(
'post_type' => $post->post_type,
'author' => get_the_author_meta( 'ID' ),
);
echo $this->get_edit_link( $args, esc_html( $author ) );
} else {
echo '<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">' . __( '(no author)' ) . '</span>';
}
}
/**
* Handles the default column output.
*
* @since 4.3.0
* @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
*
* @param WP_Post $item The current WP_Post object.
* @param string $column_name The current column name.
*/
public function column_default( $item, $column_name ) {
// Restores the more descriptive, specific name for use within this method.
$post = $item;
if ( 'categories' === $column_name ) {
$taxonomy = 'category';
} elseif ( 'tags' === $column_name ) {
$taxonomy = 'post_tag';
} elseif ( str_starts_with( $column_name, 'taxonomy-' ) ) {
$taxonomy = substr( $column_name, 9 );
} else {
$taxonomy = false;
}
if ( $taxonomy ) {
$taxonomy_object = get_taxonomy( $taxonomy );
$terms = get_the_terms( $post->ID, $taxonomy );
if ( is_array( $terms ) ) {
$term_links = array();
foreach ( $terms as $t ) {
$posts_in_term_qv = array();
if ( 'post' !== $post->post_type ) {
$posts_in_term_qv['post_type'] = $post->post_type;
}
if ( $taxonomy_object->query_var ) {
$posts_in_term_qv[ $taxonomy_object->query_var ] = $t->slug;
} else {
$posts_in_term_qv['taxonomy'] = $taxonomy;
$posts_in_term_qv['term'] = $t->slug;
}
$label = esc_html( sanitize_term_field( 'name', $t->name, $t->term_id, $taxonomy, 'display' ) );
$term_links[] = $this->get_edit_link( $posts_in_term_qv, $label );
}
/**
* Filters the links in `$taxonomy` column of edit.php.
*
* @since 5.2.0
*
* @param string[] $term_links Array of term editing links.
* @param string $taxonomy Taxonomy name.
* @param WP_Term[] $terms Array of term objects appearing in the post row.
*/
$term_links = apply_filters( 'post_column_taxonomy_links', $term_links, $taxonomy, $terms );
echo implode( wp_get_list_item_separator(), $term_links );
} else {
echo '<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">' . $taxonomy_object->labels->no_terms . '</span>';
}
return;
}
if ( is_post_type_hierarchical( $post->post_type ) ) {
/**
* Fires in each custom column on the Posts list table.
*
* This hook only fires if the current post type is hierarchical,
* such as pages.
*
* @since 2.5.0
*
* @param string $column_name The name of the column to display.
* @param int $post_id The current post ID.
*/
do_action( 'manage_pages_custom_column', $column_name, $post->ID );
} else {
/**
* Fires in each custom column in the Posts list table.
*
* This hook only fires if the current post type is non-hierarchical,
* such as posts.
*
* @since 1.5.0
*
* @param string $column_name The name of the column to display.
* @param int $post_id The current post ID.
*/
do_action( 'manage_posts_custom_column', $column_name, $post->ID );
}
/**
* Fires for each custom column of a specific post type in the Posts list table.
*
* The dynamic portion of the hook name, `$post->post_type`, refers to the post type.
*
* Possible hook names include:
*
* - `manage_post_posts_custom_column`
* - `manage_page_posts_custom_column`
*
* @since 3.1.0
*
* @param string $column_name The name of the column to display.
* @param int $post_id The current post ID.
*/
do_action( "manage_{$post->post_type}_posts_custom_column", $column_name, $post->ID );
}
/**
* @global WP_Post $post Global post object.
*
* @param int|WP_Post $post
* @param int $level
*/
public function single_row( $post, $level = 0 ) {
$global_post = get_post();
$post = get_post( $post );
$this->current_level = $level;
$GLOBALS['post'] = $post;
setup_postdata( $post );
$classes = 'iedit author-' . ( get_current_user_id() === (int) $post->post_author ? 'self' : 'other' );
$lock_holder = wp_check_post_lock( $post->ID );
if ( $lock_holder ) {
$classes .= ' wp-locked';
}
if ( $post->post_parent ) {
$count = count( get_post_ancestors( $post->ID ) );
$classes .= ' level-' . $count;
} else {
$classes .= ' level-0';
}
?>
<tr id="post-<?php echo $post->ID; ?>" class="<?php echo implode( ' ', get_post_class( $classes, $post->ID ) ); ?>">
<?php $this->single_row_columns( $post ); ?>
</tr>
<?php
$GLOBALS['post'] = $global_post;
}
/**
* Gets the name of the default primary column.
*
* @since 4.3.0
*
* @return string Name of the default primary column, in this case, 'title'.
*/
protected function get_default_primary_column_name() {
return 'title';
}
/**
* Generates and displays row action links.
*
* @since 4.3.0
* @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
*
* @param WP_Post $item Post being acted upon.
* @param string $column_name Current column name.
* @param string $primary Primary column name.
* @return string Row actions output for posts, or an empty string
* if the current column is not the primary column.
*/
protected function handle_row_actions( $item, $column_name, $primary ) {
if ( $primary !== $column_name ) {
return '';
}
// Restores the more descriptive, specific name for use within this method.
$post = $item;
$post_type_object = get_post_type_object( $post->post_type );
$can_edit_post = current_user_can( 'edit_post', $post->ID );
$actions = array();
$title = _draft_or_post_title();
if ( $can_edit_post && 'trash' !== $post->post_status ) {
$actions['edit'] = sprintf(
'<a href="%s" aria-label="%s">%s</a>',
get_edit_post_link( $post->ID ),
/* translators: %s: Post title. */
esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;' ), $title ) ),
__( 'Edit' )
);
/**
* Filters whether Quick Edit should be enabled for the given post type.
*
* @since 6.4.0
*
* @param bool $enable Whether to enable the Quick Edit functionality. Default true.
* @param string $post_type Post type name.
*/
$quick_edit_enabled = apply_filters( 'quick_edit_enabled_for_post_type', true, $post->post_type );
if ( $quick_edit_enabled && 'wp_block' !== $post->post_type ) {
$actions['inline hide-if-no-js'] = sprintf(
'<button type="button" class="button-link editinline" aria-label="%s" aria-expanded="false">%s</button>',
/* translators: %s: Post title. */
esc_attr( sprintf( __( 'Quick edit &#8220;%s&#8221; inline' ), $title ) ),
__( 'Quick&nbsp;Edit' )
);
}
}
if ( current_user_can( 'delete_post', $post->ID ) ) {
if ( 'trash' === $post->post_status ) {
$actions['untrash'] = sprintf(
'<a href="%s" aria-label="%s">%s</a>',
wp_nonce_url( admin_url( sprintf( $post_type_object->_edit_link . '&amp;action=untrash', $post->ID ) ), 'untrash-post_' . $post->ID ),
/* translators: %s: Post title. */
esc_attr( sprintf( __( 'Restore &#8220;%s&#8221; from the Trash' ), $title ) ),
__( 'Restore' )
);
} elseif ( EMPTY_TRASH_DAYS ) {
$actions['trash'] = sprintf(
'<a href="%s" class="submitdelete" aria-label="%s">%s</a>',
get_delete_post_link( $post->ID ),
/* translators: %s: Post title. */
esc_attr( sprintf( __( 'Move &#8220;%s&#8221; to the Trash' ), $title ) ),
_x( 'Trash', 'verb' )
);
}
if ( 'trash' === $post->post_status || ! EMPTY_TRASH_DAYS ) {
$actions['delete'] = sprintf(
'<a href="%s" class="submitdelete" aria-label="%s">%s</a>',
get_delete_post_link( $post->ID, '', true ),
/* translators: %s: Post title. */
esc_attr( sprintf( __( 'Delete &#8220;%s&#8221; permanently' ), $title ) ),
__( 'Delete Permanently' )
);
}
}
if ( is_post_type_viewable( $post_type_object ) ) {
if ( in_array( $post->post_status, array( 'pending', 'draft', 'future' ), true ) ) {
if ( $can_edit_post ) {
$preview_link = get_preview_post_link( $post );
$actions['view'] = sprintf(
'<a href="%s" rel="bookmark" aria-label="%s">%s</a>',
esc_url( $preview_link ),
/* translators: %s: Post title. */
esc_attr( sprintf( __( 'Preview &#8220;%s&#8221;' ), $title ) ),
__( 'Preview' )
);
}
} elseif ( 'trash' !== $post->post_status ) {
$actions['view'] = sprintf(
'<a href="%s" rel="bookmark" aria-label="%s">%s</a>',
get_permalink( $post->ID ),
/* translators: %s: Post title. */
esc_attr( sprintf( __( 'View &#8220;%s&#8221;' ), $title ) ),
__( 'View' )
);
}
}
if ( 'wp_block' === $post->post_type ) {
$actions['export'] = sprintf(
'<button type="button" class="wp-list-reusable-blocks__export button-link" data-id="%s" aria-label="%s">%s</button>',
$post->ID,
/* translators: %s: Post title. */
esc_attr( sprintf( __( 'Export &#8220;%s&#8221; as JSON' ), $title ) ),
__( 'Export as JSON' )
);
}
if ( is_post_type_hierarchical( $post->post_type ) ) {
/**
* Filters the array of row action links on the Pages list table.
*
* The filter is evaluated only for hierarchical post types.
*
* @since 2.8.0
*
* @param string[] $actions An array of row action links. Defaults are
* 'Edit', 'Quick Edit', 'Restore', 'Trash',
* 'Delete Permanently', 'Preview', and 'View'.
* @param WP_Post $post The post object.
*/
$actions = apply_filters( 'page_row_actions', $actions, $post );
} else {
/**
* Filters the array of row action links on the Posts list table.
*
* The filter is evaluated only for non-hierarchical post types.
*
* @since 2.8.0
*
* @param string[] $actions An array of row action links. Defaults are
* 'Edit', 'Quick Edit', 'Restore', 'Trash',
* 'Delete Permanently', 'Preview', and 'View'.
* @param WP_Post $post The post object.
*/
$actions = apply_filters( 'post_row_actions', $actions, $post );
}
return $this->row_actions( $actions );
}
/**
* Outputs the hidden row displayed when inline editing
*
* @since 3.1.0
*
* @global string $mode List table view mode.
*/
public function inline_edit() {
global $mode;
$screen = $this->screen;
$post = get_default_post_to_edit( $screen->post_type );
$post_type_object = get_post_type_object( $screen->post_type );
$taxonomy_names = get_object_taxonomies( $screen->post_type );
$hierarchical_taxonomies = array();
$flat_taxonomies = array();
foreach ( $taxonomy_names as $taxonomy_name ) {
$taxonomy = get_taxonomy( $taxonomy_name );
$show_in_quick_edit = $taxonomy->show_in_quick_edit;
/**
* Filters whether the current taxonomy should be shown in the Quick Edit panel.
*
* @since 4.2.0
*
* @param bool $show_in_quick_edit Whether to show the current taxonomy in Quick Edit.
* @param string $taxonomy_name Taxonomy name.
* @param string $post_type Post type of current Quick Edit post.
*/
if ( ! apply_filters( 'quick_edit_show_taxonomy', $show_in_quick_edit, $taxonomy_name, $screen->post_type ) ) {
continue;
}
if ( $taxonomy->hierarchical ) {
$hierarchical_taxonomies[] = $taxonomy;
} else {
$flat_taxonomies[] = $taxonomy;
}
}
$m = ( isset( $mode ) && 'excerpt' === $mode ) ? 'excerpt' : 'list';
$can_publish = current_user_can( $post_type_object->cap->publish_posts );
$core_columns = array(
'cb' => true,
'date' => true,
'title' => true,
'categories' => true,
'tags' => true,
'comments' => true,
'author' => true,
);
?>
<form method="get">
<table style="display: none"><tbody id="inlineedit">
<?php
$hclass = count( $hierarchical_taxonomies ) ? 'post' : 'page';
$inline_edit_classes = "inline-edit-row inline-edit-row-$hclass";
$bulk_edit_classes = "bulk-edit-row bulk-edit-row-$hclass bulk-edit-{$screen->post_type}";
$quick_edit_classes = "quick-edit-row quick-edit-row-$hclass inline-edit-{$screen->post_type}";
$bulk = 0;
while ( $bulk < 2 ) :
$classes = $inline_edit_classes . ' ';
$classes .= $bulk ? $bulk_edit_classes : $quick_edit_classes;
?>
<tr id="<?php echo $bulk ? 'bulk-edit' : 'inline-edit'; ?>" class="<?php echo $classes; ?>" style="display: none">
<td colspan="<?php echo $this->get_column_count(); ?>" class="colspanchange">
<div class="inline-edit-wrapper" role="region" aria-labelledby="<?php echo $bulk ? 'bulk' : 'quick'; ?>-edit-legend">
<fieldset class="inline-edit-col-left">
<legend class="inline-edit-legend" id="<?php echo $bulk ? 'bulk' : 'quick'; ?>-edit-legend"><?php echo $bulk ? __( 'Bulk Edit' ) : __( 'Quick Edit' ); ?></legend>
<div class="inline-edit-col">
<?php if ( post_type_supports( $screen->post_type, 'title' ) ) : ?>
<?php if ( $bulk ) : ?>
<div id="bulk-title-div">
<div id="bulk-titles"></div>
</div>
<?php else : // $bulk ?>
<label>
<span class="title"><?php _e( 'Title' ); ?></span>
<span class="input-text-wrap"><input type="text" name="post_title" class="ptitle" value="" /></span>
</label>
<?php if ( is_post_type_viewable( $screen->post_type ) ) : ?>
<label>
<span class="title"><?php _e( 'Slug' ); ?></span>
<span class="input-text-wrap"><input type="text" name="post_name" value="" autocomplete="off" spellcheck="false" /></span>
</label>
<?php endif; // is_post_type_viewable() ?>
<?php endif; // $bulk ?>
<?php endif; // post_type_supports( ... 'title' ) ?>
<?php if ( ! $bulk ) : ?>
<fieldset class="inline-edit-date">
<legend><span class="title"><?php _e( 'Date' ); ?></span></legend>
<?php touch_time( 1, 1, 0, 1 ); ?>
</fieldset>
<br class="clear" />
<?php endif; // $bulk ?>
<?php
if ( post_type_supports( $screen->post_type, 'author' ) ) {
$authors_dropdown = '';
if ( current_user_can( $post_type_object->cap->edit_others_posts ) ) {
$dropdown_name = 'post_author';
$dropdown_class = 'authors';
if ( wp_is_large_user_count() ) {
$authors_dropdown = sprintf( '<select name="%s" class="%s hidden"></select>', esc_attr( $dropdown_name ), esc_attr( $dropdown_class ) );
} else {
$users_opt = array(
'hide_if_only_one_author' => false,
'capability' => array( $post_type_object->cap->edit_posts ),
'name' => $dropdown_name,
'class' => $dropdown_class,
'multi' => 1,
'echo' => 0,
'show' => 'display_name_with_login',
);
if ( $bulk ) {
$users_opt['show_option_none'] = __( '&mdash; No Change &mdash;' );
}
/**
* Filters the arguments used to generate the Quick Edit authors drop-down.
*
* @since 5.6.0
*
* @see wp_dropdown_users()
*
* @param array $users_opt An array of arguments passed to wp_dropdown_users().
* @param bool $bulk A flag to denote if it's a bulk action.
*/
$users_opt = apply_filters( 'quick_edit_dropdown_authors_args', $users_opt, $bulk );
$authors = wp_dropdown_users( $users_opt );
if ( $authors ) {
$authors_dropdown = '<label class="inline-edit-author">';
$authors_dropdown .= '<span class="title">' . __( 'Author' ) . '</span>';
$authors_dropdown .= $authors;
$authors_dropdown .= '</label>';
}
}
} // current_user_can( 'edit_others_posts' )
if ( ! $bulk ) {
echo $authors_dropdown;
}
} // post_type_supports( ... 'author' )
?>
<?php if ( ! $bulk && $can_publish ) : ?>
<div class="inline-edit-group wp-clearfix">
<label class="alignleft">
<span class="title"><?php _e( 'Password' ); ?></span>
<span class="input-text-wrap"><input type="text" name="post_password" class="inline-edit-password-input" value="" /></span>
</label>
<span class="alignleft inline-edit-or">
<?php
/* translators: Between password field and private checkbox on post quick edit interface. */
_e( '&ndash;OR&ndash;' );
?>
</span>
<label class="alignleft inline-edit-private">
<input type="checkbox" name="keep_private" value="private" />
<span class="checkbox-title"><?php _e( 'Private' ); ?></span>
</label>
</div>
<?php endif; ?>
</div>
</fieldset>
<?php if ( count( $hierarchical_taxonomies ) && ! $bulk ) : ?>
<fieldset class="inline-edit-col-center inline-edit-categories">
<div class="inline-edit-col">
<?php foreach ( $hierarchical_taxonomies as $taxonomy ) : ?>
<span class="title inline-edit-categories-label"><?php echo esc_html( $taxonomy->labels->name ); ?></span>
<input type="hidden" name="<?php echo ( 'category' === $taxonomy->name ) ? 'post_category[]' : 'tax_input[' . esc_attr( $taxonomy->name ) . '][]'; ?>" value="0" />
<ul class="cat-checklist <?php echo esc_attr( $taxonomy->name ); ?>-checklist">
<?php wp_terms_checklist( 0, array( 'taxonomy' => $taxonomy->name ) ); ?>
</ul>
<?php endforeach; // $hierarchical_taxonomies as $taxonomy ?>
</div>
</fieldset>
<?php endif; // count( $hierarchical_taxonomies ) && ! $bulk ?>
<fieldset class="inline-edit-col-right">
<div class="inline-edit-col">
<?php
if ( post_type_supports( $screen->post_type, 'author' ) && $bulk ) {
echo $authors_dropdown;
}
?>
<?php if ( post_type_supports( $screen->post_type, 'page-attributes' ) ) : ?>
<?php if ( $post_type_object->hierarchical ) : ?>
<label>
<span class="title"><?php _e( 'Parent' ); ?></span>
<?php
$dropdown_args = array(
'post_type' => $post_type_object->name,
'selected' => $post->post_parent,
'name' => 'post_parent',
'show_option_none' => __( 'Main Page (no parent)' ),
'option_none_value' => 0,
'sort_column' => 'menu_order, post_title',
);
if ( $bulk ) {
$dropdown_args['show_option_no_change'] = __( '&mdash; No Change &mdash;' );
$dropdown_args['id'] = 'bulk_edit_post_parent';
}
/**
* Filters the arguments used to generate the Quick Edit page-parent drop-down.
*
* @since 2.7.0
* @since 5.6.0 The `$bulk` parameter was added.
*
* @see wp_dropdown_pages()
*
* @param array $dropdown_args An array of arguments passed to wp_dropdown_pages().
* @param bool $bulk A flag to denote if it's a bulk action.
*/
$dropdown_args = apply_filters( 'quick_edit_dropdown_pages_args', $dropdown_args, $bulk );
wp_dropdown_pages( $dropdown_args );
?>
</label>
<?php endif; // hierarchical ?>
<?php if ( ! $bulk ) : ?>
<label>
<span class="title"><?php _e( 'Order' ); ?></span>
<span class="input-text-wrap"><input type="text" name="menu_order" class="inline-edit-menu-order-input" value="<?php echo $post->menu_order; ?>" /></span>
</label>
<?php endif; // ! $bulk ?>
<?php endif; // post_type_supports( ... 'page-attributes' ) ?>
<?php if ( 0 < count( get_page_templates( null, $screen->post_type ) ) ) : ?>
<label>
<span class="title"><?php _e( 'Template' ); ?></span>
<select name="page_template">
<?php if ( $bulk ) : ?>
<option value="-1"><?php _e( '&mdash; No Change &mdash;' ); ?></option>
<?php endif; // $bulk ?>
<?php
/** This filter is documented in wp-admin/includes/meta-boxes.php */
$default_title = apply_filters( 'default_page_template_title', __( 'Default template' ), 'quick-edit' );
?>
<option value="default"><?php echo esc_html( $default_title ); ?></option>
<?php page_template_dropdown( '', $screen->post_type ); ?>
</select>
</label>
<?php endif; ?>
<?php if ( count( $flat_taxonomies ) && ! $bulk ) : ?>
<?php foreach ( $flat_taxonomies as $taxonomy ) : ?>
<?php if ( current_user_can( $taxonomy->cap->assign_terms ) ) : ?>
<?php $taxonomy_name = esc_attr( $taxonomy->name ); ?>
<div class="inline-edit-tags-wrap">
<label class="inline-edit-tags">
<span class="title"><?php echo esc_html( $taxonomy->labels->name ); ?></span>
<textarea data-wp-taxonomy="<?php echo $taxonomy_name; ?>" cols="22" rows="1" name="tax_input[<?php echo esc_attr( $taxonomy->name ); ?>]" class="tax_input_<?php echo esc_attr( $taxonomy->name ); ?>" aria-describedby="inline-edit-<?php echo esc_attr( $taxonomy->name ); ?>-desc"></textarea>
</label>
<p class="howto" id="inline-edit-<?php echo esc_attr( $taxonomy->name ); ?>-desc"><?php echo esc_html( $taxonomy->labels->separate_items_with_commas ); ?></p>
</div>
<?php endif; // current_user_can( 'assign_terms' ) ?>
<?php endforeach; // $flat_taxonomies as $taxonomy ?>
<?php endif; // count( $flat_taxonomies ) && ! $bulk ?>
<?php if ( post_type_supports( $screen->post_type, 'comments' ) || post_type_supports( $screen->post_type, 'trackbacks' ) ) : ?>
<?php if ( $bulk ) : ?>
<div class="inline-edit-group wp-clearfix">
<?php if ( post_type_supports( $screen->post_type, 'comments' ) ) : ?>
<label class="alignleft">
<span class="title"><?php _e( 'Comments' ); ?></span>
<select name="comment_status">
<option value=""><?php _e( '&mdash; No Change &mdash;' ); ?></option>
<option value="open"><?php _e( 'Allow' ); ?></option>
<option value="closed"><?php _e( 'Do not allow' ); ?></option>
</select>
</label>
<?php endif; ?>
<?php if ( post_type_supports( $screen->post_type, 'trackbacks' ) ) : ?>
<label class="alignright">
<span class="title"><?php _e( 'Pings' ); ?></span>
<select name="ping_status">
<option value=""><?php _e( '&mdash; No Change &mdash;' ); ?></option>
<option value="open"><?php _e( 'Allow' ); ?></option>
<option value="closed"><?php _e( 'Do not allow' ); ?></option>
</select>
</label>
<?php endif; ?>
</div>
<?php else : // $bulk ?>
<div class="inline-edit-group wp-clearfix">
<?php if ( post_type_supports( $screen->post_type, 'comments' ) ) : ?>
<label class="alignleft">
<input type="checkbox" name="comment_status" value="open" />
<span class="checkbox-title"><?php _e( 'Allow Comments' ); ?></span>
</label>
<?php endif; ?>
<?php if ( post_type_supports( $screen->post_type, 'trackbacks' ) ) : ?>
<label class="alignleft">
<input type="checkbox" name="ping_status" value="open" />
<span class="checkbox-title"><?php _e( 'Allow Pings' ); ?></span>
</label>
<?php endif; ?>
</div>
<?php endif; // $bulk ?>
<?php endif; // post_type_supports( ... comments or pings ) ?>
<div class="inline-edit-group wp-clearfix">
<label class="inline-edit-status alignleft">
<span class="title"><?php _e( 'Status' ); ?></span>
<select name="_status">
<?php if ( $bulk ) : ?>
<option value="-1"><?php _e( '&mdash; No Change &mdash;' ); ?></option>
<?php endif; // $bulk ?>
<?php if ( $can_publish ) : // Contributors only get "Unpublished" and "Pending Review". ?>
<option value="publish"><?php _e( 'Published' ); ?></option>
<option value="future"><?php _e( 'Scheduled' ); ?></option>
<?php if ( $bulk ) : ?>
<option value="private"><?php _e( 'Private' ); ?></option>
<?php endif; // $bulk ?>
<?php endif; ?>
<option value="pending"><?php _e( 'Pending Review' ); ?></option>
<option value="draft"><?php _e( 'Draft' ); ?></option>
</select>
</label>
<?php if ( 'post' === $screen->post_type && $can_publish && current_user_can( $post_type_object->cap->edit_others_posts ) ) : ?>
<?php if ( $bulk ) : ?>
<label class="alignright">
<span class="title"><?php _e( 'Sticky' ); ?></span>
<select name="sticky">
<option value="-1"><?php _e( '&mdash; No Change &mdash;' ); ?></option>
<option value="sticky"><?php _e( 'Sticky' ); ?></option>
<option value="unsticky"><?php _e( 'Not Sticky' ); ?></option>
</select>
</label>
<?php else : // $bulk ?>
<label class="alignleft">
<input type="checkbox" name="sticky" value="sticky" />
<span class="checkbox-title"><?php _e( 'Make this post sticky' ); ?></span>
</label>
<?php endif; // $bulk ?>
<?php endif; // 'post' && $can_publish && current_user_can( 'edit_others_posts' ) ?>
</div>
<?php if ( $bulk && current_theme_supports( 'post-formats' ) && post_type_supports( $screen->post_type, 'post-formats' ) ) : ?>
<?php $post_formats = get_theme_support( 'post-formats' ); ?>
<label class="alignleft">
<span class="title"><?php _ex( 'Format', 'post format' ); ?></span>
<select name="post_format">
<option value="-1"><?php _e( '&mdash; No Change &mdash;' ); ?></option>
<option value="0"><?php echo get_post_format_string( 'standard' ); ?></option>
<?php if ( is_array( $post_formats[0] ) ) : ?>
<?php foreach ( $post_formats[0] as $format ) : ?>
<option value="<?php echo esc_attr( $format ); ?>"><?php echo esc_html( get_post_format_string( $format ) ); ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</label>
<?php endif; ?>
</div>
</fieldset>
<?php
list( $columns ) = $this->get_column_info();
foreach ( $columns as $column_name => $column_display_name ) {
if ( isset( $core_columns[ $column_name ] ) ) {
continue;
}
if ( $bulk ) {
/**
* Fires once for each column in Bulk Edit mode.
*
* @since 2.7.0
*
* @param string $column_name Name of the column to edit.
* @param string $post_type The post type slug.
*/
do_action( 'bulk_edit_custom_box', $column_name, $screen->post_type );
} else {
/**
* Fires once for each column in Quick Edit mode.
*
* @since 2.7.0
*
* @param string $column_name Name of the column to edit.
* @param string $post_type The post type slug, or current screen name if this is a taxonomy list table.
* @param string $taxonomy The taxonomy name, if any.
*/
do_action( 'quick_edit_custom_box', $column_name, $screen->post_type, '' );
}
}
?>
<div class="submit inline-edit-save">
<?php if ( ! $bulk ) : ?>
<?php wp_nonce_field( 'inlineeditnonce', '_inline_edit', false ); ?>
<button type="button" class="button button-primary save"><?php _e( 'Update' ); ?></button>
<?php else : ?>
<?php submit_button( __( 'Update' ), 'primary', 'bulk_edit', false ); ?>
<?php endif; ?>
<button type="button" class="button cancel"><?php _e( 'Cancel' ); ?></button>
<?php if ( ! $bulk ) : ?>
<span class="spinner"></span>
<?php endif; ?>
<input type="hidden" name="post_view" value="<?php echo esc_attr( $m ); ?>" />
<input type="hidden" name="screen" value="<?php echo esc_attr( $screen->id ); ?>" />
<?php if ( ! $bulk && ! post_type_supports( $screen->post_type, 'author' ) ) : ?>
<input type="hidden" name="post_author" value="<?php echo esc_attr( $post->post_author ); ?>" />
<?php endif; ?>
<?php
wp_admin_notice(
'<p class="error"></p>',
array(
'type' => 'error',
'additional_classes' => array( 'notice-alt', 'inline', 'hidden' ),
'paragraph_wrap' => false,
)
);
?>
</div>
</div> <!-- end of .inline-edit-wrapper -->
</td></tr>
<?php
++$bulk;
endwhile;
?>
</tbody></table>
</form>
<?php
}
}
<?php
/**
* WordPress Dashboard Widget Administration Screen API
*
* @package WordPress
* @subpackage Administration
*/
/**
* Registers dashboard widgets.
*
* Handles POST data, sets up filters.
*
* @since 2.5.0
*
* @global array $wp_registered_widgets
* @global array $wp_registered_widget_controls
* @global callable[] $wp_dashboard_control_callbacks
*/
function wp_dashboard_setup() {
global $wp_registered_widgets, $wp_registered_widget_controls, $wp_dashboard_control_callbacks;
$screen = get_current_screen();
/* Register Widgets and Controls */
$wp_dashboard_control_callbacks = array();
// Browser version
$check_browser = wp_check_browser_version();
if ( $check_browser && $check_browser['upgrade'] ) {
add_filter( 'postbox_classes_dashboard_dashboard_browser_nag', 'dashboard_browser_nag_class' );
if ( $check_browser['insecure'] ) {
wp_add_dashboard_widget( 'dashboard_browser_nag', __( 'You are using an insecure browser!' ), 'wp_dashboard_browser_nag' );
} else {
wp_add_dashboard_widget( 'dashboard_browser_nag', __( 'Your browser is out of date!' ), 'wp_dashboard_browser_nag' );
}
}
// PHP Version.
$check_php = wp_check_php_version();
if ( $check_php && current_user_can( 'update_php' ) ) {
// If "not acceptable" the widget will be shown.
if ( isset( $check_php['is_acceptable'] ) && ! $check_php['is_acceptable'] ) {
add_filter( 'postbox_classes_dashboard_dashboard_php_nag', 'dashboard_php_nag_class' );
if ( $check_php['is_lower_than_future_minimum'] ) {
wp_add_dashboard_widget( 'dashboard_php_nag', __( 'PHP Update Required' ), 'wp_dashboard_php_nag' );
} else {
wp_add_dashboard_widget( 'dashboard_php_nag', __( 'PHP Update Recommended' ), 'wp_dashboard_php_nag' );
}
}
}
// Site Health.
if ( current_user_can( 'view_site_health_checks' ) && ! is_network_admin() ) {
if ( ! class_exists( 'WP_Site_Health' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-site-health.php';
}
WP_Site_Health::get_instance();
wp_enqueue_style( 'site-health' );
wp_enqueue_script( 'site-health' );
wp_add_dashboard_widget( 'dashboard_site_health', __( 'Site Health Status' ), 'wp_dashboard_site_health' );
}
// Right Now.
if ( is_blog_admin() && current_user_can( 'edit_posts' ) ) {
wp_add_dashboard_widget( 'dashboard_right_now', __( 'At a Glance' ), 'wp_dashboard_right_now' );
}
if ( is_network_admin() ) {
wp_add_dashboard_widget( 'network_dashboard_right_now', __( 'Right Now' ), 'wp_network_dashboard_right_now' );
}
// Activity Widget.
if ( is_blog_admin() ) {
wp_add_dashboard_widget( 'dashboard_activity', __( 'Activity' ), 'wp_dashboard_site_activity' );
}
// QuickPress Widget.
if ( is_blog_admin() && current_user_can( get_post_type_object( 'post' )->cap->create_posts ) ) {
$quick_draft_title = sprintf( '<span class="hide-if-no-js">%1$s</span> <span class="hide-if-js">%2$s</span>', __( 'Quick Draft' ), __( 'Your Recent Drafts' ) );
wp_add_dashboard_widget( 'dashboard_quick_press', $quick_draft_title, 'wp_dashboard_quick_press' );
}
// WordPress Events and News.
wp_add_dashboard_widget( 'dashboard_primary', __( 'WordPress Events and News' ), 'wp_dashboard_events_news' );
if ( is_network_admin() ) {
/**
* Fires after core widgets for the Network Admin dashboard have been registered.
*
* @since 3.1.0
*/
do_action( 'wp_network_dashboard_setup' );
/**
* Filters the list of widgets to load for the Network Admin dashboard.
*
* @since 3.1.0
*
* @param string[] $dashboard_widgets An array of dashboard widget IDs.
*/
$dashboard_widgets = apply_filters( 'wp_network_dashboard_widgets', array() );
} elseif ( is_user_admin() ) {
/**
* Fires after core widgets for the User Admin dashboard have been registered.
*
* @since 3.1.0
*/
do_action( 'wp_user_dashboard_setup' );
/**
* Filters the list of widgets to load for the User Admin dashboard.
*
* @since 3.1.0
*
* @param string[] $dashboard_widgets An array of dashboard widget IDs.
*/
$dashboard_widgets = apply_filters( 'wp_user_dashboard_widgets', array() );
} else {
/**
* Fires after core widgets for the admin dashboard have been registered.
*
* @since 2.5.0
*/
do_action( 'wp_dashboard_setup' );
/**
* Filters the list of widgets to load for the admin dashboard.
*
* @since 2.5.0
*
* @param string[] $dashboard_widgets An array of dashboard widget IDs.
*/
$dashboard_widgets = apply_filters( 'wp_dashboard_widgets', array() );
}
foreach ( $dashboard_widgets as $widget_id ) {
$name = empty( $wp_registered_widgets[ $widget_id ]['all_link'] ) ? $wp_registered_widgets[ $widget_id ]['name'] : $wp_registered_widgets[ $widget_id ]['name'] . " <a href='{$wp_registered_widgets[$widget_id]['all_link']}' class='edit-box open-box'>" . __( 'View all' ) . '</a>';
wp_add_dashboard_widget( $widget_id, $name, $wp_registered_widgets[ $widget_id ]['callback'], $wp_registered_widget_controls[ $widget_id ]['callback'] );
}
if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['widget_id'] ) ) {
check_admin_referer( 'edit-dashboard-widget_' . $_POST['widget_id'], 'dashboard-widget-nonce' );
ob_start(); // Hack - but the same hack wp-admin/widgets.php uses.
wp_dashboard_trigger_widget_control( $_POST['widget_id'] );
ob_end_clean();
wp_redirect( remove_query_arg( 'edit' ) );
exit;
}
/** This action is documented in wp-admin/includes/meta-boxes.php */
do_action( 'do_meta_boxes', $screen->id, 'normal', '' );
/** This action is documented in wp-admin/includes/meta-boxes.php */
do_action( 'do_meta_boxes', $screen->id, 'side', '' );
}
/**
* Adds a new dashboard widget.
*
* @since 2.7.0
* @since 5.6.0 The `$context` and `$priority` parameters were added.
*
* @global callable[] $wp_dashboard_control_callbacks
*
* @param string $widget_id Widget ID (used in the 'id' attribute for the widget).
* @param string $widget_name Title of the widget.
* @param callable $callback Function that fills the widget with the desired content.
* The function should echo its output.
* @param callable $control_callback Optional. Function that outputs controls for the widget. Default null.
* @param array $callback_args Optional. Data that should be set as the $args property of the widget array
* (which is the second parameter passed to your callback). Default null.
* @param string $context Optional. The context within the screen where the box should display.
* Accepts 'normal', 'side', 'column3', or 'column4'. Default 'normal'.
* @param string $priority Optional. The priority within the context where the box should show.
* Accepts 'high', 'core', 'default', or 'low'. Default 'core'.
*/
function wp_add_dashboard_widget( $widget_id, $widget_name, $callback, $control_callback = null, $callback_args = null, $context = 'normal', $priority = 'core' ) {
global $wp_dashboard_control_callbacks;
$screen = get_current_screen();
$private_callback_args = array( '__widget_basename' => $widget_name );
if ( is_null( $callback_args ) ) {
$callback_args = $private_callback_args;
} elseif ( is_array( $callback_args ) ) {
$callback_args = array_merge( $callback_args, $private_callback_args );
}
if ( $control_callback && is_callable( $control_callback ) && current_user_can( 'edit_dashboard' ) ) {
$wp_dashboard_control_callbacks[ $widget_id ] = $control_callback;
if ( isset( $_GET['edit'] ) && $widget_id === $_GET['edit'] ) {
list($url) = explode( '#', add_query_arg( 'edit', false ), 2 );
$widget_name .= ' <span class="postbox-title-action"><a href="' . esc_url( $url ) . '">' . __( 'Cancel' ) . '</a></span>';
$callback = '_wp_dashboard_control_callback';
} else {
list($url) = explode( '#', add_query_arg( 'edit', $widget_id ), 2 );
$widget_name .= ' <span class="postbox-title-action"><a href="' . esc_url( "$url#$widget_id" ) . '" class="edit-box open-box">' . __( 'Configure' ) . '</a></span>';
}
}
$side_widgets = array( 'dashboard_quick_press', 'dashboard_primary' );
if ( in_array( $widget_id, $side_widgets, true ) ) {
$context = 'side';
}
$high_priority_widgets = array( 'dashboard_browser_nag', 'dashboard_php_nag' );
if ( in_array( $widget_id, $high_priority_widgets, true ) ) {
$priority = 'high';
}
if ( empty( $context ) ) {
$context = 'normal';
}
if ( empty( $priority ) ) {
$priority = 'core';
}
add_meta_box( $widget_id, $widget_name, $callback, $screen, $context, $priority, $callback_args );
}
/**
* Outputs controls for the current dashboard widget.
*
* @access private
* @since 2.7.0
*
* @param mixed $dashboard
* @param array $meta_box
*/
function _wp_dashboard_control_callback( $dashboard, $meta_box ) {
echo '<form method="post" class="dashboard-widget-control-form wp-clearfix">';
wp_dashboard_trigger_widget_control( $meta_box['id'] );
wp_nonce_field( 'edit-dashboard-widget_' . $meta_box['id'], 'dashboard-widget-nonce' );
echo '<input type="hidden" name="widget_id" value="' . esc_attr( $meta_box['id'] ) . '" />';
submit_button( __( 'Save Changes' ) );
echo '</form>';
}
/**
* Displays the dashboard.
*
* @since 2.5.0
*/
function wp_dashboard() {
$screen = get_current_screen();
$columns = absint( $screen->get_columns() );
$columns_css = '';
if ( $columns ) {
$columns_css = " columns-$columns";
}
?>
<div id="dashboard-widgets" class="metabox-holder<?php echo $columns_css; ?>">
<div id="postbox-container-1" class="postbox-container">
<?php do_meta_boxes( $screen->id, 'normal', '' ); ?>
</div>
<div id="postbox-container-2" class="postbox-container">
<?php do_meta_boxes( $screen->id, 'side', '' ); ?>
</div>
<div id="postbox-container-3" class="postbox-container">
<?php do_meta_boxes( $screen->id, 'column3', '' ); ?>
</div>
<div id="postbox-container-4" class="postbox-container">
<?php do_meta_boxes( $screen->id, 'column4', '' ); ?>
</div>
</div>
<?php
wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false );
wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false );
}
//
// Dashboard Widgets.
//
/**
* Dashboard widget that displays some basic stats about the site.
*
* Formerly 'Right Now'. A streamlined 'At a Glance' as of 3.8.
*
* @since 2.7.0
*/
function wp_dashboard_right_now() {
?>
<div class="main">
<ul>
<?php
// Posts and Pages.
foreach ( array( 'post', 'page' ) as $post_type ) {
$num_posts = wp_count_posts( $post_type );
if ( $num_posts && $num_posts->publish ) {
if ( 'post' === $post_type ) {
/* translators: %s: Number of posts. */
$text = _n( '%s Post', '%s Posts', $num_posts->publish );
} else {
/* translators: %s: Number of pages. */
$text = _n( '%s Page', '%s Pages', $num_posts->publish );
}
$text = sprintf( $text, number_format_i18n( $num_posts->publish ) );
$post_type_object = get_post_type_object( $post_type );
if ( $post_type_object && current_user_can( $post_type_object->cap->edit_posts ) ) {
printf( '<li class="%1$s-count"><a href="edit.php?post_type=%1$s">%2$s</a></li>', $post_type, $text );
} else {
printf( '<li class="%1$s-count"><span>%2$s</span></li>', $post_type, $text );
}
}
}
// Comments.
$num_comm = wp_count_comments();
if ( $num_comm && ( $num_comm->approved || $num_comm->moderated ) ) {
/* translators: %s: Number of comments. */
$text = sprintf( _n( '%s Comment', '%s Comments', $num_comm->approved ), number_format_i18n( $num_comm->approved ) );
?>
<li class="comment-count">
<a href="edit-comments.php"><?php echo $text; ?></a>
</li>
<?php
$moderated_comments_count_i18n = number_format_i18n( $num_comm->moderated );
/* translators: %s: Number of comments. */
$text = sprintf( _n( '%s Comment in moderation', '%s Comments in moderation', $num_comm->moderated ), $moderated_comments_count_i18n );
?>
<li class="comment-mod-count<?php echo ! $num_comm->moderated ? ' hidden' : ''; ?>">
<a href="edit-comments.php?comment_status=moderated" class="comments-in-moderation-text"><?php echo $text; ?></a>
</li>
<?php
}
/**
* Filters the array of extra elements to list in the 'At a Glance'
* dashboard widget.
*
* Prior to 3.8.0, the widget was named 'Right Now'. Each element
* is wrapped in list-item tags on output.
*
* @since 3.8.0
*
* @param string[] $items Array of extra 'At a Glance' widget items.
*/
$elements = apply_filters( 'dashboard_glance_items', array() );
if ( $elements ) {
echo '<li>' . implode( "</li>\n<li>", $elements ) . "</li>\n";
}
?>
</ul>
<?php
update_right_now_message();
// Check if search engines are asked not to index this site.
if ( ! is_network_admin() && ! is_user_admin()
&& current_user_can( 'manage_options' ) && ! get_option( 'blog_public' )
) {
/**
* Filters the link title attribute for the 'Search engines discouraged'
* message displayed in the 'At a Glance' dashboard widget.
*
* Prior to 3.8.0, the widget was named 'Right Now'.
*
* @since 3.0.0
* @since 4.5.0 The default for `$title` was updated to an empty string.
*
* @param string $title Default attribute text.
*/
$title = apply_filters( 'privacy_on_link_title', '' );
/**
* Filters the link label for the 'Search engines discouraged' message
* displayed in the 'At a Glance' dashboard widget.
*
* Prior to 3.8.0, the widget was named 'Right Now'.
*
* @since 3.0.0
*
* @param string $content Default text.
*/
$content = apply_filters( 'privacy_on_link_text', __( 'Search engines discouraged' ) );
$title_attr = '' === $title ? '' : " title='$title'";
echo "<p class='search-engines-info'><a href='options-reading.php'$title_attr>$content</a></p>";
}
?>
</div>
<?php
/*
* activity_box_end has a core action, but only prints content when multisite.
* Using an output buffer is the only way to really check if anything's displayed here.
*/
ob_start();
/**
* Fires at the end of the 'At a Glance' dashboard widget.
*
* Prior to 3.8.0, the widget was named 'Right Now'.
*
* @since 2.5.0
*/
do_action( 'rightnow_end' );
/**
* Fires at the end of the 'At a Glance' dashboard widget.
*
* Prior to 3.8.0, the widget was named 'Right Now'.
*
* @since 2.0.0
*/
do_action( 'activity_box_end' );
$actions = ob_get_clean();
if ( ! empty( $actions ) ) :
?>
<div class="sub">
<?php echo $actions; ?>
</div>
<?php
endif;
}
/**
* @since 3.1.0
*/
function wp_network_dashboard_right_now() {
$actions = array();
if ( current_user_can( 'create_sites' ) ) {
$actions['create-site'] = '<a href="' . network_admin_url( 'site-new.php' ) . '">' . __( 'Create a New Site' ) . '</a>';
}
if ( current_user_can( 'create_users' ) ) {
$actions['create-user'] = '<a href="' . network_admin_url( 'user-new.php' ) . '">' . __( 'Create a New User' ) . '</a>';
}
$c_users = get_user_count();
$c_blogs = get_blog_count();
/* translators: %s: Number of users on the network. */
$user_text = sprintf( _n( '%s user', '%s users', $c_users ), number_format_i18n( $c_users ) );
/* translators: %s: Number of sites on the network. */
$blog_text = sprintf( _n( '%s site', '%s sites', $c_blogs ), number_format_i18n( $c_blogs ) );
/* translators: 1: Text indicating the number of sites on the network, 2: Text indicating the number of users on the network. */
$sentence = sprintf( __( 'You have %1$s and %2$s.' ), $blog_text, $user_text );
if ( $actions ) {
echo '<ul class="subsubsub">';
foreach ( $actions as $class => $action ) {
$actions[ $class ] = "\t<li class='$class'>$action";
}
echo implode( " |</li>\n", $actions ) . "</li>\n";
echo '</ul>';
}
?>
<br class="clear" />
<p class="youhave"><?php echo $sentence; ?></p>
<?php
/**
* Fires in the Network Admin 'Right Now' dashboard widget
* just before the user and site search form fields.
*
* @since MU (3.0.0)
*/
do_action( 'wpmuadminresult' );
?>
<form action="<?php echo esc_url( network_admin_url( 'users.php' ) ); ?>" method="get">
<p>
<label class="screen-reader-text" for="search-users">
<?php
/* translators: Hidden accessibility text. */
_e( 'Search Users' );
?>
</label>
<input type="search" name="s" value="" size="30" autocomplete="off" id="search-users" />
<?php submit_button( __( 'Search Users' ), '', false, false, array( 'id' => 'submit_users' ) ); ?>
</p>
</form>
<form action="<?php echo esc_url( network_admin_url( 'sites.php' ) ); ?>" method="get">
<p>
<label class="screen-reader-text" for="search-sites">
<?php
/* translators: Hidden accessibility text. */
_e( 'Search Sites' );
?>
</label>
<input type="search" name="s" value="" size="30" autocomplete="off" id="search-sites" />
<?php submit_button( __( 'Search Sites' ), '', false, false, array( 'id' => 'submit_sites' ) ); ?>
</p>
</form>
<?php
/**
* Fires at the end of the 'Right Now' widget in the Network Admin dashboard.
*
* @since MU (3.0.0)
*/
do_action( 'mu_rightnow_end' );
/**
* Fires at the end of the 'Right Now' widget in the Network Admin dashboard.
*
* @since MU (3.0.0)
*/
do_action( 'mu_activity_box_end' );
}
/**
* Displays the Quick Draft widget.
*
* @since 3.8.0
*
* @global int $post_ID
*
* @param string|false $error_msg Optional. Error message. Default false.
*/
function wp_dashboard_quick_press( $error_msg = false ) {
global $post_ID;
if ( ! current_user_can( 'edit_posts' ) ) {
return;
}
// Check if a new auto-draft (= no new post_ID) is needed or if the old can be used.
$last_post_id = (int) get_user_option( 'dashboard_quick_press_last_post_id' ); // Get the last post_ID.
if ( $last_post_id ) {
$post = get_post( $last_post_id );
if ( empty( $post ) || 'auto-draft' !== $post->post_status ) { // auto-draft doesn't exist anymore.
$post = get_default_post_to_edit( 'post', true );
update_user_option( get_current_user_id(), 'dashboard_quick_press_last_post_id', (int) $post->ID ); // Save post_ID.
} else {
$post->post_title = ''; // Remove the auto draft title.
}
} else {
$post = get_default_post_to_edit( 'post', true );
$user_id = get_current_user_id();
// Don't create an option if this is a super admin who does not belong to this site.
if ( in_array( get_current_blog_id(), array_keys( get_blogs_of_user( $user_id ) ), true ) ) {
update_user_option( $user_id, 'dashboard_quick_press_last_post_id', (int) $post->ID ); // Save post_ID.
}
}
$post_ID = (int) $post->ID;
?>
<form name="post" action="<?php echo esc_url( admin_url( 'post.php' ) ); ?>" method="post" id="quick-press" class="initial-form hide-if-no-js">
<?php
if ( $error_msg ) {
wp_admin_notice(
$error_msg,
array(
'additional_classes' => array( 'error' ),
)
);
}
?>
<div class="input-text-wrap" id="title-wrap">
<label for="title">
<?php
/** This filter is documented in wp-admin/edit-form-advanced.php */
echo apply_filters( 'enter_title_here', __( 'Title' ), $post );
?>
</label>
<input type="text" name="post_title" id="title" autocomplete="off" />
</div>
<div class="textarea-wrap" id="description-wrap">
<label for="content"><?php _e( 'Content' ); ?></label>
<textarea name="content" id="content" placeholder="<?php esc_attr_e( 'What&#8217;s on your mind?' ); ?>" class="mceEditor" rows="3" cols="15" autocomplete="off"></textarea>
</div>
<p class="submit">
<input type="hidden" name="action" id="quickpost-action" value="post-quickdraft-save" />
<input type="hidden" name="post_ID" value="<?php echo $post_ID; ?>" />
<input type="hidden" name="post_type" value="post" />
<?php wp_nonce_field( 'add-post' ); ?>
<?php submit_button( __( 'Save Draft' ), 'primary', 'save', false, array( 'id' => 'save-post' ) ); ?>
<br class="clear" />
</p>
</form>
<?php
wp_dashboard_recent_drafts();
}
/**
* Show recent drafts of the user on the dashboard.
*
* @since 2.7.0
*
* @param WP_Post[]|false $drafts Optional. Array of posts to display. Default false.
*/
function wp_dashboard_recent_drafts( $drafts = false ) {
if ( ! $drafts ) {
$query_args = array(
'post_type' => 'post',
'post_status' => 'draft',
'author' => get_current_user_id(),
'posts_per_page' => 4,
'orderby' => 'modified',
'order' => 'DESC',
);
/**
* Filters the post query arguments for the 'Recent Drafts' dashboard widget.
*
* @since 4.4.0
*
* @param array $query_args The query arguments for the 'Recent Drafts' dashboard widget.
*/
$query_args = apply_filters( 'dashboard_recent_drafts_query_args', $query_args );
$drafts = get_posts( $query_args );
if ( ! $drafts ) {
return;
}
}
echo '<div class="drafts">';
if ( count( $drafts ) > 3 ) {
printf(
'<p class="view-all"><a href="%s">%s</a></p>' . "\n",
esc_url( admin_url( 'edit.php?post_status=draft' ) ),
__( 'View all drafts' )
);
}
echo '<h2 class="hide-if-no-js">' . __( 'Your Recent Drafts' ) . "</h2>\n";
echo '<ul>';
/* translators: Maximum number of words used in a preview of a draft on the dashboard. */
$draft_length = (int) _x( '10', 'draft_length' );
$drafts = array_slice( $drafts, 0, 3 );
foreach ( $drafts as $draft ) {
$url = get_edit_post_link( $draft->ID );
$title = _draft_or_post_title( $draft->ID );
echo "<li>\n";
printf(
'<div class="draft-title"><a href="%s" aria-label="%s">%s</a><time datetime="%s">%s</time></div>',
esc_url( $url ),
/* translators: %s: Post title. */
esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;' ), $title ) ),
esc_html( $title ),
get_the_time( 'c', $draft ),
get_the_time( __( 'F j, Y' ), $draft )
);
$the_content = wp_trim_words( $draft->post_content, $draft_length );
if ( $the_content ) {
echo '<p>' . $the_content . '</p>';
}
echo "</li>\n";
}
echo "</ul>\n";
echo '</div>';
}
/**
* Outputs a row for the Recent Comments widget.
*
* @access private
* @since 2.7.0
*
* @global WP_Comment $comment Global comment object.
*
* @param WP_Comment $comment The current comment.
* @param bool $show_date Optional. Whether to display the date.
*/
function _wp_dashboard_recent_comments_row( &$comment, $show_date = true ) {
$GLOBALS['comment'] = clone $comment;
if ( $comment->comment_post_ID > 0 ) {
$comment_post_title = _draft_or_post_title( $comment->comment_post_ID );
$comment_post_url = get_the_permalink( $comment->comment_post_ID );
$comment_post_link = '<a href="' . esc_url( $comment_post_url ) . '">' . $comment_post_title . '</a>';
} else {
$comment_post_link = '';
}
$actions_string = '';
if ( current_user_can( 'edit_comment', $comment->comment_ID ) ) {
// Pre-order it: Approve | Reply | Edit | Spam | Trash.
$actions = array(
'approve' => '',
'unapprove' => '',
'reply' => '',
'edit' => '',
'spam' => '',
'trash' => '',
'delete' => '',
'view' => '',
);
$approve_nonce = esc_html( '_wpnonce=' . wp_create_nonce( 'approve-comment_' . $comment->comment_ID ) );
$del_nonce = esc_html( '_wpnonce=' . wp_create_nonce( 'delete-comment_' . $comment->comment_ID ) );
$action_string = 'comment.php?action=%s&p=' . $comment->comment_post_ID . '&c=' . $comment->comment_ID . '&%s';
$approve_url = sprintf( $action_string, 'approvecomment', $approve_nonce );
$unapprove_url = sprintf( $action_string, 'unapprovecomment', $approve_nonce );
$spam_url = sprintf( $action_string, 'spamcomment', $del_nonce );
$trash_url = sprintf( $action_string, 'trashcomment', $del_nonce );
$delete_url = sprintf( $action_string, 'deletecomment', $del_nonce );
$actions['approve'] = sprintf(
'<a href="%s" data-wp-lists="%s" class="vim-a aria-button-if-js" aria-label="%s">%s</a>',
esc_url( $approve_url ),
"dim:the-comment-list:comment-{$comment->comment_ID}:unapproved:e7e7d3:e7e7d3:new=approved",
esc_attr__( 'Approve this comment' ),
__( 'Approve' )
);
$actions['unapprove'] = sprintf(
'<a href="%s" data-wp-lists="%s" class="vim-u aria-button-if-js" aria-label="%s">%s</a>',
esc_url( $unapprove_url ),
"dim:the-comment-list:comment-{$comment->comment_ID}:unapproved:e7e7d3:e7e7d3:new=unapproved",
esc_attr__( 'Unapprove this comment' ),
__( 'Unapprove' )
);
$actions['edit'] = sprintf(
'<a href="%s" aria-label="%s">%s</a>',
"comment.php?action=editcomment&amp;c={$comment->comment_ID}",
esc_attr__( 'Edit this comment' ),
__( 'Edit' )
);
$actions['reply'] = sprintf(
'<button type="button" onclick="window.commentReply && commentReply.open(\'%s\',\'%s\');" class="vim-r button-link hide-if-no-js" aria-label="%s">%s</button>',
$comment->comment_ID,
$comment->comment_post_ID,
esc_attr__( 'Reply to this comment' ),
__( 'Reply' )
);
$actions['spam'] = sprintf(
'<a href="%s" data-wp-lists="%s" class="vim-s vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
esc_url( $spam_url ),
"delete:the-comment-list:comment-{$comment->comment_ID}::spam=1",
esc_attr__( 'Mark this comment as spam' ),
/* translators: "Mark as spam" link. */
_x( 'Spam', 'verb' )
);
if ( ! EMPTY_TRASH_DAYS ) {
$actions['delete'] = sprintf(
'<a href="%s" data-wp-lists="%s" class="delete vim-d vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
esc_url( $delete_url ),
"delete:the-comment-list:comment-{$comment->comment_ID}::trash=1",
esc_attr__( 'Delete this comment permanently' ),
__( 'Delete Permanently' )
);
} else {
$actions['trash'] = sprintf(
'<a href="%s" data-wp-lists="%s" class="delete vim-d vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
esc_url( $trash_url ),
"delete:the-comment-list:comment-{$comment->comment_ID}::trash=1",
esc_attr__( 'Move this comment to the Trash' ),
_x( 'Trash', 'verb' )
);
}
$actions['view'] = sprintf(
'<a class="comment-link" href="%s" aria-label="%s">%s</a>',
esc_url( get_comment_link( $comment ) ),
esc_attr__( 'View this comment' ),
__( 'View' )
);
/** This filter is documented in wp-admin/includes/class-wp-comments-list-table.php */
$actions = apply_filters( 'comment_row_actions', array_filter( $actions ), $comment );
$i = 0;
foreach ( $actions as $action => $link ) {
++$i;
if ( ( ( 'approve' === $action || 'unapprove' === $action ) && 2 === $i )
|| 1 === $i
) {
$separator = '';
} else {
$separator = ' | ';
}
// Reply and quickedit need a hide-if-no-js span.
if ( 'reply' === $action || 'quickedit' === $action ) {
$action .= ' hide-if-no-js';
}
if ( 'view' === $action && '1' !== $comment->comment_approved ) {
$action .= ' hidden';
}
$actions_string .= "<span class='$action'>{$separator}{$link}</span>";
}
}
?>
<li id="comment-<?php echo $comment->comment_ID; ?>" <?php comment_class( array( 'comment-item', wp_get_comment_status( $comment ) ), $comment ); ?>>
<?php
$comment_row_class = '';
if ( get_option( 'show_avatars' ) ) {
echo get_avatar( $comment, 50, 'mystery' );
$comment_row_class .= ' has-avatar';
}
?>
<?php if ( ! $comment->comment_type || 'comment' === $comment->comment_type ) : ?>
<div class="dashboard-comment-wrap has-row-actions <?php echo $comment_row_class; ?>">
<p class="comment-meta">
<?php
// Comments might not have a post they relate to, e.g. programmatically created ones.
if ( $comment_post_link ) {
printf(
/* translators: 1: Comment author, 2: Post link, 3: Notification if the comment is pending. */
__( 'From %1$s on %2$s %3$s' ),
'<cite class="comment-author">' . get_comment_author_link( $comment ) . '</cite>',
$comment_post_link,
'<span class="approve">' . __( '[Pending]' ) . '</span>'
);
} else {
printf(
/* translators: 1: Comment author, 2: Notification if the comment is pending. */
__( 'From %1$s %2$s' ),
'<cite class="comment-author">' . get_comment_author_link( $comment ) . '</cite>',
'<span class="approve">' . __( '[Pending]' ) . '</span>'
);
}
?>
</p>
<?php
else :
switch ( $comment->comment_type ) {
case 'pingback':
$type = __( 'Pingback' );
break;
case 'trackback':
$type = __( 'Trackback' );
break;
default:
$type = ucwords( $comment->comment_type );
}
$type = esc_html( $type );
?>
<div class="dashboard-comment-wrap has-row-actions">
<p class="comment-meta">
<?php
// Pingbacks, Trackbacks or custom comment types might not have a post they relate to, e.g. programmatically created ones.
if ( $comment_post_link ) {
printf(
/* translators: 1: Type of comment, 2: Post link, 3: Notification if the comment is pending. */
_x( '%1$s on %2$s %3$s', 'dashboard' ),
"<strong>$type</strong>",
$comment_post_link,
'<span class="approve">' . __( '[Pending]' ) . '</span>'
);
} else {
printf(
/* translators: 1: Type of comment, 2: Notification if the comment is pending. */
_x( '%1$s %2$s', 'dashboard' ),
"<strong>$type</strong>",
'<span class="approve">' . __( '[Pending]' ) . '</span>'
);
}
?>
</p>
<p class="comment-author"><?php comment_author_link( $comment ); ?></p>
<?php endif; // comment_type ?>
<blockquote><p><?php comment_excerpt( $comment ); ?></p></blockquote>
<?php if ( $actions_string ) : ?>
<p class="row-actions"><?php echo $actions_string; ?></p>
<?php endif; ?>
</div>
</li>
<?php
$GLOBALS['comment'] = null;
}
/**
* Outputs the Activity widget.
*
* Callback function for {@see 'dashboard_activity'}.
*
* @since 3.8.0
*/
function wp_dashboard_site_activity() {
echo '<div id="activity-widget">';
$future_posts = wp_dashboard_recent_posts(
array(
'max' => 5,
'status' => 'future',
'order' => 'ASC',
'title' => __( 'Publishing Soon' ),
'id' => 'future-posts',
)
);
$recent_posts = wp_dashboard_recent_posts(
array(
'max' => 5,
'status' => 'publish',
'order' => 'DESC',
'title' => __( 'Recently Published' ),
'id' => 'published-posts',
)
);
$recent_comments = wp_dashboard_recent_comments();
if ( ! $future_posts && ! $recent_posts && ! $recent_comments ) {
echo '<div class="no-activity">';
echo '<p>' . __( 'No activity yet!' ) . '</p>';
echo '</div>';
}
echo '</div>';
}
/**
* Generates Publishing Soon and Recently Published sections.
*
* @since 3.8.0
*
* @param array $args {
* An array of query and display arguments.
*
* @type int $max Number of posts to display.
* @type string $status Post status.
* @type string $order Designates ascending ('ASC') or descending ('DESC') order.
* @type string $title Section title.
* @type string $id The container id.
* }
* @return bool False if no posts were found. True otherwise.
*/
function wp_dashboard_recent_posts( $args ) {
$query_args = array(
'post_type' => 'post',
'post_status' => $args['status'],
'orderby' => 'date',
'order' => $args['order'],
'posts_per_page' => (int) $args['max'],
'no_found_rows' => true,
'cache_results' => true,
'perm' => ( 'future' === $args['status'] ) ? 'editable' : 'readable',
);
/**
* Filters the query arguments used for the Recent Posts widget.
*
* @since 4.2.0
*
* @param array $query_args The arguments passed to WP_Query to produce the list of posts.
*/
$query_args = apply_filters( 'dashboard_recent_posts_query_args', $query_args );
$posts = new WP_Query( $query_args );
if ( $posts->have_posts() ) {
echo '<div id="' . $args['id'] . '" class="activity-block">';
echo '<h3>' . $args['title'] . '</h3>';
echo '<ul>';
$today = current_time( 'Y-m-d' );
$tomorrow = current_datetime()->modify( '+1 day' )->format( 'Y-m-d' );
$year = current_time( 'Y' );
while ( $posts->have_posts() ) {
$posts->the_post();
$time = get_the_time( 'U' );
if ( gmdate( 'Y-m-d', $time ) === $today ) {
$relative = __( 'Today' );
} elseif ( gmdate( 'Y-m-d', $time ) === $tomorrow ) {
$relative = __( 'Tomorrow' );
} elseif ( gmdate( 'Y', $time ) !== $year ) {
/* translators: Date and time format for recent posts on the dashboard, from a different calendar year, see https://www.php.net/manual/datetime.format.php */
$relative = date_i18n( __( 'M jS Y' ), $time );
} else {
/* translators: Date and time format for recent posts on the dashboard, see https://www.php.net/manual/datetime.format.php */
$relative = date_i18n( __( 'M jS' ), $time );
}
// Use the post edit link for those who can edit, the permalink otherwise.
$recent_post_link = current_user_can( 'edit_post', get_the_ID() ) ? get_edit_post_link() : get_permalink();
$draft_or_post_title = _draft_or_post_title();
printf(
'<li><span>%1$s</span> <a href="%2$s" aria-label="%3$s">%4$s</a></li>',
/* translators: 1: Relative date, 2: Time. */
sprintf( _x( '%1$s, %2$s', 'dashboard' ), $relative, get_the_time() ),
$recent_post_link,
/* translators: %s: Post title. */
esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;' ), $draft_or_post_title ) ),
$draft_or_post_title
);
}
echo '</ul>';
echo '</div>';
} else {
return false;
}
wp_reset_postdata();
return true;
}
/**
* Show Comments section.
*
* @since 3.8.0
*
* @param int $total_items Optional. Number of comments to query. Default 5.
* @return bool False if no comments were found. True otherwise.
*/
function wp_dashboard_recent_comments( $total_items = 5 ) {
// Select all comment types and filter out spam later for better query performance.
$comments = array();
$comments_query = array(
'number' => $total_items * 5,
'offset' => 0,
);
if ( ! current_user_can( 'edit_posts' ) ) {
$comments_query['status'] = 'approve';
}
while ( count( $comments ) < $total_items && $possible = get_comments( $comments_query ) ) {
if ( ! is_array( $possible ) ) {
break;
}
foreach ( $possible as $comment ) {
if ( ! current_user_can( 'edit_post', $comment->comment_post_ID )
&& ( post_password_required( $comment->comment_post_ID )
|| ! current_user_can( 'read_post', $comment->comment_post_ID ) )
) {
// The user has no access to the post and thus cannot see the comments.
continue;
}
$comments[] = $comment;
if ( count( $comments ) === $total_items ) {
break 2;
}
}
$comments_query['offset'] += $comments_query['number'];
$comments_query['number'] = $total_items * 10;
}
if ( $comments ) {
echo '<div id="latest-comments" class="activity-block table-view-list">';
echo '<h3>' . __( 'Recent Comments' ) . '</h3>';
echo '<ul id="the-comment-list" data-wp-lists="list:comment">';
foreach ( $comments as $comment ) {
_wp_dashboard_recent_comments_row( $comment );
}
echo '</ul>';
if ( current_user_can( 'edit_posts' ) ) {
echo '<h3 class="screen-reader-text">' .
/* translators: Hidden accessibility text. */
__( 'View more comments' ) .
'</h3>';
_get_list_table( 'WP_Comments_List_Table' )->views();
}
wp_comment_reply( -1, false, 'dashboard', false );
wp_comment_trashnotice();
echo '</div>';
} else {
return false;
}
return true;
}
/**
* Display generic dashboard RSS widget feed.
*
* @since 2.5.0
*
* @param string $widget_id
*/
function wp_dashboard_rss_output( $widget_id ) {
$widgets = get_option( 'dashboard_widget_options' );
echo '<div class="rss-widget">';
wp_widget_rss_output( $widgets[ $widget_id ] );
echo '</div>';
}
/**
* Checks to see if all of the feed url in $check_urls are cached.
*
* If $check_urls is empty, look for the rss feed url found in the dashboard
* widget options of $widget_id. If cached, call $callback, a function that
* echoes out output for this widget. If not cache, echo a "Loading..." stub
* which is later replaced by Ajax call (see top of /wp-admin/index.php)
*
* @since 2.5.0
* @since 5.3.0 Formalized the existing and already documented `...$args` parameter
* by adding it to the function signature.
*
* @param string $widget_id The widget ID.
* @param callable $callback The callback function used to display each feed.
* @param array $check_urls RSS feeds.
* @param mixed ...$args Optional additional parameters to pass to the callback function.
* @return bool True on success, false on failure.
*/
function wp_dashboard_cached_rss_widget( $widget_id, $callback, $check_urls = array(), ...$args ) {
$doing_ajax = wp_doing_ajax();
$loading = '<p class="widget-loading hide-if-no-js">' . __( 'Loading&hellip;' ) . '</p>';
$loading .= wp_get_admin_notice(
__( 'This widget requires JavaScript.' ),
array(
'type' => 'error',
'additional_classes' => array( 'inline', 'hide-if-js' ),
)
);
if ( empty( $check_urls ) ) {
$widgets = get_option( 'dashboard_widget_options' );
if ( empty( $widgets[ $widget_id ]['url'] ) && ! $doing_ajax ) {
echo $loading;
return false;
}
$check_urls = array( $widgets[ $widget_id ]['url'] );
}
$locale = get_user_locale();
$cache_key = 'dash_v2_' . md5( $widget_id . '_' . $locale );
$output = get_transient( $cache_key );
if ( false !== $output ) {
echo $output;
return true;
}
if ( ! $doing_ajax ) {
echo $loading;
return false;
}
if ( $callback && is_callable( $callback ) ) {
array_unshift( $args, $widget_id, $check_urls );
ob_start();
call_user_func_array( $callback, $args );
// Default lifetime in cache of 12 hours (same as the feeds).
set_transient( $cache_key, ob_get_flush(), 12 * HOUR_IN_SECONDS );
}
return true;
}
//
// Dashboard Widgets Controls.
//
/**
* Calls widget control callback.
*
* @since 2.5.0
*
* @global callable[] $wp_dashboard_control_callbacks
*
* @param int|false $widget_control_id Optional. Registered widget ID. Default false.
*/
function wp_dashboard_trigger_widget_control( $widget_control_id = false ) {
global $wp_dashboard_control_callbacks;
if ( is_scalar( $widget_control_id ) && $widget_control_id
&& isset( $wp_dashboard_control_callbacks[ $widget_control_id ] )
&& is_callable( $wp_dashboard_control_callbacks[ $widget_control_id ] )
) {
call_user_func(
$wp_dashboard_control_callbacks[ $widget_control_id ],
'',
array(
'id' => $widget_control_id,
'callback' => $wp_dashboard_control_callbacks[ $widget_control_id ],
)
);
}
}
/**
* Sets up the RSS dashboard widget control and $args to be used as input to wp_widget_rss_form().
*
* Handles POST data from RSS-type widgets.
*
* @since 2.5.0
*
* @param string $widget_id
* @param array $form_inputs
*/
function wp_dashboard_rss_control( $widget_id, $form_inputs = array() ) {
$widget_options = get_option( 'dashboard_widget_options' );
if ( ! $widget_options ) {
$widget_options = array();
}
if ( ! isset( $widget_options[ $widget_id ] ) ) {
$widget_options[ $widget_id ] = array();
}
$number = 1; // Hack to use wp_widget_rss_form().
$widget_options[ $widget_id ]['number'] = $number;
if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['widget-rss'][ $number ] ) ) {
$_POST['widget-rss'][ $number ] = wp_unslash( $_POST['widget-rss'][ $number ] );
$widget_options[ $widget_id ] = wp_widget_rss_process( $_POST['widget-rss'][ $number ] );
$widget_options[ $widget_id ]['number'] = $number;
// Title is optional. If black, fill it if possible.
if ( ! $widget_options[ $widget_id ]['title'] && isset( $_POST['widget-rss'][ $number ]['title'] ) ) {
$rss = fetch_feed( $widget_options[ $widget_id ]['url'] );
if ( is_wp_error( $rss ) ) {
$widget_options[ $widget_id ]['title'] = htmlentities( __( 'Unknown Feed' ) );
} else {
$widget_options[ $widget_id ]['title'] = htmlentities( strip_tags( $rss->get_title() ) );
$rss->__destruct();
unset( $rss );
}
}
update_option( 'dashboard_widget_options', $widget_options, false );
$locale = get_user_locale();
$cache_key = 'dash_v2_' . md5( $widget_id . '_' . $locale );
delete_transient( $cache_key );
}
wp_widget_rss_form( $widget_options[ $widget_id ], $form_inputs );
}
/**
* Renders the Events and News dashboard widget.
*
* @since 4.8.0
*/
function wp_dashboard_events_news() {
wp_print_community_events_markup();
?>
<div class="wordpress-news hide-if-no-js">
<?php wp_dashboard_primary(); ?>
</div>
<p class="community-events-footer">
<?php
printf(
'<a href="%1$s" target="_blank">%2$s <span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
'https://make.wordpress.org/community/meetups-landing-page',
__( 'Meetups' ),
/* translators: Hidden accessibility text. */
__( '(opens in a new tab)' )
);
?>
|
<?php
printf(
'<a href="%1$s" target="_blank">%2$s <span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
'https://central.wordcamp.org/schedule/',
__( 'WordCamps' ),
/* translators: Hidden accessibility text. */
__( '(opens in a new tab)' )
);
?>
|
<?php
printf(
'<a href="%1$s" target="_blank">%2$s <span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
/* translators: If a Rosetta site exists (e.g. https://es.wordpress.org/news/), then use that. Otherwise, leave untranslated. */
esc_url( _x( 'https://wordpress.org/news/', 'Events and News dashboard widget' ) ),
__( 'News' ),
/* translators: Hidden accessibility text. */
__( '(opens in a new tab)' )
);
?>
</p>
<?php
}
/**
* Prints the markup for the Community Events section of the Events and News Dashboard widget.
*
* @since 4.8.0
*/
function wp_print_community_events_markup() {
$community_events_notice = '<p class="hide-if-js">' . ( 'This widget requires JavaScript.' ) . '</p>';
$community_events_notice .= '<p class="community-events-error-occurred" aria-hidden="true">' . __( 'An error occurred. Please try again.' ) . '</p>';
$community_events_notice .= '<p class="community-events-could-not-locate" aria-hidden="true"></p>';
wp_admin_notice(
$community_events_notice,
array(
'type' => 'error',
'additional_classes' => array( 'community-events-errors', 'inline', 'hide-if-js' ),
'paragraph_wrap' => false,
)
);
/*
* Hide the main element when the page first loads, because the content
* won't be ready until wp.communityEvents.renderEventsTemplate() has run.
*/
?>
<div id="community-events" class="community-events" aria-hidden="true">
<div class="activity-block">
<p>
<span id="community-events-location-message"></span>
<button class="button-link community-events-toggle-location" aria-expanded="false">
<span class="dashicons dashicons-location" aria-hidden="true"></span>
<span class="community-events-location-edit"><?php _e( 'Select location' ); ?></span>
</button>
</p>
<form class="community-events-form" aria-hidden="true" action="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>" method="post">
<label for="community-events-location">
<?php _e( 'City:' ); ?>
</label>
<?php
/* translators: Replace with a city related to your locale.
* Test that it matches the expected location and has upcoming
* events before including it. If no cities related to your
* locale have events, then use a city related to your locale
* that would be recognizable to most users. Use only the city
* name itself, without any region or country. Use the endonym
* (native locale name) instead of the English name if possible.
*/
?>
<input id="community-events-location" class="regular-text" type="text" name="community-events-location" placeholder="<?php esc_attr_e( 'Cincinnati' ); ?>" />
<?php submit_button( __( 'Submit' ), 'secondary', 'community-events-submit', false ); ?>
<button class="community-events-cancel button-link" type="button" aria-expanded="false">
<?php _e( 'Cancel' ); ?>
</button>
<span class="spinner"></span>
</form>
</div>
<ul class="community-events-results activity-block last"></ul>
</div>
<?php
}
/**
* Renders the events templates for the Event and News widget.
*
* @since 4.8.0
*/
function wp_print_community_events_templates() {
?>
<script id="tmpl-community-events-attend-event-near" type="text/template">
<?php
printf(
/* translators: %s: The name of a city. */
__( 'Attend an upcoming event near %s.' ),
'<strong>{{ data.location.description }}</strong>'
);
?>
</script>
<script id="tmpl-community-events-could-not-locate" type="text/template">
<?php
printf(
/* translators: %s is the name of the city we couldn't locate.
* Replace the examples with cities in your locale, but test
* that they match the expected location before including them.
* Use endonyms (native locale names) whenever possible.
*/
__( '%s could not be located. Please try another nearby city. For example: Kansas City; Springfield; Portland.' ),
'<em>{{data.unknownCity}}</em>'
);
?>
</script>
<script id="tmpl-community-events-event-list" type="text/template">
<# _.each( data.events, function( event ) { #>
<li class="event event-{{ event.type }} wp-clearfix">
<div class="event-info">
<div class="dashicons event-icon" aria-hidden="true"></div>
<div class="event-info-inner">
<a class="event-title" href="{{ event.url }}">{{ event.title }}</a>
<# if ( event.type ) {
const titleCaseEventType = event.type.replace(
/\w\S*/g,
function ( type ) { return type.charAt(0).toUpperCase() + type.substr(1).toLowerCase(); }
);
#>
{{ 'wordcamp' === event.type ? 'WordCamp' : titleCaseEventType }}
<span class="ce-separator"></span>
<# } #>
<span class="event-city">{{ event.location.location }}</span>
</div>
</div>
<div class="event-date-time">
<span class="event-date">{{ event.user_formatted_date }}</span>
<# if ( 'meetup' === event.type ) { #>
<span class="event-time">
{{ event.user_formatted_time }} {{ event.timeZoneAbbreviation }}
</span>
<# } #>
</div>
</li>
<# } ) #>
<# if ( data.events.length <= 2 ) { #>
<li class="event-none">
<?php
printf(
/* translators: %s: Localized meetup organization documentation URL. */
__( 'Want more events? <a href="%s">Help organize the next one</a>!' ),
__( 'https://make.wordpress.org/community/organize-event-landing-page/' )
);
?>
</li>
<# } #>
</script>
<script id="tmpl-community-events-no-upcoming-events" type="text/template">
<li class="event-none">
<# if ( data.location.description ) { #>
<?php
printf(
/* translators: 1: The city the user searched for, 2: Meetup organization documentation URL. */
__( 'There are no events scheduled near %1$s at the moment. Would you like to <a href="%2$s">organize a WordPress event</a>?' ),
'{{ data.location.description }}',
__( 'https://make.wordpress.org/community/handbook/meetup-organizer/welcome/' )
);
?>
<# } else { #>
<?php
printf(
/* translators: %s: Meetup organization documentation URL. */
__( 'There are no events scheduled near you at the moment. Would you like to <a href="%s">organize a WordPress event</a>?' ),
__( 'https://make.wordpress.org/community/handbook/meetup-organizer/welcome/' )
);
?>
<# } #>
</li>
</script>
<?php
}
/**
* 'WordPress Events and News' dashboard widget.
*
* @since 2.7.0
* @since 4.8.0 Removed popular plugins feed.
*/
function wp_dashboard_primary() {
$feeds = array(
'news' => array(
/**
* Filters the primary link URL for the 'WordPress Events and News' dashboard widget.
*
* @since 2.5.0
*
* @param string $link The widget's primary link URL.
*/
'link' => apply_filters( 'dashboard_primary_link', __( 'https://wordpress.org/news/' ) ),
/**
* Filters the primary feed URL for the 'WordPress Events and News' dashboard widget.
*
* @since 2.3.0
*
* @param string $url The widget's primary feed URL.
*/
'url' => apply_filters( 'dashboard_primary_feed', __( 'https://wordpress.org/news/feed/' ) ),
/**
* Filters the primary link title for the 'WordPress Events and News' dashboard widget.
*
* @since 2.3.0
*
* @param string $title Title attribute for the widget's primary link.
*/
'title' => apply_filters( 'dashboard_primary_title', __( 'WordPress Blog' ) ),
'items' => 2,
'show_summary' => 0,
'show_author' => 0,
'show_date' => 0,
),
'planet' => array(
/**
* Filters the secondary link URL for the 'WordPress Events and News' dashboard widget.
*
* @since 2.3.0
*
* @param string $link The widget's secondary link URL.
*/
'link' => apply_filters(
'dashboard_secondary_link',
/* translators: Link to the Planet website of the locale. */
__( 'https://planet.wordpress.org/' )
),
/**
* Filters the secondary feed URL for the 'WordPress Events and News' dashboard widget.
*
* @since 2.3.0
*
* @param string $url The widget's secondary feed URL.
*/
'url' => apply_filters(
'dashboard_secondary_feed',
/* translators: Link to the Planet feed of the locale. */
__( 'https://planet.wordpress.org/feed/' )
),
/**
* Filters the secondary link title for the 'WordPress Events and News' dashboard widget.
*
* @since 2.3.0
*
* @param string $title Title attribute for the widget's secondary link.
*/
'title' => apply_filters( 'dashboard_secondary_title', __( 'Other WordPress News' ) ),
/**
* Filters the number of secondary link items for the 'WordPress Events and News' dashboard widget.
*
* @since 4.4.0
*
* @param string $items How many items to show in the secondary feed.
*/
'items' => apply_filters( 'dashboard_secondary_items', 3 ),
'show_summary' => 0,
'show_author' => 0,
'show_date' => 0,
),
);
wp_dashboard_cached_rss_widget( 'dashboard_primary', 'wp_dashboard_primary_output', $feeds );
}
/**
* Displays the WordPress events and news feeds.
*
* @since 3.8.0
* @since 4.8.0 Removed popular plugins feed.
*
* @param string $widget_id Widget ID.
* @param array $feeds Array of RSS feeds.
*/
function wp_dashboard_primary_output( $widget_id, $feeds ) {
foreach ( $feeds as $type => $args ) {
$args['type'] = $type;
echo '<div class="rss-widget">';
wp_widget_rss_output( $args['url'], $args );
echo '</div>';
}
}
/**
* Displays file upload quota on dashboard.
*
* Runs on the {@see 'activity_box_end'} hook in wp_dashboard_right_now().
*
* @since 3.0.0
*
* @return true|void True if not multisite, user can't upload files, or the space check option is disabled.
*/
function wp_dashboard_quota() {
if ( ! is_multisite() || ! current_user_can( 'upload_files' )
|| get_site_option( 'upload_space_check_disabled' )
) {
return true;
}
$quota = get_space_allowed();
$used = get_space_used();
if ( $used > $quota ) {
$percentused = '100';
} else {
$percentused = ( $used / $quota ) * 100;
}
$used_class = ( $percentused >= 70 ) ? ' warning' : '';
$used = round( $used, 2 );
$percentused = number_format( $percentused );
?>
<h3 class="mu-storage"><?php _e( 'Storage Space' ); ?></h3>
<div class="mu-storage">
<ul>
<li class="storage-count">
<?php
$text = sprintf(
/* translators: %s: Number of megabytes. */
__( '%s MB Space Allowed' ),
number_format_i18n( $quota )
);
printf(
'<a href="%1$s">%2$s<span class="screen-reader-text"> (%3$s)</span></a>',
esc_url( admin_url( 'upload.php' ) ),
$text,
/* translators: Hidden accessibility text. */
__( 'Manage Uploads' )
);
?>
</li><li class="storage-count <?php echo $used_class; ?>">
<?php
$text = sprintf(
/* translators: 1: Number of megabytes, 2: Percentage. */
__( '%1$s MB (%2$s%%) Space Used' ),
number_format_i18n( $used, 2 ),
$percentused
);
printf(
'<a href="%1$s" class="musublink">%2$s<span class="screen-reader-text"> (%3$s)</span></a>',
esc_url( admin_url( 'upload.php' ) ),
$text,
/* translators: Hidden accessibility text. */
__( 'Manage Uploads' )
);
?>
</li>
</ul>
</div>
<?php
}
/**
* Displays the browser update nag.
*
* @since 3.2.0
* @since 5.8.0 Added a special message for Internet Explorer users.
*
* @global bool $is_IE
*/
function wp_dashboard_browser_nag() {
global $is_IE;
$notice = '';
$response = wp_check_browser_version();
if ( $response ) {
if ( $is_IE ) {
$msg = __( 'Internet Explorer does not give you the best WordPress experience. Switch to Microsoft Edge, or another more modern browser to get the most from your site.' );
} elseif ( $response['insecure'] ) {
$msg = sprintf(
/* translators: %s: Browser name and link. */
__( "It looks like you're using an insecure version of %s. Using an outdated browser makes your computer unsafe. For the best WordPress experience, please update your browser." ),
sprintf( '<a href="%s">%s</a>', esc_url( $response['update_url'] ), esc_html( $response['name'] ) )
);
} else {
$msg = sprintf(
/* translators: %s: Browser name and link. */
__( "It looks like you're using an old version of %s. For the best WordPress experience, please update your browser." ),
sprintf( '<a href="%s">%s</a>', esc_url( $response['update_url'] ), esc_html( $response['name'] ) )
);
}
$browser_nag_class = '';
if ( ! empty( $response['img_src'] ) ) {
$img_src = ( is_ssl() && ! empty( $response['img_src_ssl'] ) ) ? $response['img_src_ssl'] : $response['img_src'];
$notice .= '<div class="alignright browser-icon"><img src="' . esc_url( $img_src ) . '" alt="" /></div>';
$browser_nag_class = ' has-browser-icon';
}
$notice .= "<p class='browser-update-nag{$browser_nag_class}'>{$msg}</p>";
$browsehappy = 'https://browsehappy.com/';
$locale = get_user_locale();
if ( 'en_US' !== $locale ) {
$browsehappy = add_query_arg( 'locale', $locale, $browsehappy );
}
if ( $is_IE ) {
$msg_browsehappy = sprintf(
/* translators: %s: Browse Happy URL. */
__( 'Learn how to <a href="%s" class="update-browser-link">browse happy</a>' ),
esc_url( $browsehappy )
);
} else {
$msg_browsehappy = sprintf(
/* translators: 1: Browser update URL, 2: Browser name, 3: Browse Happy URL. */
__( '<a href="%1$s" class="update-browser-link">Update %2$s</a> or learn how to <a href="%3$s" class="browse-happy-link">browse happy</a>' ),
esc_attr( $response['update_url'] ),
esc_html( $response['name'] ),
esc_url( $browsehappy )
);
}
$notice .= '<p>' . $msg_browsehappy . '</p>';
$notice .= '<p class="hide-if-no-js"><a href="" class="dismiss" aria-label="' . esc_attr__( 'Dismiss the browser warning panel' ) . '">' . __( 'Dismiss' ) . '</a></p>';
$notice .= '<div class="clear"></div>';
}
/**
* Filters the notice output for the 'Browse Happy' nag meta box.
*
* @since 3.2.0
*
* @param string $notice The notice content.
* @param array|false $response An array containing web browser information, or
* false on failure. See wp_check_browser_version().
*/
echo apply_filters( 'browse-happy-notice', $notice, $response ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
}
/**
* Adds an additional class to the browser nag if the current version is insecure.
*
* @since 3.2.0
*
* @param string[] $classes Array of meta box classes.
* @return string[] Modified array of meta box classes.
*/
function dashboard_browser_nag_class( $classes ) {
$response = wp_check_browser_version();
if ( $response && $response['insecure'] ) {
$classes[] = 'browser-insecure';
}
return $classes;
}
/**
* Checks if the user needs a browser update.
*
* @since 3.2.0
*
* @return array|false Array of browser data on success, false on failure.
*/
function wp_check_browser_version() {
if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
return false;
}
$key = md5( $_SERVER['HTTP_USER_AGENT'] );
$response = get_site_transient( 'browser_' . $key );
if ( false === $response ) {
$url = 'http://api.wordpress.org/core/browse-happy/1.1/';
$options = array(
'body' => array( 'useragent' => $_SERVER['HTTP_USER_AGENT'] ),
'user-agent' => 'WordPress/' . wp_get_wp_version() . '; ' . home_url( '/' ),
);
if ( wp_http_supports( array( 'ssl' ) ) ) {
$url = set_url_scheme( $url, 'https' );
}
$response = wp_remote_post( $url, $options );
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
return false;
}
/**
* Response should be an array with:
* 'platform' - string - A user-friendly platform name, if it can be determined
* 'name' - string - A user-friendly browser name
* 'version' - string - The version of the browser the user is using
* 'current_version' - string - The most recent version of the browser
* 'upgrade' - boolean - Whether the browser needs an upgrade
* 'insecure' - boolean - Whether the browser is deemed insecure
* 'update_url' - string - The url to visit to upgrade
* 'img_src' - string - An image representing the browser
* 'img_src_ssl' - string - An image (over SSL) representing the browser
*/
$response = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! is_array( $response ) ) {
return false;
}
set_site_transient( 'browser_' . $key, $response, WEEK_IN_SECONDS );
}
return $response;
}
/**
* Displays the PHP update nag.
*
* @since 5.1.0
*/
function wp_dashboard_php_nag() {
$response = wp_check_php_version();
if ( ! $response ) {
return;
}
if ( isset( $response['is_secure'] ) && ! $response['is_secure'] ) {
// The `is_secure` array key name doesn't actually imply this is a secure version of PHP. It only means it receives security updates.
if ( $response['is_lower_than_future_minimum'] ) {
$message = sprintf(
/* translators: %s: The server PHP version. */
__( 'Your site is running on an outdated version of PHP (%s), which does not receive security updates and soon will not be supported by WordPress. Ensure that PHP is updated on your server as soon as possible. Otherwise you will not be able to upgrade WordPress.' ),
PHP_VERSION
);
} else {
$message = sprintf(
/* translators: %s: The server PHP version. */
__( 'Your site is running on an outdated version of PHP (%s), which does not receive security updates. It should be updated.' ),
PHP_VERSION
);
}
} elseif ( $response['is_lower_than_future_minimum'] ) {
$message = sprintf(
/* translators: %s: The server PHP version. */
__( 'Your site is running on an outdated version of PHP (%s), which soon will not be supported by WordPress. Ensure that PHP is updated on your server as soon as possible. Otherwise you will not be able to upgrade WordPress.' ),
PHP_VERSION
);
} else {
$message = sprintf(
/* translators: %s: The server PHP version. */
__( 'Your site is running on an outdated version of PHP (%s), which should be updated.' ),
PHP_VERSION
);
}
?>
<p class="bigger-bolder-text"><?php echo $message; ?></p>
<p><?php _e( 'What is PHP and how does it affect my site?' ); ?></p>
<p>
<?php _e( 'PHP is one of the programming languages used to build WordPress. Newer versions of PHP receive regular security updates and may increase your site&#8217;s performance.' ); ?>
<?php
if ( ! empty( $response['recommended_version'] ) ) {
printf(
/* translators: %s: The minimum recommended PHP version. */
__( 'The minimum recommended version of PHP is %s.' ),
$response['recommended_version']
);
}
?>
</p>
<p class="button-container">
<?php
printf(
'<a class="button button-primary" href="%1$s" target="_blank">%2$s<span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
esc_url( wp_get_update_php_url() ),
__( 'Learn more about updating PHP' ),
/* translators: Hidden accessibility text. */
__( '(opens in a new tab)' )
);
?>
</p>
<?php
wp_update_php_annotation();
wp_direct_php_update_button();
}
/**
* Adds an additional class to the PHP nag if the current version is insecure.
*
* @since 5.1.0
*
* @param string[] $classes Array of meta box classes.
* @return string[] Modified array of meta box classes.
*/
function dashboard_php_nag_class( $classes ) {
$response = wp_check_php_version();
if ( ! $response ) {
return $classes;
}
if ( isset( $response['is_secure'] ) && ! $response['is_secure'] ) {
$classes[] = 'php-no-security-updates';
} elseif ( $response['is_lower_than_future_minimum'] ) {
$classes[] = 'php-version-lower-than-future-minimum';
}
return $classes;
}
/**
* Displays the Site Health Status widget.
*
* @since 5.4.0
*/
function wp_dashboard_site_health() {
$get_issues = get_transient( 'health-check-site-status-result' );
$issue_counts = array();
if ( false !== $get_issues ) {
$issue_counts = json_decode( $get_issues, true );
}
if ( ! is_array( $issue_counts ) || ! $issue_counts ) {
$issue_counts = array(
'good' => 0,
'recommended' => 0,
'critical' => 0,
);
}
$issues_total = $issue_counts['recommended'] + $issue_counts['critical'];
?>
<div class="health-check-widget">
<div class="health-check-widget-title-section site-health-progress-wrapper loading hide-if-no-js">
<div class="site-health-progress">
<svg aria-hidden="true" focusable="false" width="100%" height="100%" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg">
<circle r="90" cx="100" cy="100" fill="transparent" stroke-dasharray="565.48" stroke-dashoffset="0"></circle>
<circle id="bar" r="90" cx="100" cy="100" fill="transparent" stroke-dasharray="565.48" stroke-dashoffset="0"></circle>
</svg>
</div>
<div class="site-health-progress-label">
<?php if ( false === $get_issues ) : ?>
<?php _e( 'No information yet&hellip;' ); ?>
<?php else : ?>
<?php _e( 'Results are still loading&hellip;' ); ?>
<?php endif; ?>
</div>
</div>
<div class="site-health-details">
<?php if ( false === $get_issues ) : ?>
<p>
<?php
printf(
/* translators: %s: URL to Site Health screen. */
__( 'Site health checks will automatically run periodically to gather information about your site. You can also <a href="%s">visit the Site Health screen</a> to gather information about your site now.' ),
esc_url( admin_url( 'site-health.php' ) )
);
?>
</p>
<?php else : ?>
<p>
<?php if ( $issues_total <= 0 ) : ?>
<?php _e( 'Great job! Your site currently passes all site health checks.' ); ?>
<?php elseif ( 1 === (int) $issue_counts['critical'] ) : ?>
<?php _e( 'Your site has a critical issue that should be addressed as soon as possible to improve its performance and security.' ); ?>
<?php elseif ( $issue_counts['critical'] > 1 ) : ?>
<?php _e( 'Your site has critical issues that should be addressed as soon as possible to improve its performance and security.' ); ?>
<?php elseif ( 1 === (int) $issue_counts['recommended'] ) : ?>
<?php _e( 'Your site&#8217;s health is looking good, but there is still one thing you can do to improve its performance and security.' ); ?>
<?php else : ?>
<?php _e( 'Your site&#8217;s health is looking good, but there are still some things you can do to improve its performance and security.' ); ?>
<?php endif; ?>
</p>
<?php endif; ?>
<?php if ( $issues_total > 0 && false !== $get_issues ) : ?>
<p>
<?php
printf(
/* translators: 1: Number of issues. 2: URL to Site Health screen. */
_n(
'Take a look at the <strong>%1$d item</strong> on the <a href="%2$s">Site Health screen</a>.',
'Take a look at the <strong>%1$d items</strong> on the <a href="%2$s">Site Health screen</a>.',
$issues_total
),
$issues_total,
esc_url( admin_url( 'site-health.php' ) )
);
?>
</p>
<?php endif; ?>
</div>
</div>
<?php
}
/**
* Outputs empty dashboard widget to be populated by JS later.
*
* Usable by plugins.
*
* @since 2.5.0
*/
function wp_dashboard_empty() {}
/**
* Displays a welcome panel to introduce users to WordPress.
*
* @since 3.3.0
* @since 5.9.0 Send users to the Site Editor if the active theme is block-based.
*/
function wp_welcome_panel() {
list( $display_version ) = explode( '-', wp_get_wp_version() );
$can_customize = current_user_can( 'customize' );
$is_block_theme = wp_is_block_theme();
?>
<div class="welcome-panel-content">
<div class="welcome-panel-header">
<div class="welcome-panel-header-image">
<?php echo file_get_contents( dirname( __DIR__ ) . '/images/dashboard-background.svg' ); ?>
</div>
<h2><?php _e( 'Welcome to WordPress!' ); ?></h2>
<p>
<a href="<?php echo esc_url( admin_url( 'about.php' ) ); ?>">
<?php
/* translators: %s: Current WordPress version. */
printf( __( 'Learn more about the %s version.' ), esc_html( $display_version ) );
?>
</a>
</p>
</div>
<div class="welcome-panel-column-container">
<div class="welcome-panel-column">
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
<rect width="48" height="48" rx="4" fill="#1E1E1E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.0668 17.0854L28.8221 13.9454L18.2008 24.671L16.8983 29.0827L21.4257 27.8309L32.0668 17.0854ZM16 32.75H24V31.25H16V32.75Z" fill="white"/>
</svg>
<div class="welcome-panel-column-content">
<h3><?php _e( 'Author rich content with blocks and patterns' ); ?></h3>
<p><?php _e( 'Block patterns are pre-configured block layouts. Use them to get inspired or create new pages in a flash.' ); ?></p>
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=page' ) ); ?>"><?php _e( 'Add a new page' ); ?></a>
</div>
</div>
<div class="welcome-panel-column">
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
<rect width="48" height="48" rx="4" fill="#1E1E1E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 16h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H18a2 2 0 0 1-2-2V18a2 2 0 0 1 2-2zm12 1.5H18a.5.5 0 0 0-.5.5v3h13v-3a.5.5 0 0 0-.5-.5zm.5 5H22v8h8a.5.5 0 0 0 .5-.5v-7.5zm-10 0h-3V30a.5.5 0 0 0 .5.5h2.5v-8z" fill="#fff"/>
</svg>
<div class="welcome-panel-column-content">
<?php if ( $is_block_theme ) : ?>
<h3><?php _e( 'Customize your entire site with block themes' ); ?></h3>
<p><?php _e( 'Design everything on your site &#8212; from the header down to the footer, all using blocks and patterns.' ); ?></p>
<a href="<?php echo esc_url( admin_url( 'site-editor.php' ) ); ?>"><?php _e( 'Open site editor' ); ?></a>
<?php else : ?>
<h3><?php _e( 'Start Customizing' ); ?></h3>
<p><?php _e( 'Configure your site&#8217;s logo, header, menus, and more in the Customizer.' ); ?></p>
<?php if ( $can_customize ) : ?>
<a class="load-customize hide-if-no-customize" href="<?php echo wp_customize_url(); ?>"><?php _e( 'Open the Customizer' ); ?></a>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<div class="welcome-panel-column">
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
<rect width="48" height="48" rx="4" fill="#1E1E1E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M31 24a7 7 0 0 1-7 7V17a7 7 0 0 1 7 7zm-7-8a8 8 0 1 1 0 16 8 8 0 0 1 0-16z" fill="#fff"/>
</svg>
<div class="welcome-panel-column-content">
<?php if ( $is_block_theme ) : ?>
<h3><?php _e( 'Switch up your site&#8217;s look & feel with Styles' ); ?></h3>
<p><?php _e( 'Tweak your site, or give it a whole new look! Get creative &#8212; how about a new color palette or font?' ); ?></p>
<a href="<?php echo esc_url( admin_url( '/site-editor.php?path=%2Fwp_global_styles' ) ); ?>"><?php _e( 'Edit styles' ); ?></a>
<?php else : ?>
<h3><?php _e( 'Discover a new way to build your site.' ); ?></h3>
<p><?php _e( 'There is a new kind of WordPress theme, called a block theme, that lets you build the site you&#8217;ve always wanted &#8212; with blocks and styles.' ); ?></p>
<a href="<?php echo esc_url( __( 'https://wordpress.org/documentation/article/block-themes/' ) ); ?>"><?php _e( 'Learn about block themes' ); ?></a>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php
}
<?php
/**
* Core Navigation Menu API
*
* @package WordPress
* @subpackage Nav_Menus
* @since 3.0.0
*/
/** Walker_Nav_Menu_Edit class */
require_once ABSPATH . 'wp-admin/includes/class-walker-nav-menu-edit.php';
/** Walker_Nav_Menu_Checklist class */
require_once ABSPATH . 'wp-admin/includes/class-walker-nav-menu-checklist.php';
/**
* Prints the appropriate response to a menu quick search.
*
* @since 3.0.0
*
* @param array $request The unsanitized request values.
*/
function _wp_ajax_menu_quick_search( $request = array() ) {
$args = array();
$type = isset( $request['type'] ) ? $request['type'] : '';
$object_type = isset( $request['object_type'] ) ? $request['object_type'] : '';
$query = isset( $request['q'] ) ? $request['q'] : '';
$response_format = isset( $request['response-format'] ) ? $request['response-format'] : '';
if ( ! $response_format || ! in_array( $response_format, array( 'json', 'markup' ), true ) ) {
$response_format = 'json';
}
if ( 'markup' === $response_format ) {
$args['walker'] = new Walker_Nav_Menu_Checklist();
}
if ( 'get-post-item' === $type ) {
if ( post_type_exists( $object_type ) ) {
if ( isset( $request['ID'] ) ) {
$object_id = (int) $request['ID'];
if ( 'markup' === $response_format ) {
echo walk_nav_menu_tree(
array_map( 'wp_setup_nav_menu_item', array( get_post( $object_id ) ) ),
0,
(object) $args
);
} elseif ( 'json' === $response_format ) {
echo wp_json_encode(
array(
'ID' => $object_id,
'post_title' => get_the_title( $object_id ),
'post_type' => get_post_type( $object_id ),
)
);
echo "\n";
}
}
} elseif ( taxonomy_exists( $object_type ) ) {
if ( isset( $request['ID'] ) ) {
$object_id = (int) $request['ID'];
if ( 'markup' === $response_format ) {
echo walk_nav_menu_tree(
array_map( 'wp_setup_nav_menu_item', array( get_term( $object_id, $object_type ) ) ),
0,
(object) $args
);
} elseif ( 'json' === $response_format ) {
$post_obj = get_term( $object_id, $object_type );
echo wp_json_encode(
array(
'ID' => $object_id,
'post_title' => $post_obj->name,
'post_type' => $object_type,
)
);
echo "\n";
}
}
}
} elseif ( preg_match( '/quick-search-(posttype|taxonomy)-([a-zA-Z_-]*\b)/', $type, $matches ) ) {
if ( 'posttype' === $matches[1] && get_post_type_object( $matches[2] ) ) {
$post_type_obj = _wp_nav_menu_meta_box_object( get_post_type_object( $matches[2] ) );
$args = array_merge(
$args,
array(
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'posts_per_page' => 10,
'post_type' => $matches[2],
's' => $query,
)
);
if ( isset( $post_type_obj->_default_query ) ) {
$args = array_merge( $args, (array) $post_type_obj->_default_query );
}
$search_results_query = new WP_Query( $args );
if ( ! $search_results_query->have_posts() ) {
return;
}
while ( $search_results_query->have_posts() ) {
$post = $search_results_query->next_post();
if ( 'markup' === $response_format ) {
$var_by_ref = $post->ID;
echo walk_nav_menu_tree(
array_map( 'wp_setup_nav_menu_item', array( get_post( $var_by_ref ) ) ),
0,
(object) $args
);
} elseif ( 'json' === $response_format ) {
echo wp_json_encode(
array(
'ID' => $post->ID,
'post_title' => get_the_title( $post->ID ),
'post_type' => $matches[2],
)
);
echo "\n";
}
}
} elseif ( 'taxonomy' === $matches[1] ) {
$terms = get_terms(
array(
'taxonomy' => $matches[2],
'name__like' => $query,
'number' => 10,
'hide_empty' => false,
)
);
if ( empty( $terms ) || is_wp_error( $terms ) ) {
return;
}
foreach ( (array) $terms as $term ) {
if ( 'markup' === $response_format ) {
echo walk_nav_menu_tree(
array_map( 'wp_setup_nav_menu_item', array( $term ) ),
0,
(object) $args
);
} elseif ( 'json' === $response_format ) {
echo wp_json_encode(
array(
'ID' => $term->term_id,
'post_title' => $term->name,
'post_type' => $matches[2],
)
);
echo "\n";
}
}
}
}
}
/**
* Register nav menu meta boxes and advanced menu items.
*
* @since 3.0.0
*/
function wp_nav_menu_setup() {
// Register meta boxes.
wp_nav_menu_post_type_meta_boxes();
add_meta_box(
'add-custom-links',
__( 'Custom Links' ),
'wp_nav_menu_item_link_meta_box',
'nav-menus',
'side',
'default'
);
wp_nav_menu_taxonomy_meta_boxes();
// Register advanced menu items (columns).
add_filter( 'manage_nav-menus_columns', 'wp_nav_menu_manage_columns' );
// If first time editing, disable advanced items by default.
if ( false === get_user_option( 'managenav-menuscolumnshidden' ) ) {
$user = wp_get_current_user();
update_user_meta(
$user->ID,
'managenav-menuscolumnshidden',
array(
0 => 'link-target',
1 => 'css-classes',
2 => 'xfn',
3 => 'description',
4 => 'title-attribute',
)
);
}
}
/**
* Limit the amount of meta boxes to pages, posts, links, and categories for first time users.
*
* @since 3.0.0
*
* @global array $wp_meta_boxes Global meta box state.
*/
function wp_initial_nav_menu_meta_boxes() {
global $wp_meta_boxes;
if ( get_user_option( 'metaboxhidden_nav-menus' ) !== false || ! is_array( $wp_meta_boxes ) ) {
return;
}
$initial_meta_boxes = array( 'add-post-type-page', 'add-post-type-post', 'add-custom-links', 'add-category' );
$hidden_meta_boxes = array();
foreach ( array_keys( $wp_meta_boxes['nav-menus'] ) as $context ) {
foreach ( array_keys( $wp_meta_boxes['nav-menus'][ $context ] ) as $priority ) {
foreach ( $wp_meta_boxes['nav-menus'][ $context ][ $priority ] as $box ) {
if ( in_array( $box['id'], $initial_meta_boxes, true ) ) {
unset( $box['id'] );
} else {
$hidden_meta_boxes[] = $box['id'];
}
}
}
}
$user = wp_get_current_user();
update_user_meta( $user->ID, 'metaboxhidden_nav-menus', $hidden_meta_boxes );
}
/**
* Creates meta boxes for any post type menu item..
*
* @since 3.0.0
*/
function wp_nav_menu_post_type_meta_boxes() {
$post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'object' );
if ( ! $post_types ) {
return;
}
foreach ( $post_types as $post_type ) {
/**
* Filters whether a menu items meta box will be added for the current
* object type.
*
* If a falsey value is returned instead of an object, the menu items
* meta box for the current meta box object will not be added.
*
* @since 3.0.0
*
* @param WP_Post_Type|false $post_type The current object to add a menu items
* meta box for.
*/
$post_type = apply_filters( 'nav_menu_meta_box_object', $post_type );
if ( $post_type ) {
$id = $post_type->name;
// Give pages a higher priority.
$priority = ( 'page' === $post_type->name ? 'core' : 'default' );
add_meta_box(
"add-post-type-{$id}",
$post_type->labels->name,
'wp_nav_menu_item_post_type_meta_box',
'nav-menus',
'side',
$priority,
$post_type
);
}
}
}
/**
* Creates meta boxes for any taxonomy menu item.
*
* @since 3.0.0
*/
function wp_nav_menu_taxonomy_meta_boxes() {
$taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'object' );
if ( ! $taxonomies ) {
return;
}
foreach ( $taxonomies as $tax ) {
/** This filter is documented in wp-admin/includes/nav-menu.php */
$tax = apply_filters( 'nav_menu_meta_box_object', $tax );
if ( $tax ) {
$id = $tax->name;
add_meta_box(
"add-{$id}",
$tax->labels->name,
'wp_nav_menu_item_taxonomy_meta_box',
'nav-menus',
'side',
'default',
$tax
);
}
}
}
/**
* Check whether to disable the Menu Locations meta box submit button and inputs.
*
* @since 3.6.0
* @since 5.3.1 The `$display` parameter was added.
*
* @global bool $one_theme_location_no_menus to determine if no menus exist
*
* @param int|string $nav_menu_selected_id ID, name, or slug of the currently selected menu.
* @param bool $display Whether to display or just return the string.
* @return string|false Disabled attribute if at least one menu exists, false if not.
*/
function wp_nav_menu_disabled_check( $nav_menu_selected_id, $display = true ) {
global $one_theme_location_no_menus;
if ( $one_theme_location_no_menus ) {
return false;
}
return disabled( $nav_menu_selected_id, 0, $display );
}
/**
* Displays a meta box for the custom links menu item.
*
* @since 3.0.0
*
* @global int $_nav_menu_placeholder
* @global int|string $nav_menu_selected_id
*/
function wp_nav_menu_item_link_meta_box() {
global $_nav_menu_placeholder, $nav_menu_selected_id;
$_nav_menu_placeholder = 0 > $_nav_menu_placeholder ? $_nav_menu_placeholder - 1 : -1;
?>
<div class="customlinkdiv" id="customlinkdiv">
<input type="hidden" value="custom" name="menu-item[<?php echo $_nav_menu_placeholder; ?>][menu-item-type]" />
<p id="menu-item-url-wrap" class="wp-clearfix">
<label class="howto" for="custom-menu-item-url"><?php _e( 'URL' ); ?></label>
<input id="custom-menu-item-url" name="menu-item[<?php echo $_nav_menu_placeholder; ?>][menu-item-url]"
type="text"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
class="code menu-item-textbox form-required" placeholder="https://"
/>
<span id="custom-url-error" class="error-message" style="display: none;"><?php _e( 'Please provide a valid link.' ); ?></span>
</p>
<p id="menu-item-name-wrap" class="wp-clearfix">
<label class="howto" for="custom-menu-item-name"><?php _e( 'Link Text' ); ?></label>
<input id="custom-menu-item-name" name="menu-item[<?php echo $_nav_menu_placeholder; ?>][menu-item-title]"
type="text"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
class="regular-text menu-item-textbox"
/>
</p>
<p class="button-controls wp-clearfix">
<span class="add-to-menu">
<input id="submit-customlinkdiv" name="add-custom-menu-item"
type="submit"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
class="button submit-add-to-menu right" value="<?php esc_attr_e( 'Add to Menu' ); ?>"
/>
<span class="spinner"></span>
</span>
</p>
</div><!-- /.customlinkdiv -->
<?php
}
/**
* Displays a meta box for a post type menu item.
*
* @since 3.0.0
*
* @global int $_nav_menu_placeholder
* @global int|string $nav_menu_selected_id
*
* @param string $data_object Not used.
* @param array $box {
* Post type menu item meta box arguments.
*
* @type string $id Meta box 'id' attribute.
* @type string $title Meta box title.
* @type callable $callback Meta box display callback.
* @type WP_Post_Type $args Extra meta box arguments (the post type object for this meta box).
* }
*/
function wp_nav_menu_item_post_type_meta_box( $data_object, $box ) {
global $_nav_menu_placeholder, $nav_menu_selected_id;
$post_type_name = $box['args']->name;
$post_type = get_post_type_object( $post_type_name );
$tab_name = $post_type_name . '-tab';
// Paginate browsing for large numbers of post objects.
$per_page = 50;
$pagenum = isset( $_REQUEST[ $tab_name ] ) && isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 1;
$offset = 0 < $pagenum ? $per_page * ( $pagenum - 1 ) : 0;
$args = array(
'offset' => $offset,
'order' => 'ASC',
'orderby' => 'title',
'posts_per_page' => $per_page,
'post_type' => $post_type_name,
'suppress_filters' => true,
'update_post_term_cache' => false,
'update_post_meta_cache' => false,
);
if ( isset( $box['args']->_default_query ) ) {
$args = array_merge( $args, (array) $box['args']->_default_query );
}
/*
* If we're dealing with pages, let's prioritize the Front Page,
* Posts Page and Privacy Policy Page at the top of the list.
*/
$important_pages = array();
if ( 'page' === $post_type_name ) {
$suppress_page_ids = array();
// Insert Front Page or custom Home link.
$front_page = 'page' === get_option( 'show_on_front' ) ? (int) get_option( 'page_on_front' ) : 0;
$front_page_obj = null;
if ( ! empty( $front_page ) ) {
$front_page_obj = get_post( $front_page );
}
if ( $front_page_obj ) {
$front_page_obj->front_or_home = true;
$important_pages[] = $front_page_obj;
$suppress_page_ids[] = $front_page_obj->ID;
} else {
$_nav_menu_placeholder = ( 0 > $_nav_menu_placeholder ) ? (int) $_nav_menu_placeholder - 1 : -1;
$front_page_obj = (object) array(
'front_or_home' => true,
'ID' => 0,
'object_id' => $_nav_menu_placeholder,
'post_content' => '',
'post_excerpt' => '',
'post_parent' => '',
'post_title' => _x( 'Home', 'nav menu home label' ),
'post_type' => 'nav_menu_item',
'type' => 'custom',
'url' => home_url( '/' ),
);
$important_pages[] = $front_page_obj;
}
// Insert Posts Page.
$posts_page = 'page' === get_option( 'show_on_front' ) ? (int) get_option( 'page_for_posts' ) : 0;
if ( ! empty( $posts_page ) ) {
$posts_page_obj = get_post( $posts_page );
if ( $posts_page_obj ) {
$front_page_obj->posts_page = true;
$important_pages[] = $posts_page_obj;
$suppress_page_ids[] = $posts_page_obj->ID;
}
}
// Insert Privacy Policy Page.
$privacy_policy_page_id = (int) get_option( 'wp_page_for_privacy_policy' );
if ( ! empty( $privacy_policy_page_id ) ) {
$privacy_policy_page = get_post( $privacy_policy_page_id );
if ( $privacy_policy_page instanceof WP_Post && 'publish' === $privacy_policy_page->post_status ) {
$privacy_policy_page->privacy_policy_page = true;
$important_pages[] = $privacy_policy_page;
$suppress_page_ids[] = $privacy_policy_page->ID;
}
}
// Add suppression array to arguments for WP_Query.
if ( ! empty( $suppress_page_ids ) ) {
$args['post__not_in'] = $suppress_page_ids;
}
}
// @todo Transient caching of these results with proper invalidation on updating of a post of this type.
$get_posts = new WP_Query();
$posts = $get_posts->query( $args );
// Only suppress and insert when more than just suppression pages available.
if ( ! $get_posts->post_count ) {
if ( ! empty( $suppress_page_ids ) ) {
unset( $args['post__not_in'] );
$get_posts = new WP_Query();
$posts = $get_posts->query( $args );
} else {
echo '<p>' . __( 'No items.' ) . '</p>';
return;
}
} elseif ( ! empty( $important_pages ) ) {
$posts = array_merge( $important_pages, $posts );
}
$num_pages = $get_posts->max_num_pages;
$page_links = paginate_links(
array(
'base' => add_query_arg(
array(
$tab_name => 'all',
'paged' => '%#%',
'item-type' => 'post_type',
'item-object' => $post_type_name,
)
),
'format' => '',
'prev_text' => '<span aria-label="' . esc_attr__( 'Previous page' ) . '">' . __( '&laquo;' ) . '</span>',
'next_text' => '<span aria-label="' . esc_attr__( 'Next page' ) . '">' . __( '&raquo;' ) . '</span>',
/* translators: Hidden accessibility text. */
'before_page_number' => '<span class="screen-reader-text">' . __( 'Page' ) . '</span> ',
'total' => $num_pages,
'current' => $pagenum,
)
);
$db_fields = false;
if ( is_post_type_hierarchical( $post_type_name ) ) {
$db_fields = array(
'parent' => 'post_parent',
'id' => 'ID',
);
}
$walker = new Walker_Nav_Menu_Checklist( $db_fields );
$current_tab = 'most-recent';
if ( isset( $_REQUEST[ $tab_name ] ) && in_array( $_REQUEST[ $tab_name ], array( 'all', 'search' ), true ) ) {
$current_tab = $_REQUEST[ $tab_name ];
}
if ( ! empty( $_REQUEST[ "quick-search-posttype-{$post_type_name}" ] ) ) {
$current_tab = 'search';
}
$removed_args = array(
'action',
'customlink-tab',
'edit-menu-item',
'menu-item',
'page-tab',
'_wpnonce',
);
$most_recent_url = '';
$view_all_url = '';
$search_url = '';
if ( $nav_menu_selected_id ) {
$most_recent_url = add_query_arg( $tab_name, 'most-recent', remove_query_arg( $removed_args ) );
$view_all_url = add_query_arg( $tab_name, 'all', remove_query_arg( $removed_args ) );
$search_url = add_query_arg( $tab_name, 'search', remove_query_arg( $removed_args ) );
}
?>
<div id="<?php echo esc_attr( "posttype-{$post_type_name}" ); ?>" class="posttypediv">
<ul id="<?php echo esc_attr( "posttype-{$post_type_name}-tabs" ); ?>" class="posttype-tabs add-menu-item-tabs">
<li <?php echo ( 'most-recent' === $current_tab ? ' class="tabs"' : '' ); ?>>
<a class="nav-tab-link"
data-type="<?php echo esc_attr( "tabs-panel-posttype-{$post_type_name}-most-recent" ); ?>"
href="<?php echo esc_url( $most_recent_url . "#tabs-panel-posttype-{$post_type_name}-most-recent" ); ?>"
>
<?php _e( 'Most Recent' ); ?>
</a>
</li>
<li <?php echo ( 'all' === $current_tab ? ' class="tabs"' : '' ); ?>>
<a class="nav-tab-link"
data-type="<?php echo esc_attr( "{$post_type_name}-all" ); ?>"
href="<?php echo esc_url( $view_all_url . "#{$post_type_name}-all" ); ?>"
>
<?php _e( 'View All' ); ?>
</a>
</li>
<li <?php echo ( 'search' === $current_tab ? ' class="tabs"' : '' ); ?>>
<a class="nav-tab-link"
data-type="<?php echo esc_attr( "tabs-panel-posttype-{$post_type_name}-search" ); ?>"
href="<?php echo esc_url( $search_url . "#tabs-panel-posttype-{$post_type_name}-search" ); ?>"
>
<?php _e( 'Search' ); ?>
</a>
</li>
</ul><!-- .posttype-tabs -->
<div id="<?php echo esc_attr( "tabs-panel-posttype-{$post_type_name}-most-recent" ); ?>"
class="tabs-panel <?php echo ( 'most-recent' === $current_tab ? 'tabs-panel-active' : 'tabs-panel-inactive' ); ?>"
role="region" aria-label="<?php esc_attr_e( 'Most Recent' ); ?>" tabindex="0"
>
<ul id="<?php echo esc_attr( "{$post_type_name}checklist-most-recent" ); ?>"
class="categorychecklist form-no-clear"
>
<?php
$recent_args = array_merge(
$args,
array(
'orderby' => 'post_date',
'order' => 'DESC',
'posts_per_page' => 15,
)
);
$most_recent = $get_posts->query( $recent_args );
$args['walker'] = $walker;
/**
* Filters the posts displayed in the 'Most Recent' tab of the current
* post type's menu items meta box.
*
* The dynamic portion of the hook name, `$post_type_name`, refers to the post type name.
*
* Possible hook names include:
*
* - `nav_menu_items_post_recent`
* - `nav_menu_items_page_recent`
*
* @since 4.3.0
* @since 4.9.0 Added the `$recent_args` parameter.
*
* @param WP_Post[] $most_recent An array of post objects being listed.
* @param array $args An array of `WP_Query` arguments for the meta box.
* @param array $box Arguments passed to `wp_nav_menu_item_post_type_meta_box()`.
* @param array $recent_args An array of `WP_Query` arguments for 'Most Recent' tab.
*/
$most_recent = apply_filters(
"nav_menu_items_{$post_type_name}_recent",
$most_recent,
$args,
$box,
$recent_args
);
echo walk_nav_menu_tree(
array_map( 'wp_setup_nav_menu_item', $most_recent ),
0,
(object) $args
);
?>
</ul>
</div><!-- /.tabs-panel -->
<div id="<?php echo esc_attr( "tabs-panel-posttype-{$post_type_name}-search" ); ?>"
class="tabs-panel <?php echo ( 'search' === $current_tab ? 'tabs-panel-active' : 'tabs-panel-inactive' ); ?>"
role="region" aria-label="<?php echo esc_attr( $post_type->labels->search_items ); ?>" tabindex="0"
>
<?php
if ( isset( $_REQUEST[ "quick-search-posttype-{$post_type_name}" ] ) ) {
$searched = esc_attr( $_REQUEST[ "quick-search-posttype-{$post_type_name}" ] );
$search_results = get_posts(
array(
's' => $searched,
'post_type' => $post_type_name,
'fields' => 'all',
'order' => 'DESC',
)
);
} else {
$searched = '';
$search_results = array();
}
?>
<p class="quick-search-wrap">
<label for="<?php echo esc_attr( "quick-search-posttype-{$post_type_name}" ); ?>" class="screen-reader-text">
<?php
/* translators: Hidden accessibility text. */
_e( 'Search' );
?>
</label>
<input type="search"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
class="quick-search" value="<?php echo $searched; ?>"
name="<?php echo esc_attr( "quick-search-posttype-{$post_type_name}" ); ?>"
id="<?php echo esc_attr( "quick-search-posttype-{$post_type_name}" ); ?>"
/>
<span class="spinner"></span>
<?php
submit_button(
__( 'Search' ),
'small quick-search-submit hide-if-js',
'submit',
false,
array( 'id' => "submit-quick-search-posttype-{$post_type_name}" )
);
?>
</p>
<ul id="<?php echo esc_attr( "{$post_type_name}-search-checklist" ); ?>"
data-wp-lists="<?php echo esc_attr( "list:{$post_type_name}" ); ?>"
class="categorychecklist form-no-clear"
>
<?php if ( ! empty( $search_results ) && ! is_wp_error( $search_results ) ) : ?>
<?php
$args['walker'] = $walker;
echo walk_nav_menu_tree(
array_map( 'wp_setup_nav_menu_item', $search_results ),
0,
(object) $args
);
?>
<?php elseif ( is_wp_error( $search_results ) ) : ?>
<li><?php echo $search_results->get_error_message(); ?></li>
<?php elseif ( ! empty( $searched ) ) : ?>
<li><?php _e( 'No results found.' ); ?></li>
<?php endif; ?>
</ul>
</div><!-- /.tabs-panel -->
<div id="<?php echo esc_attr( "{$post_type_name}-all" ); ?>"
class="tabs-panel tabs-panel-view-all <?php echo ( 'all' === $current_tab ? 'tabs-panel-active' : 'tabs-panel-inactive' ); ?>"
role="region" aria-label="<?php echo esc_attr( $post_type->labels->all_items ); ?>" tabindex="0"
>
<?php if ( ! empty( $page_links ) ) : ?>
<div class="add-menu-item-pagelinks">
<?php echo $page_links; ?>
</div>
<?php endif; ?>
<ul id="<?php echo esc_attr( "{$post_type_name}checklist" ); ?>"
data-wp-lists="<?php echo esc_attr( "list:{$post_type_name}" ); ?>"
class="categorychecklist form-no-clear"
>
<?php
$args['walker'] = $walker;
if ( $post_type->has_archive ) {
$_nav_menu_placeholder = ( 0 > $_nav_menu_placeholder ) ? (int) $_nav_menu_placeholder - 1 : -1;
array_unshift(
$posts,
(object) array(
'ID' => 0,
'object_id' => $_nav_menu_placeholder,
'object' => $post_type_name,
'post_content' => '',
'post_excerpt' => '',
'post_title' => $post_type->labels->archives,
'post_type' => 'nav_menu_item',
'type' => 'post_type_archive',
'url' => get_post_type_archive_link( $post_type_name ),
)
);
}
/**
* Filters the posts displayed in the 'View All' tab of the current
* post type's menu items meta box.
*
* The dynamic portion of the hook name, `$post_type_name`, refers
* to the slug of the current post type.
*
* Possible hook names include:
*
* - `nav_menu_items_post`
* - `nav_menu_items_page`
*
* @since 3.2.0
* @since 4.6.0 Converted the `$post_type` parameter to accept a WP_Post_Type object.
*
* @see WP_Query::query()
*
* @param object[] $posts The posts for the current post type. Mostly `WP_Post` objects, but
* can also contain "fake" post objects to represent other menu items.
* @param array $args An array of `WP_Query` arguments.
* @param WP_Post_Type $post_type The current post type object for this menu item meta box.
*/
$posts = apply_filters(
"nav_menu_items_{$post_type_name}",
$posts,
$args,
$post_type
);
$checkbox_items = walk_nav_menu_tree(
array_map( 'wp_setup_nav_menu_item', $posts ),
0,
(object) $args
);
echo $checkbox_items;
?>
</ul>
<?php if ( ! empty( $page_links ) ) : ?>
<div class="add-menu-item-pagelinks">
<?php echo $page_links; ?>
</div>
<?php endif; ?>
</div><!-- /.tabs-panel -->
<p class="button-controls wp-clearfix" data-items-type="<?php echo esc_attr( "posttype-{$post_type_name}" ); ?>">
<span class="list-controls hide-if-no-js">
<input type="checkbox"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
id="<?php echo esc_attr( $tab_name ); ?>" class="select-all"
/>
<label for="<?php echo esc_attr( $tab_name ); ?>"><?php _e( 'Select All' ); ?></label>
</span>
<span class="add-to-menu">
<input type="submit"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
class="button submit-add-to-menu right" value="<?php esc_attr_e( 'Add to Menu' ); ?>"
name="add-post-type-menu-item" id="<?php echo esc_attr( "submit-posttype-{$post_type_name}" ); ?>"
/>
<span class="spinner"></span>
</span>
</p>
</div><!-- /.posttypediv -->
<?php
}
/**
* Displays a meta box for a taxonomy menu item.
*
* @since 3.0.0
*
* @global int|string $nav_menu_selected_id
*
* @param string $data_object Not used.
* @param array $box {
* Taxonomy menu item meta box arguments.
*
* @type string $id Meta box 'id' attribute.
* @type string $title Meta box title.
* @type callable $callback Meta box display callback.
* @type object $args Extra meta box arguments (the taxonomy object for this meta box).
* }
*/
function wp_nav_menu_item_taxonomy_meta_box( $data_object, $box ) {
global $nav_menu_selected_id;
$taxonomy_name = $box['args']->name;
$taxonomy = get_taxonomy( $taxonomy_name );
$tab_name = $taxonomy_name . '-tab';
// Paginate browsing for large numbers of objects.
$per_page = 50;
$pagenum = isset( $_REQUEST[ $tab_name ] ) && isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 1;
$offset = 0 < $pagenum ? $per_page * ( $pagenum - 1 ) : 0;
$args = array(
'taxonomy' => $taxonomy_name,
'child_of' => 0,
'exclude' => '',
'hide_empty' => false,
'hierarchical' => 1,
'include' => '',
'number' => $per_page,
'offset' => $offset,
'order' => 'ASC',
'orderby' => 'name',
'pad_counts' => false,
);
$terms = get_terms( $args );
if ( ! $terms || is_wp_error( $terms ) ) {
echo '<p>' . __( 'No items.' ) . '</p>';
return;
}
$num_pages = (int) ceil(
(int) wp_count_terms(
array_merge(
$args,
array(
'number' => '',
'offset' => '',
)
)
) / $per_page
);
$page_links = paginate_links(
array(
'base' => add_query_arg(
array(
$tab_name => 'all',
'paged' => '%#%',
'item-type' => 'taxonomy',
'item-object' => $taxonomy_name,
)
),
'format' => '',
'prev_text' => '<span aria-label="' . esc_attr__( 'Previous page' ) . '">' . __( '&laquo;' ) . '</span>',
'next_text' => '<span aria-label="' . esc_attr__( 'Next page' ) . '">' . __( '&raquo;' ) . '</span>',
/* translators: Hidden accessibility text. */
'before_page_number' => '<span class="screen-reader-text">' . __( 'Page' ) . '</span> ',
'total' => $num_pages,
'current' => $pagenum,
)
);
$db_fields = false;
if ( is_taxonomy_hierarchical( $taxonomy_name ) ) {
$db_fields = array(
'parent' => 'parent',
'id' => 'term_id',
);
}
$walker = new Walker_Nav_Menu_Checklist( $db_fields );
$current_tab = 'most-used';
if ( isset( $_REQUEST[ $tab_name ] ) && in_array( $_REQUEST[ $tab_name ], array( 'all', 'most-used', 'search' ), true ) ) {
$current_tab = $_REQUEST[ $tab_name ];
}
if ( ! empty( $_REQUEST[ "quick-search-taxonomy-{$taxonomy_name}" ] ) ) {
$current_tab = 'search';
}
$removed_args = array(
'action',
'customlink-tab',
'edit-menu-item',
'menu-item',
'page-tab',
'_wpnonce',
);
$most_used_url = '';
$view_all_url = '';
$search_url = '';
if ( $nav_menu_selected_id ) {
$most_used_url = add_query_arg( $tab_name, 'most-used', remove_query_arg( $removed_args ) );
$view_all_url = add_query_arg( $tab_name, 'all', remove_query_arg( $removed_args ) );
$search_url = add_query_arg( $tab_name, 'search', remove_query_arg( $removed_args ) );
}
?>
<div id="<?php echo esc_attr( "taxonomy-{$taxonomy_name}" ); ?>" class="taxonomydiv">
<ul id="<?php echo esc_attr( "taxonomy-{$taxonomy_name}-tabs" ); ?>" class="taxonomy-tabs add-menu-item-tabs">
<li <?php echo ( 'most-used' === $current_tab ? ' class="tabs"' : '' ); ?>>
<a class="nav-tab-link"
data-type="<?php echo esc_attr( "tabs-panel-{$taxonomy_name}-pop" ); ?>"
href="<?php echo esc_url( $most_used_url . "#tabs-panel-{$taxonomy_name}-pop" ); ?>"
>
<?php echo esc_html( $taxonomy->labels->most_used ); ?>
</a>
</li>
<li <?php echo ( 'all' === $current_tab ? ' class="tabs"' : '' ); ?>>
<a class="nav-tab-link"
data-type="<?php echo esc_attr( "tabs-panel-{$taxonomy_name}-all" ); ?>"
href="<?php echo esc_url( $view_all_url . "#tabs-panel-{$taxonomy_name}-all" ); ?>"
>
<?php _e( 'View All' ); ?>
</a>
</li>
<li <?php echo ( 'search' === $current_tab ? ' class="tabs"' : '' ); ?>>
<a class="nav-tab-link"
data-type="<?php echo esc_attr( "tabs-panel-search-taxonomy-{$taxonomy_name}" ); ?>"
href="<?php echo esc_url( $search_url . "#tabs-panel-search-taxonomy-{$taxonomy_name}" ); ?>"
>
<?php _e( 'Search' ); ?>
</a>
</li>
</ul><!-- .taxonomy-tabs -->
<div id="<?php echo esc_attr( "tabs-panel-{$taxonomy_name}-pop" ); ?>"
class="tabs-panel <?php echo ( 'most-used' === $current_tab ? 'tabs-panel-active' : 'tabs-panel-inactive' ); ?>"
role="region" aria-label="<?php echo esc_attr( $taxonomy->labels->most_used ); ?>" tabindex="0"
>
<ul id="<?php echo esc_attr( "{$taxonomy_name}checklist-pop" ); ?>"
class="categorychecklist form-no-clear"
>
<?php
$popular_terms = get_terms(
array(
'taxonomy' => $taxonomy_name,
'orderby' => 'count',
'order' => 'DESC',
'number' => 10,
'hierarchical' => false,
)
);
$args['walker'] = $walker;
echo walk_nav_menu_tree(
array_map( 'wp_setup_nav_menu_item', $popular_terms ),
0,
(object) $args
);
?>
</ul>
</div><!-- /.tabs-panel -->
<div id="<?php echo esc_attr( "tabs-panel-{$taxonomy_name}-all" ); ?>"
class="tabs-panel tabs-panel-view-all <?php echo ( 'all' === $current_tab ? 'tabs-panel-active' : 'tabs-panel-inactive' ); ?>"
role="region" aria-label="<?php echo esc_attr( $taxonomy->labels->all_items ); ?>" tabindex="0"
>
<?php if ( ! empty( $page_links ) ) : ?>
<div class="add-menu-item-pagelinks">
<?php echo $page_links; ?>
</div>
<?php endif; ?>
<ul id="<?php echo esc_attr( "{$taxonomy_name}checklist" ); ?>"
data-wp-lists="<?php echo esc_attr( "list:{$taxonomy_name}" ); ?>"
class="categorychecklist form-no-clear"
>
<?php
$args['walker'] = $walker;
echo walk_nav_menu_tree(
array_map( 'wp_setup_nav_menu_item', $terms ),
0,
(object) $args
);
?>
</ul>
<?php if ( ! empty( $page_links ) ) : ?>
<div class="add-menu-item-pagelinks">
<?php echo $page_links; ?>
</div>
<?php endif; ?>
</div><!-- /.tabs-panel -->
<div id="<?php echo esc_attr( "tabs-panel-search-taxonomy-{$taxonomy_name}" ); ?>"
class="tabs-panel <?php echo ( 'search' === $current_tab ? 'tabs-panel-active' : 'tabs-panel-inactive' ); ?>"
role="region" aria-label="<?php echo esc_attr( $taxonomy->labels->search_items ); ?>" tabindex="0">
<?php
if ( isset( $_REQUEST[ "quick-search-taxonomy-{$taxonomy_name}" ] ) ) {
$searched = esc_attr( $_REQUEST[ "quick-search-taxonomy-{$taxonomy_name}" ] );
$search_results = get_terms(
array(
'taxonomy' => $taxonomy_name,
'name__like' => $searched,
'fields' => 'all',
'orderby' => 'count',
'order' => 'DESC',
'hierarchical' => false,
)
);
} else {
$searched = '';
$search_results = array();
}
?>
<p class="quick-search-wrap">
<label for="<?php echo esc_attr( "quick-search-taxonomy-{$taxonomy_name}" ); ?>" class="screen-reader-text">
<?php
/* translators: Hidden accessibility text. */
_e( 'Search' );
?>
</label>
<input type="search"
class="quick-search" value="<?php echo $searched; ?>"
name="<?php echo esc_attr( "quick-search-taxonomy-{$taxonomy_name}" ); ?>"
id="<?php echo esc_attr( "quick-search-taxonomy-{$taxonomy_name}" ); ?>"
/>
<span class="spinner"></span>
<?php
submit_button(
__( 'Search' ),
'small quick-search-submit hide-if-js',
'submit',
false,
array( 'id' => "submit-quick-search-taxonomy-{$taxonomy_name}" )
);
?>
</p>
<ul id="<?php echo esc_attr( "{$taxonomy_name}-search-checklist" ); ?>"
data-wp-lists="<?php echo esc_attr( "list:{$taxonomy_name}" ); ?>"
class="categorychecklist form-no-clear"
>
<?php if ( ! empty( $search_results ) && ! is_wp_error( $search_results ) ) : ?>
<?php
$args['walker'] = $walker;
echo walk_nav_menu_tree(
array_map( 'wp_setup_nav_menu_item', $search_results ),
0,
(object) $args
);
?>
<?php elseif ( is_wp_error( $search_results ) ) : ?>
<li><?php echo $search_results->get_error_message(); ?></li>
<?php elseif ( ! empty( $searched ) ) : ?>
<li><?php _e( 'No results found.' ); ?></li>
<?php endif; ?>
</ul>
</div><!-- /.tabs-panel -->
<p class="button-controls wp-clearfix" data-items-type="<?php echo esc_attr( "taxonomy-{$taxonomy_name}" ); ?>">
<span class="list-controls hide-if-no-js">
<input type="checkbox"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
id="<?php echo esc_attr( $tab_name ); ?>" class="select-all"
/>
<label for="<?php echo esc_attr( $tab_name ); ?>"><?php _e( 'Select All' ); ?></label>
</span>
<span class="add-to-menu">
<input type="submit"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
class="button submit-add-to-menu right" value="<?php esc_attr_e( 'Add to Menu' ); ?>"
name="add-taxonomy-menu-item" id="<?php echo esc_attr( "submit-taxonomy-{$taxonomy_name}" ); ?>"
/>
<span class="spinner"></span>
</span>
</p>
</div><!-- /.taxonomydiv -->
<?php
}
/**
* Save posted nav menu item data.
*
* @since 3.0.0
*
* @param int $menu_id The menu ID for which to save this item. Value of 0 makes a draft, orphaned menu item. Default 0.
* @param array[] $menu_data The unsanitized POSTed menu item data.
* @return int[] The database IDs of the items saved
*/
function wp_save_nav_menu_items( $menu_id = 0, $menu_data = array() ) {
$menu_id = (int) $menu_id;
$items_saved = array();
if ( 0 === $menu_id || is_nav_menu( $menu_id ) ) {
// Loop through all the menu items' POST values.
foreach ( (array) $menu_data as $_possible_db_id => $_item_object_data ) {
if (
// Checkbox is not checked.
empty( $_item_object_data['menu-item-object-id'] ) &&
(
// And item type either isn't set.
! isset( $_item_object_data['menu-item-type'] ) ||
// Or URL is the default.
in_array( $_item_object_data['menu-item-url'], array( 'https://', 'http://', '' ), true ) ||
// Or it's not a custom menu item (but not the custom home page).
! ( 'custom' === $_item_object_data['menu-item-type'] && ! isset( $_item_object_data['menu-item-db-id'] ) ) ||
// Or it *is* a custom menu item that already exists.
! empty( $_item_object_data['menu-item-db-id'] )
)
) {
// Then this potential menu item is not getting added to this menu.
continue;
}
// If this possible menu item doesn't actually have a menu database ID yet.
if (
empty( $_item_object_data['menu-item-db-id'] ) ||
( 0 > $_possible_db_id ) ||
$_possible_db_id !== (int) $_item_object_data['menu-item-db-id']
) {
$_actual_db_id = 0;
} else {
$_actual_db_id = (int) $_item_object_data['menu-item-db-id'];
}
$args = array(
'menu-item-db-id' => ( isset( $_item_object_data['menu-item-db-id'] ) ? $_item_object_data['menu-item-db-id'] : '' ),
'menu-item-object-id' => ( isset( $_item_object_data['menu-item-object-id'] ) ? $_item_object_data['menu-item-object-id'] : '' ),
'menu-item-object' => ( isset( $_item_object_data['menu-item-object'] ) ? $_item_object_data['menu-item-object'] : '' ),
'menu-item-parent-id' => ( isset( $_item_object_data['menu-item-parent-id'] ) ? $_item_object_data['menu-item-parent-id'] : '' ),
'menu-item-position' => ( isset( $_item_object_data['menu-item-position'] ) ? $_item_object_data['menu-item-position'] : '' ),
'menu-item-type' => ( isset( $_item_object_data['menu-item-type'] ) ? $_item_object_data['menu-item-type'] : '' ),
'menu-item-title' => ( isset( $_item_object_data['menu-item-title'] ) ? $_item_object_data['menu-item-title'] : '' ),
'menu-item-url' => ( isset( $_item_object_data['menu-item-url'] ) ? $_item_object_data['menu-item-url'] : '' ),
'menu-item-description' => ( isset( $_item_object_data['menu-item-description'] ) ? $_item_object_data['menu-item-description'] : '' ),
'menu-item-attr-title' => ( isset( $_item_object_data['menu-item-attr-title'] ) ? $_item_object_data['menu-item-attr-title'] : '' ),
'menu-item-target' => ( isset( $_item_object_data['menu-item-target'] ) ? $_item_object_data['menu-item-target'] : '' ),
'menu-item-classes' => ( isset( $_item_object_data['menu-item-classes'] ) ? $_item_object_data['menu-item-classes'] : '' ),
'menu-item-xfn' => ( isset( $_item_object_data['menu-item-xfn'] ) ? $_item_object_data['menu-item-xfn'] : '' ),
);
$items_saved[] = wp_update_nav_menu_item( $menu_id, $_actual_db_id, $args );
}
}
return $items_saved;
}
/**
* Adds custom arguments to some of the meta box object types.
*
* @since 3.0.0
*
* @access private
*
* @param object $data_object The post type or taxonomy meta-object.
* @return object The post type or taxonomy object.
*/
function _wp_nav_menu_meta_box_object( $data_object = null ) {
if ( isset( $data_object->name ) ) {
if ( 'page' === $data_object->name ) {
$data_object->_default_query = array(
'orderby' => 'menu_order title',
'post_status' => 'publish',
);
// Posts should show only published items.
} elseif ( 'post' === $data_object->name ) {
$data_object->_default_query = array(
'post_status' => 'publish',
);
// Categories should be in reverse chronological order.
} elseif ( 'category' === $data_object->name ) {
$data_object->_default_query = array(
'orderby' => 'id',
'order' => 'DESC',
);
// Custom post types should show only published items.
} else {
$data_object->_default_query = array(
'post_status' => 'publish',
);
}
}
return $data_object;
}
/**
* Returns the menu formatted to edit.
*
* @since 3.0.0
*
* @param int $menu_id Optional. The ID of the menu to format. Default 0.
* @return string|WP_Error The menu formatted to edit or error object on failure.
*/
function wp_get_nav_menu_to_edit( $menu_id = 0 ) {
$menu = wp_get_nav_menu_object( $menu_id );
// If the menu exists, get its items.
if ( is_nav_menu( $menu ) ) {
$menu_items = wp_get_nav_menu_items( $menu->term_id, array( 'post_status' => 'any' ) );
$result = '<div id="menu-instructions" class="post-body-plain';
$result .= ( ! empty( $menu_items ) ) ? ' menu-instructions-inactive">' : '">';
$result .= '<p>' . __( 'Add menu items from the column on the left.' ) . '</p>';
$result .= '</div>';
if ( empty( $menu_items ) ) {
return $result . ' <ul class="menu" id="menu-to-edit"> </ul>';
}
/**
* Filters the Walker class used when adding nav menu items.
*
* @since 3.0.0
*
* @param string $class The walker class to use. Default 'Walker_Nav_Menu_Edit'.
* @param int $menu_id ID of the menu being rendered.
*/
$walker_class_name = apply_filters( 'wp_edit_nav_menu_walker', 'Walker_Nav_Menu_Edit', $menu_id );
if ( class_exists( $walker_class_name ) ) {
$walker = new $walker_class_name();
} else {
return new WP_Error(
'menu_walker_not_exist',
sprintf(
/* translators: %s: Walker class name. */
__( 'The Walker class named %s does not exist.' ),
'<strong>' . $walker_class_name . '</strong>'
)
);
}
$some_pending_menu_items = false;
$some_invalid_menu_items = false;
foreach ( (array) $menu_items as $menu_item ) {
if ( isset( $menu_item->post_status ) && 'draft' === $menu_item->post_status ) {
$some_pending_menu_items = true;
}
if ( ! empty( $menu_item->_invalid ) ) {
$some_invalid_menu_items = true;
}
}
if ( $some_pending_menu_items ) {
$message = __( 'Click Save Menu to make pending menu items public.' );
$notice_args = array(
'type' => 'info',
'additional_classes' => array( 'notice-alt', 'inline' ),
);
$result .= wp_get_admin_notice( $message, $notice_args );
}
if ( $some_invalid_menu_items ) {
$message = __( 'There are some invalid menu items. Please check or delete them.' );
$notice_args = array(
'type' => 'error',
'additional_classes' => array( 'notice-alt', 'inline' ),
);
$result .= wp_get_admin_notice( $message, $notice_args );
}
$result .= '<ul class="menu" id="menu-to-edit"> ';
$result .= walk_nav_menu_tree(
array_map( 'wp_setup_nav_menu_item', $menu_items ),
0,
(object) array( 'walker' => $walker )
);
$result .= ' </ul> ';
return $result;
} elseif ( is_wp_error( $menu ) ) {
return $menu;
}
}
/**
* Returns the columns for the nav menus page.
*
* @since 3.0.0
*
* @return string[] Array of column titles keyed by their column name.
*/
function wp_nav_menu_manage_columns() {
return array(
'_title' => __( 'Show advanced menu properties' ),
'cb' => '<input type="checkbox" />',
'link-target' => __( 'Link Target' ),
'title-attribute' => __( 'Title Attribute' ),
'css-classes' => __( 'CSS Classes' ),
'xfn' => __( 'Link Relationship (XFN)' ),
'description' => __( 'Description' ),
);
}
/**
* Deletes orphaned draft menu items
*
* @access private
* @since 3.0.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*/
function _wp_delete_orphaned_draft_menu_items() {
global $wpdb;
$delete_timestamp = time() - ( DAY_IN_SECONDS * EMPTY_TRASH_DAYS );
// Delete orphaned draft menu items.
$menu_items_to_delete = $wpdb->get_col(
$wpdb->prepare(
"SELECT ID FROM $wpdb->posts AS p
LEFT JOIN $wpdb->postmeta AS m ON p.ID = m.post_id
WHERE post_type = 'nav_menu_item' AND post_status = 'draft'
AND meta_key = '_menu_item_orphaned' AND meta_value < %d",
$delete_timestamp
)
);
foreach ( (array) $menu_items_to_delete as $menu_item_id ) {
wp_delete_post( $menu_item_id, true );
}
}
/**
* Saves nav menu items.
*
* @since 3.6.0
*
* @param int|string $nav_menu_selected_id ID, slug, or name of the currently-selected menu.
* @param string $nav_menu_selected_title Title of the currently-selected menu.
* @return string[] The menu updated messages.
*/
function wp_nav_menu_update_menu_items( $nav_menu_selected_id, $nav_menu_selected_title ) {
$unsorted_menu_items = wp_get_nav_menu_items(
$nav_menu_selected_id,
array(
'orderby' => 'ID',
'output' => ARRAY_A,
'output_key' => 'ID',
'post_status' => 'draft,publish',
)
);
$messages = array();
$menu_items = array();
// Index menu items by DB ID.
foreach ( $unsorted_menu_items as $_item ) {
$menu_items[ $_item->db_id ] = $_item;
}
$post_fields = array(
'menu-item-db-id',
'menu-item-object-id',
'menu-item-object',
'menu-item-parent-id',
'menu-item-position',
'menu-item-type',
'menu-item-title',
'menu-item-url',
'menu-item-description',
'menu-item-attr-title',
'menu-item-target',
'menu-item-classes',
'menu-item-xfn',
);
wp_defer_term_counting( true );
// Loop through all the menu items' POST variables.
if ( ! empty( $_POST['menu-item-db-id'] ) ) {
foreach ( (array) $_POST['menu-item-db-id'] as $_key => $k ) {
// Menu item title can't be blank.
if ( ! isset( $_POST['menu-item-title'][ $_key ] ) || '' === $_POST['menu-item-title'][ $_key ] ) {
continue;
}
$args = array();
foreach ( $post_fields as $field ) {
$args[ $field ] = isset( $_POST[ $field ][ $_key ] ) ? $_POST[ $field ][ $_key ] : '';
}
$menu_item_db_id = wp_update_nav_menu_item(
$nav_menu_selected_id,
( (int) $_POST['menu-item-db-id'][ $_key ] !== $_key ? 0 : $_key ),
$args
);
if ( is_wp_error( $menu_item_db_id ) ) {
$messages[] = wp_get_admin_notice(
$menu_item_db_id->get_error_message(),
array(
'id' => 'message',
'additional_classes' => array( 'error' ),
)
);
} else {
unset( $menu_items[ $menu_item_db_id ] );
}
}
}
// Remove menu items from the menu that weren't in $_POST.
if ( ! empty( $menu_items ) ) {
foreach ( array_keys( $menu_items ) as $menu_item_id ) {
if ( is_nav_menu_item( $menu_item_id ) ) {
wp_delete_post( $menu_item_id );
}
}
}
// Store 'auto-add' pages.
$auto_add = ! empty( $_POST['auto-add-pages'] );
$nav_menu_option = (array) get_option( 'nav_menu_options' );
if ( ! isset( $nav_menu_option['auto_add'] ) ) {
$nav_menu_option['auto_add'] = array();
}
if ( $auto_add ) {
if ( ! in_array( $nav_menu_selected_id, $nav_menu_option['auto_add'], true ) ) {
$nav_menu_option['auto_add'][] = $nav_menu_selected_id;
}
} else {
$key = array_search( $nav_menu_selected_id, $nav_menu_option['auto_add'], true );
if ( false !== $key ) {
unset( $nav_menu_option['auto_add'][ $key ] );
}
}
// Remove non-existent/deleted menus.
$nav_menu_option['auto_add'] = array_intersect(
$nav_menu_option['auto_add'],
wp_get_nav_menus( array( 'fields' => 'ids' ) )
);
update_option( 'nav_menu_options', $nav_menu_option, false );
wp_defer_term_counting( false );
/** This action is documented in wp-includes/nav-menu.php */
do_action( 'wp_update_nav_menu', $nav_menu_selected_id );
/* translators: %s: Nav menu title. */
$message = sprintf( __( '%s has been updated.' ), '<strong>' . $nav_menu_selected_title . '</strong>' );
$notice_args = array(
'id' => 'message',
'dismissible' => true,
'additional_classes' => array( 'updated' ),
);
$messages[] = wp_get_admin_notice( $message, $notice_args );
unset( $menu_items, $unsorted_menu_items );
return $messages;
}
/**
* If a JSON blob of navigation menu data is in POST data, expand it and inject
* it into `$_POST` to avoid PHP `max_input_vars` limitations. See #14134.
*
* @ignore
* @since 4.5.3
* @access private
*/
function _wp_expand_nav_menu_post_data() {
if ( ! isset( $_POST['nav-menu-data'] ) ) {
return;
}
$data = json_decode( stripslashes( $_POST['nav-menu-data'] ) );
if ( ! is_null( $data ) && $data ) {
foreach ( $data as $post_input_data ) {
/*
* For input names that are arrays (e.g. `menu-item-db-id[3][4][5]`),
* derive the array path keys via regex and set the value in $_POST.
*/
preg_match( '#([^\[]*)(\[(.+)\])?#', $post_input_data->name, $matches );
$array_bits = array( $matches[1] );
if ( isset( $matches[3] ) ) {
$array_bits = array_merge( $array_bits, explode( '][', $matches[3] ) );
}
$new_post_data = array();
// Build the new array value from leaf to trunk.
for ( $i = count( $array_bits ) - 1; $i >= 0; $i-- ) {
if ( count( $array_bits ) - 1 === $i ) {
$new_post_data[ $array_bits[ $i ] ] = wp_slash( $post_input_data->value );
} else {
$new_post_data = array( $array_bits[ $i ] => $new_post_data );
}
}
$_POST = array_replace_recursive( $_POST, $new_post_data );
}
}
}
/**
* @output wp-admin/js/customize-widgets.js
*/
/* global _wpCustomizeWidgetsSettings */
(function( wp, $ ){
if ( ! wp || ! wp.customize ) { return; }
// Set up our namespace...
var api = wp.customize,
l10n;
/**
* @namespace wp.customize.Widgets
*/
api.Widgets = api.Widgets || {};
api.Widgets.savedWidgetIds = {};
// Link settings.
api.Widgets.data = _wpCustomizeWidgetsSettings || {};
l10n = api.Widgets.data.l10n;
/**
* wp.customize.Widgets.WidgetModel
*
* A single widget model.
*
* @class wp.customize.Widgets.WidgetModel
* @augments Backbone.Model
*/
api.Widgets.WidgetModel = Backbone.Model.extend(/** @lends wp.customize.Widgets.WidgetModel.prototype */{
id: null,
temp_id: null,
classname: null,
control_tpl: null,
description: null,
is_disabled: null,
is_multi: null,
multi_number: null,
name: null,
id_base: null,
transport: null,
params: [],
width: null,
height: null,
search_matched: true
});
/**
* wp.customize.Widgets.WidgetCollection
*
* Collection for widget models.
*
* @class wp.customize.Widgets.WidgetCollection
* @augments Backbone.Collection
*/
api.Widgets.WidgetCollection = Backbone.Collection.extend(/** @lends wp.customize.Widgets.WidgetCollection.prototype */{
model: api.Widgets.WidgetModel,
// Controls searching on the current widget collection
// and triggers an update event.
doSearch: function( value ) {
// Don't do anything if we've already done this search.
// Useful because the search handler fires multiple times per keystroke.
if ( this.terms === value ) {
return;
}
// Updates terms with the value passed.
this.terms = value;
// If we have terms, run a search...
if ( this.terms.length > 0 ) {
this.search( this.terms );
}
// If search is blank, set all the widgets as they matched the search to reset the views.
if ( this.terms === '' ) {
this.each( function ( widget ) {
widget.set( 'search_matched', true );
} );
}
},
// Performs a search within the collection.
// @uses RegExp
search: function( term ) {
var match, haystack;
// Escape the term string for RegExp meta characters.
term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
// Consider spaces as word delimiters and match the whole string
// so matching terms can be combined.
term = term.replace( / /g, ')(?=.*' );
match = new RegExp( '^(?=.*' + term + ').+', 'i' );
this.each( function ( data ) {
haystack = [ data.get( 'name' ), data.get( 'description' ) ].join( ' ' );
data.set( 'search_matched', match.test( haystack ) );
} );
}
});
api.Widgets.availableWidgets = new api.Widgets.WidgetCollection( api.Widgets.data.availableWidgets );
/**
* wp.customize.Widgets.SidebarModel
*
* A single sidebar model.
*
* @class wp.customize.Widgets.SidebarModel
* @augments Backbone.Model
*/
api.Widgets.SidebarModel = Backbone.Model.extend(/** @lends wp.customize.Widgets.SidebarModel.prototype */{
after_title: null,
after_widget: null,
before_title: null,
before_widget: null,
'class': null,
description: null,
id: null,
name: null,
is_rendered: false
});
/**
* wp.customize.Widgets.SidebarCollection
*
* Collection for sidebar models.
*
* @class wp.customize.Widgets.SidebarCollection
* @augments Backbone.Collection
*/
api.Widgets.SidebarCollection = Backbone.Collection.extend(/** @lends wp.customize.Widgets.SidebarCollection.prototype */{
model: api.Widgets.SidebarModel
});
api.Widgets.registeredSidebars = new api.Widgets.SidebarCollection( api.Widgets.data.registeredSidebars );
api.Widgets.AvailableWidgetsPanelView = wp.Backbone.View.extend(/** @lends wp.customize.Widgets.AvailableWidgetsPanelView.prototype */{
el: '#available-widgets',
events: {
'input #widgets-search': 'search',
'focus .widget-tpl' : 'focus',
'click .widget-tpl' : '_submit',
'keypress .widget-tpl' : '_submit',
'keydown' : 'keyboardAccessible'
},
// Cache current selected widget.
selected: null,
// Cache sidebar control which has opened panel.
currentSidebarControl: null,
$search: null,
$clearResults: null,
searchMatchesCount: null,
/**
* View class for the available widgets panel.
*
* @constructs wp.customize.Widgets.AvailableWidgetsPanelView
* @augments wp.Backbone.View
*/
initialize: function() {
var self = this;
this.$search = $( '#widgets-search' );
this.$clearResults = this.$el.find( '.clear-results' );
_.bindAll( this, 'close' );
this.listenTo( this.collection, 'change', this.updateList );
this.updateList();
// Set the initial search count to the number of available widgets.
this.searchMatchesCount = this.collection.length;
/*
* If the available widgets panel is open and the customize controls
* are interacted with (i.e. available widgets panel is blurred) then
* close the available widgets panel. Also close on back button click.
*/
$( '#customize-controls, #available-widgets .customize-section-title' ).on( 'click keydown', function( e ) {
var isAddNewBtn = $( e.target ).is( '.add-new-widget, .add-new-widget *' );
if ( $( 'body' ).hasClass( 'adding-widget' ) && ! isAddNewBtn ) {
self.close();
}
} );
// Clear the search results and trigger an `input` event to fire a new search.
this.$clearResults.on( 'click', function() {
self.$search.val( '' ).trigger( 'focus' ).trigger( 'input' );
} );
// Close the panel if the URL in the preview changes.
api.previewer.bind( 'url', this.close );
},
/**
* Performs a search and handles selected widget.
*/
search: _.debounce( function( event ) {
var firstVisible;
this.collection.doSearch( event.target.value );
// Update the search matches count.
this.updateSearchMatchesCount();
// Announce how many search results.
this.announceSearchMatches();
// Remove a widget from being selected if it is no longer visible.
if ( this.selected && ! this.selected.is( ':visible' ) ) {
this.selected.removeClass( 'selected' );
this.selected = null;
}
// If a widget was selected but the filter value has been cleared out, clear selection.
if ( this.selected && ! event.target.value ) {
this.selected.removeClass( 'selected' );
this.selected = null;
}
// If a filter has been entered and a widget hasn't been selected, select the first one shown.
if ( ! this.selected && event.target.value ) {
firstVisible = this.$el.find( '> .widget-tpl:visible:first' );
if ( firstVisible.length ) {
this.select( firstVisible );
}
}
// Toggle the clear search results button.
if ( '' !== event.target.value ) {
this.$clearResults.addClass( 'is-visible' );
} else if ( '' === event.target.value ) {
this.$clearResults.removeClass( 'is-visible' );
}
// Set a CSS class on the search container when there are no search results.
if ( ! this.searchMatchesCount ) {
this.$el.addClass( 'no-widgets-found' );
} else {
this.$el.removeClass( 'no-widgets-found' );
}
}, 500 ),
/**
* Updates the count of the available widgets that have the `search_matched` attribute.
*/
updateSearchMatchesCount: function() {
this.searchMatchesCount = this.collection.where({ search_matched: true }).length;
},
/**
* Sends a message to the aria-live region to announce how many search results.
*/
announceSearchMatches: function() {
var message = l10n.widgetsFound.replace( '%d', this.searchMatchesCount ) ;
if ( ! this.searchMatchesCount ) {
message = l10n.noWidgetsFound;
}
wp.a11y.speak( message );
},
/**
* Changes visibility of available widgets.
*/
updateList: function() {
this.collection.each( function( widget ) {
var widgetTpl = $( '#widget-tpl-' + widget.id );
widgetTpl.toggle( widget.get( 'search_matched' ) && ! widget.get( 'is_disabled' ) );
if ( widget.get( 'is_disabled' ) && widgetTpl.is( this.selected ) ) {
this.selected = null;
}
} );
},
/**
* Highlights a widget.
*/
select: function( widgetTpl ) {
this.selected = $( widgetTpl );
this.selected.siblings( '.widget-tpl' ).removeClass( 'selected' );
this.selected.addClass( 'selected' );
},
/**
* Highlights a widget on focus.
*/
focus: function( event ) {
this.select( $( event.currentTarget ) );
},
/**
* Handles submit for keypress and click on widget.
*/
_submit: function( event ) {
// Only proceed with keypress if it is Enter or Spacebar.
if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
return;
}
this.submit( $( event.currentTarget ) );
},
/**
* Adds a selected widget to the sidebar.
*/
submit: function( widgetTpl ) {
var widgetId, widget, widgetFormControl;
if ( ! widgetTpl ) {
widgetTpl = this.selected;
}
if ( ! widgetTpl || ! this.currentSidebarControl ) {
return;
}
this.select( widgetTpl );
widgetId = $( this.selected ).data( 'widget-id' );
widget = this.collection.findWhere( { id: widgetId } );
if ( ! widget ) {
return;
}
widgetFormControl = this.currentSidebarControl.addWidget( widget.get( 'id_base' ) );
if ( widgetFormControl ) {
widgetFormControl.focus();
}
this.close();
},
/**
* Opens the panel.
*/
open: function( sidebarControl ) {
this.currentSidebarControl = sidebarControl;
// Wide widget controls appear over the preview, and so they need to be collapsed when the panel opens.
_( this.currentSidebarControl.getWidgetFormControls() ).each( function( control ) {
if ( control.params.is_wide ) {
control.collapseForm();
}
} );
if ( api.section.has( 'publish_settings' ) ) {
api.section( 'publish_settings' ).collapse();
}
$( 'body' ).addClass( 'adding-widget' );
this.$el.find( '.selected' ).removeClass( 'selected' );
// Reset search.
this.collection.doSearch( '' );
if ( ! api.settings.browser.mobile ) {
this.$search.trigger( 'focus' );
}
},
/**
* Closes the panel.
*/
close: function( options ) {
options = options || {};
if ( options.returnFocus && this.currentSidebarControl ) {
this.currentSidebarControl.container.find( '.add-new-widget' ).focus();
}
this.currentSidebarControl = null;
this.selected = null;
$( 'body' ).removeClass( 'adding-widget' );
this.$search.val( '' ).trigger( 'input' );
},
/**
* Adds keyboard accessibility to the panel.
*/
keyboardAccessible: function( event ) {
var isEnter = ( event.which === 13 ),
isEsc = ( event.which === 27 ),
isDown = ( event.which === 40 ),
isUp = ( event.which === 38 ),
isTab = ( event.which === 9 ),
isShift = ( event.shiftKey ),
selected = null,
firstVisible = this.$el.find( '> .widget-tpl:visible:first' ),
lastVisible = this.$el.find( '> .widget-tpl:visible:last' ),
isSearchFocused = $( event.target ).is( this.$search ),
isLastWidgetFocused = $( event.target ).is( '.widget-tpl:visible:last' );
if ( isDown || isUp ) {
if ( isDown ) {
if ( isSearchFocused ) {
selected = firstVisible;
} else if ( this.selected && this.selected.nextAll( '.widget-tpl:visible' ).length !== 0 ) {
selected = this.selected.nextAll( '.widget-tpl:visible:first' );
}
} else if ( isUp ) {
if ( isSearchFocused ) {
selected = lastVisible;
} else if ( this.selected && this.selected.prevAll( '.widget-tpl:visible' ).length !== 0 ) {
selected = this.selected.prevAll( '.widget-tpl:visible:first' );
}
}
this.select( selected );
if ( selected ) {
selected.trigger( 'focus' );
} else {
this.$search.trigger( 'focus' );
}
return;
}
// If enter pressed but nothing entered, don't do anything.
if ( isEnter && ! this.$search.val() ) {
return;
}
if ( isEnter ) {
this.submit();
} else if ( isEsc ) {
this.close( { returnFocus: true } );
}
if ( this.currentSidebarControl && isTab && ( isShift && isSearchFocused || ! isShift && isLastWidgetFocused ) ) {
this.currentSidebarControl.container.find( '.add-new-widget' ).focus();
event.preventDefault();
}
}
});
/**
* Handlers for the widget-synced event, organized by widget ID base.
* Other widgets may provide their own update handlers by adding
* listeners for the widget-synced event.
*
* @alias wp.customize.Widgets.formSyncHandlers
*/
api.Widgets.formSyncHandlers = {
/**
* @param {jQuery.Event} e
* @param {jQuery} widget
* @param {string} newForm
*/
rss: function( e, widget, newForm ) {
var oldWidgetError = widget.find( '.widget-error:first' ),
newWidgetError = $( '<div>' + newForm + '</div>' ).find( '.widget-error:first' );
if ( oldWidgetError.length && newWidgetError.length ) {
oldWidgetError.replaceWith( newWidgetError );
} else if ( oldWidgetError.length ) {
oldWidgetError.remove();
} else if ( newWidgetError.length ) {
widget.find( '.widget-content:first' ).prepend( newWidgetError );
}
}
};
api.Widgets.WidgetControl = api.Control.extend(/** @lends wp.customize.Widgets.WidgetControl.prototype */{
defaultExpandedArguments: {
duration: 'fast',
completeCallback: $.noop
},
/**
* wp.customize.Widgets.WidgetControl
*
* Customizer control for widgets.
* Note that 'widget_form' must match the WP_Widget_Form_Customize_Control::$type
*
* @since 4.1.0
*
* @constructs wp.customize.Widgets.WidgetControl
* @augments wp.customize.Control
*/
initialize: function( id, options ) {
var control = this;
control.widgetControlEmbedded = false;
control.widgetContentEmbedded = false;
control.expanded = new api.Value( false );
control.expandedArgumentsQueue = [];
control.expanded.bind( function( expanded ) {
var args = control.expandedArgumentsQueue.shift();
args = $.extend( {}, control.defaultExpandedArguments, args );
control.onChangeExpanded( expanded, args );
});
control.altNotice = true;
api.Control.prototype.initialize.call( control, id, options );
},
/**
* Set up the control.
*
* @since 3.9.0
*/
ready: function() {
var control = this;
/*
* Embed a placeholder once the section is expanded. The full widget
* form content will be embedded once the control itself is expanded,
* and at this point the widget-added event will be triggered.
*/
if ( ! control.section() ) {
control.embedWidgetControl();
} else {
api.section( control.section(), function( section ) {
var onExpanded = function( isExpanded ) {
if ( isExpanded ) {
control.embedWidgetControl();
section.expanded.unbind( onExpanded );
}
};
if ( section.expanded() ) {
onExpanded( true );
} else {
section.expanded.bind( onExpanded );
}
} );
}
},
/**
* Embed the .widget element inside the li container.
*
* @since 4.4.0
*/
embedWidgetControl: function() {
var control = this, widgetControl;
if ( control.widgetControlEmbedded ) {
return;
}
control.widgetControlEmbedded = true;
widgetControl = $( control.params.widget_control );
control.container.append( widgetControl );
control._setupModel();
control._setupWideWidget();
control._setupControlToggle();
control._setupWidgetTitle();
control._setupReorderUI();
control._setupHighlightEffects();
control._setupUpdateUI();
control._setupRemoveUI();
},
/**
* Embed the actual widget form inside of .widget-content and finally trigger the widget-added event.
*
* @since 4.4.0
*/
embedWidgetContent: function() {
var control = this, widgetContent;
control.embedWidgetControl();
if ( control.widgetContentEmbedded ) {
return;
}
control.widgetContentEmbedded = true;
// Update the notification container element now that the widget content has been embedded.
control.notifications.container = control.getNotificationsContainerElement();
control.notifications.render();
widgetContent = $( control.params.widget_content );
control.container.find( '.widget-content:first' ).append( widgetContent );
/*
* Trigger widget-added event so that plugins can attach any event
* listeners and dynamic UI elements.
*/
$( document ).trigger( 'widget-added', [ control.container.find( '.widget:first' ) ] );
},
/**
* Handle changes to the setting
*/
_setupModel: function() {
var self = this, rememberSavedWidgetId;
// Remember saved widgets so we know which to trash (move to inactive widgets sidebar).
rememberSavedWidgetId = function() {
api.Widgets.savedWidgetIds[self.params.widget_id] = true;
};
api.bind( 'ready', rememberSavedWidgetId );
api.bind( 'saved', rememberSavedWidgetId );
this._updateCount = 0;
this.isWidgetUpdating = false;
this.liveUpdateMode = true;
// Update widget whenever model changes.
this.setting.bind( function( to, from ) {
if ( ! _( from ).isEqual( to ) && ! self.isWidgetUpdating ) {
self.updateWidget( { instance: to } );
}
} );
},
/**
* Add special behaviors for wide widget controls
*/
_setupWideWidget: function() {
var self = this, $widgetInside, $widgetForm, $customizeSidebar,
$themeControlsContainer, positionWidget;
if ( ! this.params.is_wide || $( window ).width() <= 640 /* max-width breakpoint in customize-controls.css */ ) {
return;
}
$widgetInside = this.container.find( '.widget-inside' );
$widgetForm = $widgetInside.find( '> .form' );
$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
this.container.addClass( 'wide-widget-control' );
this.container.find( '.form:first' ).css( {
'max-width': this.params.width,
'min-height': this.params.height
} );
/**
* Keep the widget-inside positioned so the top of fixed-positioned
* element is at the same top position as the widget-top. When the
* widget-top is scrolled out of view, keep the widget-top in view;
* likewise, don't allow the widget to drop off the bottom of the window.
* If a widget is too tall to fit in the window, don't let the height
* exceed the window height so that the contents of the widget control
* will become scrollable (overflow:auto).
*/
positionWidget = function() {
var offsetTop = self.container.offset().top,
windowHeight = $( window ).height(),
formHeight = $widgetForm.outerHeight(),
top;
$widgetInside.css( 'max-height', windowHeight );
top = Math.max(
0, // Prevent top from going off screen.
Math.min(
Math.max( offsetTop, 0 ), // Distance widget in panel is from top of screen.
windowHeight - formHeight // Flush up against bottom of screen.
)
);
$widgetInside.css( 'top', top );
};
$themeControlsContainer = $( '#customize-theme-controls' );
this.container.on( 'expand', function() {
positionWidget();
$customizeSidebar.on( 'scroll', positionWidget );
$( window ).on( 'resize', positionWidget );
$themeControlsContainer.on( 'expanded collapsed', positionWidget );
} );
this.container.on( 'collapsed', function() {
$customizeSidebar.off( 'scroll', positionWidget );
$( window ).off( 'resize', positionWidget );
$themeControlsContainer.off( 'expanded collapsed', positionWidget );
} );
// Reposition whenever a sidebar's widgets are changed.
api.each( function( setting ) {
if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) {
setting.bind( function() {
if ( self.container.hasClass( 'expanded' ) ) {
positionWidget();
}
} );
}
} );
},
/**
* Show/hide the control when clicking on the form title, when clicking
* the close button
*/
_setupControlToggle: function() {
var self = this, $closeBtn;
this.container.find( '.widget-top' ).on( 'click', function( e ) {
e.preventDefault();
var sidebarWidgetsControl = self.getSidebarWidgetsControl();
if ( sidebarWidgetsControl.isReordering ) {
return;
}
self.expanded( ! self.expanded() );
} );
$closeBtn = this.container.find( '.widget-control-close' );
$closeBtn.on( 'click', function() {
self.collapse();
self.container.find( '.widget-top .widget-action:first' ).focus(); // Keyboard accessibility.
} );
},
/**
* Update the title of the form if a title field is entered
*/
_setupWidgetTitle: function() {
var self = this, updateTitle;
updateTitle = function() {
var title = self.setting().title,
inWidgetTitle = self.container.find( '.in-widget-title' );
if ( title ) {
inWidgetTitle.text( ': ' + title );
} else {
inWidgetTitle.text( '' );
}
};
this.setting.bind( updateTitle );
updateTitle();
},
/**
* Set up the widget-reorder-nav
*/
_setupReorderUI: function() {
var self = this, selectSidebarItem, $moveWidgetArea,
$reorderNav, updateAvailableSidebars, template;
/**
* select the provided sidebar list item in the move widget area
*
* @param {jQuery} li
*/
selectSidebarItem = function( li ) {
li.siblings( '.selected' ).removeClass( 'selected' );
li.addClass( 'selected' );
var isSelfSidebar = ( li.data( 'id' ) === self.params.sidebar_id );
self.container.find( '.move-widget-btn' ).prop( 'disabled', isSelfSidebar );
};
/**
* Add the widget reordering elements to the widget control
*/
this.container.find( '.widget-title-action' ).after( $( api.Widgets.data.tpl.widgetReorderNav ) );
template = _.template( api.Widgets.data.tpl.moveWidgetArea );
$moveWidgetArea = $( template( {
sidebars: _( api.Widgets.registeredSidebars.toArray() ).pluck( 'attributes' )
} )
);
this.container.find( '.widget-top' ).after( $moveWidgetArea );
/**
* Update available sidebars when their rendered state changes
*/
updateAvailableSidebars = function() {
var $sidebarItems = $moveWidgetArea.find( 'li' ), selfSidebarItem,
renderedSidebarCount = 0;
selfSidebarItem = $sidebarItems.filter( function(){
return $( this ).data( 'id' ) === self.params.sidebar_id;
} );
$sidebarItems.each( function() {
var li = $( this ),
sidebarId, sidebar, sidebarIsRendered;
sidebarId = li.data( 'id' );
sidebar = api.Widgets.registeredSidebars.get( sidebarId );
sidebarIsRendered = sidebar.get( 'is_rendered' );
li.toggle( sidebarIsRendered );
if ( sidebarIsRendered ) {
renderedSidebarCount += 1;
}
if ( li.hasClass( 'selected' ) && ! sidebarIsRendered ) {
selectSidebarItem( selfSidebarItem );
}
} );
if ( renderedSidebarCount > 1 ) {
self.container.find( '.move-widget' ).show();
} else {
self.container.find( '.move-widget' ).hide();
}
};
updateAvailableSidebars();
api.Widgets.registeredSidebars.on( 'change:is_rendered', updateAvailableSidebars );
/**
* Handle clicks for up/down/move on the reorder nav
*/
$reorderNav = this.container.find( '.widget-reorder-nav' );
$reorderNav.find( '.move-widget, .move-widget-down, .move-widget-up' ).each( function() {
$( this ).prepend( self.container.find( '.widget-title' ).text() + ': ' );
} ).on( 'click keypress', function( event ) {
if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
return;
}
$( this ).trigger( 'focus' );
if ( $( this ).is( '.move-widget' ) ) {
self.toggleWidgetMoveArea();
} else {
var isMoveDown = $( this ).is( '.move-widget-down' ),
isMoveUp = $( this ).is( '.move-widget-up' ),
i = self.getWidgetSidebarPosition();
if ( ( isMoveUp && i === 0 ) || ( isMoveDown && i === self.getSidebarWidgetsControl().setting().length - 1 ) ) {
return;
}
if ( isMoveUp ) {
self.moveUp();
wp.a11y.speak( l10n.widgetMovedUp );
} else {
self.moveDown();
wp.a11y.speak( l10n.widgetMovedDown );
}
$( this ).trigger( 'focus' ); // Re-focus after the container was moved.
}
} );
/**
* Handle selecting a sidebar to move to
*/
this.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function( event ) {
if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
return;
}
event.preventDefault();
selectSidebarItem( $( this ) );
} );
/**
* Move widget to another sidebar
*/
this.container.find( '.move-widget-btn' ).click( function() {
self.getSidebarWidgetsControl().toggleReordering( false );
var oldSidebarId = self.params.sidebar_id,
newSidebarId = self.container.find( '.widget-area-select li.selected' ).data( 'id' ),
oldSidebarWidgetsSetting, newSidebarWidgetsSetting,
oldSidebarWidgetIds, newSidebarWidgetIds, i;
oldSidebarWidgetsSetting = api( 'sidebars_widgets[' + oldSidebarId + ']' );
newSidebarWidgetsSetting = api( 'sidebars_widgets[' + newSidebarId + ']' );
oldSidebarWidgetIds = Array.prototype.slice.call( oldSidebarWidgetsSetting() );
newSidebarWidgetIds = Array.prototype.slice.call( newSidebarWidgetsSetting() );
i = self.getWidgetSidebarPosition();
oldSidebarWidgetIds.splice( i, 1 );
newSidebarWidgetIds.push( self.params.widget_id );
oldSidebarWidgetsSetting( oldSidebarWidgetIds );
newSidebarWidgetsSetting( newSidebarWidgetIds );
self.focus();
} );
},
/**
* Highlight widgets in preview when interacted with in the Customizer
*/
_setupHighlightEffects: function() {
var self = this;
// Highlight whenever hovering or clicking over the form.
this.container.on( 'mouseenter click', function() {
self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
} );
// Highlight when the setting is updated.
this.setting.bind( function() {
self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
} );
},
/**
* Set up event handlers for widget updating
*/
_setupUpdateUI: function() {
var self = this, $widgetRoot, $widgetContent,
$saveBtn, updateWidgetDebounced, formSyncHandler;
$widgetRoot = this.container.find( '.widget:first' );
$widgetContent = $widgetRoot.find( '.widget-content:first' );
// Configure update button.
$saveBtn = this.container.find( '.widget-control-save' );
$saveBtn.val( l10n.saveBtnLabel );
$saveBtn.attr( 'title', l10n.saveBtnTooltip );
$saveBtn.removeClass( 'button-primary' );
$saveBtn.on( 'click', function( e ) {
e.preventDefault();
self.updateWidget( { disable_form: true } ); // @todo disable_form is unused?
} );
updateWidgetDebounced = _.debounce( function() {
self.updateWidget();
}, 250 );
// Trigger widget form update when hitting Enter within an input.
$widgetContent.on( 'keydown', 'input', function( e ) {
if ( 13 === e.which ) { // Enter.
e.preventDefault();
self.updateWidget( { ignoreActiveElement: true } );
}
} );
// Handle widgets that support live previews.
$widgetContent.on( 'change input propertychange', ':input', function( e ) {
if ( ! self.liveUpdateMode ) {
return;
}
if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) {
updateWidgetDebounced();
}
} );
// Remove loading indicators when the setting is saved and the preview updates.
this.setting.previewer.channel.bind( 'synced', function() {
self.container.removeClass( 'previewer-loading' );
} );
api.previewer.bind( 'widget-updated', function( updatedWidgetId ) {
if ( updatedWidgetId === self.params.widget_id ) {
self.container.removeClass( 'previewer-loading' );
}
} );
formSyncHandler = api.Widgets.formSyncHandlers[ this.params.widget_id_base ];
if ( formSyncHandler ) {
$( document ).on( 'widget-synced', function( e, widget ) {
if ( $widgetRoot.is( widget ) ) {
formSyncHandler.apply( document, arguments );
}
} );
}
},
/**
* Update widget control to indicate whether it is currently rendered.
*
* Overrides api.Control.toggle()
*
* @since 4.1.0
*
* @param {boolean} active
* @param {Object} args
* @param {function} args.completeCallback
*/
onChangeActive: function ( active, args ) {
// Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments.
this.container.toggleClass( 'widget-rendered', active );
if ( args.completeCallback ) {
args.completeCallback();
}
},
/**
* Set up event handlers for widget removal
*/
_setupRemoveUI: function() {
var self = this, $removeBtn, replaceDeleteWithRemove;
// Configure remove button.
$removeBtn = this.container.find( '.widget-control-remove' );
$removeBtn.on( 'click', function() {
// Find an adjacent element to add focus to when this widget goes away.
var $adjacentFocusTarget;
if ( self.container.next().is( '.customize-control-widget_form' ) ) {
$adjacentFocusTarget = self.container.next().find( '.widget-action:first' );
} else if ( self.container.prev().is( '.customize-control-widget_form' ) ) {
$adjacentFocusTarget = self.container.prev().find( '.widget-action:first' );
} else {
$adjacentFocusTarget = self.container.next( '.customize-control-sidebar_widgets' ).find( '.add-new-widget:first' );
}
self.container.slideUp( function() {
var sidebarsWidgetsControl = api.Widgets.getSidebarWidgetControlContainingWidget( self.params.widget_id ),
sidebarWidgetIds, i;
if ( ! sidebarsWidgetsControl ) {
return;
}
sidebarWidgetIds = sidebarsWidgetsControl.setting().slice();
i = _.indexOf( sidebarWidgetIds, self.params.widget_id );
if ( -1 === i ) {
return;
}
sidebarWidgetIds.splice( i, 1 );
sidebarsWidgetsControl.setting( sidebarWidgetIds );
$adjacentFocusTarget.focus(); // Keyboard accessibility.
} );
} );
replaceDeleteWithRemove = function() {
$removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the button as "Delete".
$removeBtn.attr( 'title', l10n.removeBtnTooltip );
};
if ( this.params.is_new ) {
api.bind( 'saved', replaceDeleteWithRemove );
} else {
replaceDeleteWithRemove();
}
},
/**
* Find all inputs in a widget container that should be considered when
* comparing the loaded form with the sanitized form, whose fields will
* be aligned to copy the sanitized over. The elements returned by this
* are passed into this._getInputsSignature(), and they are iterated
* over when copying sanitized values over to the form loaded.
*
* @param {jQuery} container element in which to look for inputs
* @return {jQuery} inputs
* @private
*/
_getInputs: function( container ) {
return $( container ).find( ':input[name]' );
},
/**
* Iterate over supplied inputs and create a signature string for all of them together.
* This string can be used to compare whether or not the form has all of the same fields.
*
* @param {jQuery} inputs
* @return {string}
* @private
*/
_getInputsSignature: function( inputs ) {
var inputsSignatures = _( inputs ).map( function( input ) {
var $input = $( input ), signatureParts;
if ( $input.is( ':checkbox, :radio' ) ) {
signatureParts = [ $input.attr( 'id' ), $input.attr( 'name' ), $input.prop( 'value' ) ];
} else {
signatureParts = [ $input.attr( 'id' ), $input.attr( 'name' ) ];
}
return signatureParts.join( ',' );
} );
return inputsSignatures.join( ';' );
},
/**
* Get the state for an input depending on its type.
*
* @param {jQuery|Element} input
* @return {string|boolean|Array|*}
* @private
*/
_getInputState: function( input ) {
input = $( input );
if ( input.is( ':radio, :checkbox' ) ) {
return input.prop( 'checked' );
} else if ( input.is( 'select[multiple]' ) ) {
return input.find( 'option:selected' ).map( function () {
return $( this ).val();
} ).get();
} else {
return input.val();
}
},
/**
* Update an input's state based on its type.
*
* @param {jQuery|Element} input
* @param {string|boolean|Array|*} state
* @private
*/
_setInputState: function ( input, state ) {
input = $( input );
if ( input.is( ':radio, :checkbox' ) ) {
input.prop( 'checked', state );
} else if ( input.is( 'select[multiple]' ) ) {
if ( ! Array.isArray( state ) ) {
state = [];
} else {
// Make sure all state items are strings since the DOM value is a string.
state = _.map( state, function ( value ) {
return String( value );
} );
}
input.find( 'option' ).each( function () {
$( this ).prop( 'selected', -1 !== _.indexOf( state, String( this.value ) ) );
} );
} else {
input.val( state );
}
},
/***********************************************************************
* Begin public API methods
**********************************************************************/
/**
* @return {wp.customize.controlConstructor.sidebar_widgets[]}
*/
getSidebarWidgetsControl: function() {
var settingId, sidebarWidgetsControl;
settingId = 'sidebars_widgets[' + this.params.sidebar_id + ']';
sidebarWidgetsControl = api.control( settingId );
if ( ! sidebarWidgetsControl ) {
return;
}
return sidebarWidgetsControl;
},
/**
* Submit the widget form via Ajax and get back the updated instance,
* along with the new widget control form to render.
*
* @param {Object} [args]
* @param {Object|null} [args.instance=null] When the model changes, the instance is sent here; otherwise, the inputs from the form are used
* @param {Function|null} [args.complete=null] Function which is called when the request finishes. Context is bound to the control. First argument is any error. Following arguments are for success.
* @param {boolean} [args.ignoreActiveElement=false] Whether or not updating a field will be deferred if focus is still on the element.
*/
updateWidget: function( args ) {
var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent,
updateNumber, params, data, $inputs, processing, jqxhr, isChanged;
// The updateWidget logic requires that the form fields to be fully present.
self.embedWidgetContent();
args = $.extend( {
instance: null,
complete: null,
ignoreActiveElement: false
}, args );
instanceOverride = args.instance;
completeCallback = args.complete;
this._updateCount += 1;
updateNumber = this._updateCount;
$widgetRoot = this.container.find( '.widget:first' );
$widgetContent = $widgetRoot.find( '.widget-content:first' );
// Remove a previous error message.
$widgetContent.find( '.widget-error' ).remove();
this.container.addClass( 'widget-form-loading' );
this.container.addClass( 'previewer-loading' );
processing = api.state( 'processing' );
processing( processing() + 1 );
if ( ! this.liveUpdateMode ) {
this.container.addClass( 'widget-form-disabled' );
}
params = {};
params.action = 'update-widget';
params.wp_customize = 'on';
params.nonce = api.settings.nonce['update-widget'];
params.customize_theme = api.settings.theme.stylesheet;
params.customized = wp.customize.previewer.query().customized;
data = $.param( params );
$inputs = this._getInputs( $widgetContent );
/*
* Store the value we're submitting in data so that when the response comes back,
* we know if it got sanitized; if there is no difference in the sanitized value,
* then we do not need to touch the UI and mess up the user's ongoing editing.
*/
$inputs.each( function() {
$( this ).data( 'state' + updateNumber, self._getInputState( this ) );
} );
if ( instanceOverride ) {
data += '&' + $.param( { 'sanitized_widget_setting': JSON.stringify( instanceOverride ) } );
} else {
data += '&' + $inputs.serialize();
}
data += '&' + $widgetContent.find( '~ :input' ).serialize();
if ( this._previousUpdateRequest ) {
this._previousUpdateRequest.abort();
}
jqxhr = $.post( wp.ajax.settings.url, data );
this._previousUpdateRequest = jqxhr;
jqxhr.done( function( r ) {
var message, sanitizedForm, $sanitizedInputs, hasSameInputsInResponse,
isLiveUpdateAborted = false;
// Check if the user is logged out.
if ( '0' === r ) {
api.previewer.preview.iframe.hide();
api.previewer.login().done( function() {
self.updateWidget( args );
api.previewer.preview.iframe.show();
} );
return;
}
// Check for cheaters.
if ( '-1' === r ) {
api.previewer.cheatin();
return;
}
if ( r.success ) {
sanitizedForm = $( '<div>' + r.data.form + '</div>' );
$sanitizedInputs = self._getInputs( sanitizedForm );
hasSameInputsInResponse = self._getInputsSignature( $inputs ) === self._getInputsSignature( $sanitizedInputs );
// Restore live update mode if sanitized fields are now aligned with the existing fields.
if ( hasSameInputsInResponse && ! self.liveUpdateMode ) {
self.liveUpdateMode = true;
self.container.removeClass( 'widget-form-disabled' );
self.container.find( 'input[name="savewidget"]' ).hide();
}
// Sync sanitized field states to existing fields if they are aligned.
if ( hasSameInputsInResponse && self.liveUpdateMode ) {
$inputs.each( function( i ) {
var $input = $( this ),
$sanitizedInput = $( $sanitizedInputs[i] ),
submittedState, sanitizedState, canUpdateState;
submittedState = $input.data( 'state' + updateNumber );
sanitizedState = self._getInputState( $sanitizedInput );
$input.data( 'sanitized', sanitizedState );
canUpdateState = ( ! _.isEqual( submittedState, sanitizedState ) && ( args.ignoreActiveElement || ! $input.is( document.activeElement ) ) );
if ( canUpdateState ) {
self._setInputState( $input, sanitizedState );
}
} );
$( document ).trigger( 'widget-synced', [ $widgetRoot, r.data.form ] );
// Otherwise, if sanitized fields are not aligned with existing fields, disable live update mode if enabled.
} else if ( self.liveUpdateMode ) {
self.liveUpdateMode = false;
self.container.find( 'input[name="savewidget"]' ).show();
isLiveUpdateAborted = true;
// Otherwise, replace existing form with the sanitized form.
} else {
$widgetContent.html( r.data.form );
self.container.removeClass( 'widget-form-disabled' );
$( document ).trigger( 'widget-updated', [ $widgetRoot ] );
}
/**
* If the old instance is identical to the new one, there is nothing new
* needing to be rendered, and so we can preempt the event for the
* preview finishing loading.
*/
isChanged = ! isLiveUpdateAborted && ! _( self.setting() ).isEqual( r.data.instance );
if ( isChanged ) {
self.isWidgetUpdating = true; // Suppress triggering another updateWidget.
self.setting( r.data.instance );
self.isWidgetUpdating = false;
} else {
// No change was made, so stop the spinner now instead of when the preview would updates.
self.container.removeClass( 'previewer-loading' );
}
if ( completeCallback ) {
completeCallback.call( self, null, { noChange: ! isChanged, ajaxFinished: true } );
}
} else {
// General error message.
message = l10n.error;
if ( r.data && r.data.message ) {
message = r.data.message;
}
if ( completeCallback ) {
completeCallback.call( self, message );
} else {
$widgetContent.prepend( '<p class="widget-error"><strong>' + message + '</strong></p>' );
}
}
} );
jqxhr.fail( function( jqXHR, textStatus ) {
if ( completeCallback ) {
completeCallback.call( self, textStatus );
}
} );
jqxhr.always( function() {
self.container.removeClass( 'widget-form-loading' );
$inputs.each( function() {
$( this ).removeData( 'state' + updateNumber );
} );
processing( processing() - 1 );
} );
},
/**
* Expand the accordion section containing a control
*/
expandControlSection: function() {
api.Control.prototype.expand.call( this );
},
/**
* @since 4.1.0
*
* @param {Boolean} expanded
* @param {Object} [params]
* @return {Boolean} False if state already applied.
*/
_toggleExpanded: api.Section.prototype._toggleExpanded,
/**
* @since 4.1.0
*
* @param {Object} [params]
* @return {Boolean} False if already expanded.
*/
expand: api.Section.prototype.expand,
/**
* Expand the widget form control
*
* @deprecated 4.1.0 Use this.expand() instead.
*/
expandForm: function() {
this.expand();
},
/**
* @since 4.1.0
*
* @param {Object} [params]
* @return {Boolean} False if already collapsed.
*/
collapse: api.Section.prototype.collapse,
/**
* Collapse the widget form control
*
* @deprecated 4.1.0 Use this.collapse() instead.
*/
collapseForm: function() {
this.collapse();
},
/**
* Expand or collapse the widget control
*
* @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide )
*
* @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility
*/
toggleForm: function( showOrHide ) {
if ( typeof showOrHide === 'undefined' ) {
showOrHide = ! this.expanded();
}
this.expanded( showOrHide );
},
/**
* Respond to change in the expanded state.
*
* @param {boolean} expanded
* @param {Object} args merged on top of this.defaultActiveArguments
*/
onChangeExpanded: function ( expanded, args ) {
var self = this, $widget, $inside, complete, prevComplete, expandControl, $toggleBtn;
self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI.
if ( expanded ) {
self.embedWidgetContent();
}
// If the expanded state is unchanged only manipulate container expanded states.
if ( args.unchanged ) {
if ( expanded ) {
api.Control.prototype.expand.call( self, {
completeCallback: args.completeCallback
});
}
return;
}
$widget = this.container.find( 'div.widget:first' );
$inside = $widget.find( '.widget-inside:first' );
$toggleBtn = this.container.find( '.widget-top button.widget-action' );
expandControl = function() {
// Close all other widget controls before expanding this one.
api.control.each( function( otherControl ) {
if ( self.params.type === otherControl.params.type && self !== otherControl ) {
otherControl.collapse();
}
} );
complete = function() {
self.container.removeClass( 'expanding' );
self.container.addClass( 'expanded' );
$widget.addClass( 'open' );
$toggleBtn.attr( 'aria-expanded', 'true' );
self.container.trigger( 'expanded' );
};
if ( args.completeCallback ) {
prevComplete = complete;
complete = function () {
prevComplete();
args.completeCallback();
};
}
if ( self.params.is_wide ) {
$inside.fadeIn( args.duration, complete );
} else {
$inside.slideDown( args.duration, complete );
}
self.container.trigger( 'expand' );
self.container.addClass( 'expanding' );
};
if ( $toggleBtn.attr( 'aria-expanded' ) === 'false' ) {
if ( api.section.has( self.section() ) ) {
api.section( self.section() ).expand( {
completeCallback: expandControl
} );
} else {
expandControl();
}
} else {
complete = function() {
self.container.removeClass( 'collapsing' );
self.container.removeClass( 'expanded' );
$widget.removeClass( 'open' );
$toggleBtn.attr( 'aria-expanded', 'false' );
self.container.trigger( 'collapsed' );
};
if ( args.completeCallback ) {
prevComplete = complete;
complete = function () {
prevComplete();
args.completeCallback();
};
}
self.container.trigger( 'collapse' );
self.container.addClass( 'collapsing' );
if ( self.params.is_wide ) {
$inside.fadeOut( args.duration, complete );
} else {
$inside.slideUp( args.duration, function() {
$widget.css( { width:'', margin:'' } );
complete();
} );
}
}
},
/**
* Get the position (index) of the widget in the containing sidebar
*
* @return {number}
*/
getWidgetSidebarPosition: function() {
var sidebarWidgetIds, position;
sidebarWidgetIds = this.getSidebarWidgetsControl().setting();
position = _.indexOf( sidebarWidgetIds, this.params.widget_id );
if ( position === -1 ) {
return;
}
return position;
},
/**
* Move widget up one in the sidebar
*/
moveUp: function() {
this._moveWidgetByOne( -1 );
},
/**
* Move widget up one in the sidebar
*/
moveDown: function() {
this._moveWidgetByOne( 1 );
},
/**
* @private
*
* @param {number} offset 1|-1
*/
_moveWidgetByOne: function( offset ) {
var i, sidebarWidgetsSetting, sidebarWidgetIds, adjacentWidgetId;
i = this.getWidgetSidebarPosition();
sidebarWidgetsSetting = this.getSidebarWidgetsControl().setting;
sidebarWidgetIds = Array.prototype.slice.call( sidebarWidgetsSetting() ); // Clone.
adjacentWidgetId = sidebarWidgetIds[i + offset];
sidebarWidgetIds[i + offset] = this.params.widget_id;
sidebarWidgetIds[i] = adjacentWidgetId;
sidebarWidgetsSetting( sidebarWidgetIds );
},
/**
* Toggle visibility of the widget move area
*
* @param {boolean} [showOrHide]
*/
toggleWidgetMoveArea: function( showOrHide ) {
var self = this, $moveWidgetArea;
$moveWidgetArea = this.container.find( '.move-widget-area' );
if ( typeof showOrHide === 'undefined' ) {
showOrHide = ! $moveWidgetArea.hasClass( 'active' );
}
if ( showOrHide ) {
// Reset the selected sidebar.
$moveWidgetArea.find( '.selected' ).removeClass( 'selected' );
$moveWidgetArea.find( 'li' ).filter( function() {
return $( this ).data( 'id' ) === self.params.sidebar_id;
} ).addClass( 'selected' );
this.container.find( '.move-widget-btn' ).prop( 'disabled', true );
}
$moveWidgetArea.toggleClass( 'active', showOrHide );
},
/**
* Highlight the widget control and section
*/
highlightSectionAndControl: function() {
var $target;
if ( this.container.is( ':hidden' ) ) {
$target = this.container.closest( '.control-section' );
} else {
$target = this.container;
}
$( '.highlighted' ).removeClass( 'highlighted' );
$target.addClass( 'highlighted' );
setTimeout( function() {
$target.removeClass( 'highlighted' );
}, 500 );
}
} );
/**
* wp.customize.Widgets.WidgetsPanel
*
* Customizer panel containing the widget area sections.
*
* @since 4.4.0
*
* @class wp.customize.Widgets.WidgetsPanel
* @augments wp.customize.Panel
*/
api.Widgets.WidgetsPanel = api.Panel.extend(/** @lends wp.customize.Widgets.WigetsPanel.prototype */{
/**
* Add and manage the display of the no-rendered-areas notice.
*
* @since 4.4.0
*/
ready: function () {
var panel = this;
api.Panel.prototype.ready.call( panel );
panel.deferred.embedded.done(function() {
var panelMetaContainer, noticeContainer, updateNotice, getActiveSectionCount, shouldShowNotice;
panelMetaContainer = panel.container.find( '.panel-meta' );
// @todo This should use the Notifications API introduced to panels. See <https://core.trac.wordpress.org/ticket/38794>.
noticeContainer = $( '<div></div>', {
'class': 'no-widget-areas-rendered-notice',
'role': 'alert'
});
panelMetaContainer.append( noticeContainer );
/**
* Get the number of active sections in the panel.
*
* @return {number} Number of active sidebar sections.
*/
getActiveSectionCount = function() {
return _.filter( panel.sections(), function( section ) {
return 'sidebar' === section.params.type && section.active();
} ).length;
};
/**
* Determine whether or not the notice should be displayed.
*
* @return {boolean}
*/
shouldShowNotice = function() {
var activeSectionCount = getActiveSectionCount();
if ( 0 === activeSectionCount ) {
return true;
} else {
return activeSectionCount !== api.Widgets.data.registeredSidebars.length;
}
};
/**
* Update the notice.
*
* @return {void}
*/
updateNotice = function() {
var activeSectionCount = getActiveSectionCount(), someRenderedMessage, nonRenderedAreaCount, registeredAreaCount;
noticeContainer.empty();
registeredAreaCount = api.Widgets.data.registeredSidebars.length;
if ( activeSectionCount !== registeredAreaCount ) {
if ( 0 !== activeSectionCount ) {
nonRenderedAreaCount = registeredAreaCount - activeSectionCount;
someRenderedMessage = l10n.someAreasShown[ nonRenderedAreaCount ];
} else {
someRenderedMessage = l10n.noAreasShown;
}
if ( someRenderedMessage ) {
noticeContainer.append( $( '<p></p>', {
text: someRenderedMessage
} ) );
}
noticeContainer.append( $( '<p></p>', {
text: l10n.navigatePreview
} ) );
}
};
updateNotice();
/*
* Set the initial visibility state for rendered notice.
* Update the visibility of the notice whenever a reflow happens.
*/
noticeContainer.toggle( shouldShowNotice() );
api.previewer.deferred.active.done( function () {
noticeContainer.toggle( shouldShowNotice() );
});
api.bind( 'pane-contents-reflowed', function() {
var duration = ( 'resolved' === api.previewer.deferred.active.state() ) ? 'fast' : 0;
updateNotice();
if ( shouldShowNotice() ) {
noticeContainer.slideDown( duration );
} else {
noticeContainer.slideUp( duration );
}
});
});
},
/**
* Allow an active widgets panel to be contextually active even when it has no active sections (widget areas).
*
* This ensures that the widgets panel appears even when there are no
* sidebars displayed on the URL currently being previewed.
*
* @since 4.4.0
*
* @return {boolean}
*/
isContextuallyActive: function() {
var panel = this;
return panel.active();
}
});
/**
* wp.customize.Widgets.SidebarSection
*
* Customizer section representing a widget area widget
*
* @since 4.1.0
*
* @class wp.customize.Widgets.SidebarSection
* @augments wp.customize.Section
*/
api.Widgets.SidebarSection = api.Section.extend(/** @lends wp.customize.Widgets.SidebarSection.prototype */{
/**
* Sync the section's active state back to the Backbone model's is_rendered attribute
*
* @since 4.1.0
*/
ready: function () {
var section = this, registeredSidebar;
api.Section.prototype.ready.call( this );
registeredSidebar = api.Widgets.registeredSidebars.get( section.params.sidebarId );
section.active.bind( function ( active ) {
registeredSidebar.set( 'is_rendered', active );
});
registeredSidebar.set( 'is_rendered', section.active() );
}
});
/**
* wp.customize.Widgets.SidebarControl
*
* Customizer control for widgets.
* Note that 'sidebar_widgets' must match the WP_Widget_Area_Customize_Control::$type
*
* @since 3.9.0
*
* @class wp.customize.Widgets.SidebarControl
* @augments wp.customize.Control
*/
api.Widgets.SidebarControl = api.Control.extend(/** @lends wp.customize.Widgets.SidebarControl.prototype */{
/**
* Set up the control
*/
ready: function() {
this.$controlSection = this.container.closest( '.control-section' );
this.$sectionContent = this.container.closest( '.accordion-section-content' );
this._setupModel();
this._setupSortable();
this._setupAddition();
this._applyCardinalOrderClassNames();
},
/**
* Update ordering of widget control forms when the setting is updated
*/
_setupModel: function() {
var self = this;
this.setting.bind( function( newWidgetIds, oldWidgetIds ) {
var widgetFormControls, removedWidgetIds, priority;
removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds );
// Filter out any persistent widget IDs for widgets which have been deactivated.
newWidgetIds = _( newWidgetIds ).filter( function( newWidgetId ) {
var parsedWidgetId = parseWidgetId( newWidgetId );
return !! api.Widgets.availableWidgets.findWhere( { id_base: parsedWidgetId.id_base } );
} );
widgetFormControls = _( newWidgetIds ).map( function( widgetId ) {
var widgetFormControl = api.Widgets.getWidgetFormControlForWidget( widgetId );
if ( ! widgetFormControl ) {
widgetFormControl = self.addWidget( widgetId );
}
return widgetFormControl;
} );
// Sort widget controls to their new positions.
widgetFormControls.sort( function( a, b ) {
var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ),
bIndex = _.indexOf( newWidgetIds, b.params.widget_id );
return aIndex - bIndex;
});
priority = 0;
_( widgetFormControls ).each( function ( control ) {
control.priority( priority );
control.section( self.section() );
priority += 1;
});
self.priority( priority ); // Make sure sidebar control remains at end.
// Re-sort widget form controls (including widgets form other sidebars newly moved here).
self._applyCardinalOrderClassNames();
// If the widget was dragged into the sidebar, make sure the sidebar_id param is updated.
_( widgetFormControls ).each( function( widgetFormControl ) {
widgetFormControl.params.sidebar_id = self.params.sidebar_id;
} );
// Cleanup after widget removal.
_( removedWidgetIds ).each( function( removedWidgetId ) {
// Using setTimeout so that when moving a widget to another sidebar,
// the other sidebars_widgets settings get a chance to update.
setTimeout( function() {
var removedControl, wasDraggedToAnotherSidebar, inactiveWidgets, removedIdBase,
widget, isPresentInAnotherSidebar = false;
// Check if the widget is in another sidebar.
api.each( function( otherSetting ) {
if ( otherSetting.id === self.setting.id || 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) || otherSetting.id === 'sidebars_widgets[wp_inactive_widgets]' ) {
return;
}
var otherSidebarWidgets = otherSetting(), i;
i = _.indexOf( otherSidebarWidgets, removedWidgetId );
if ( -1 !== i ) {
isPresentInAnotherSidebar = true;
}
} );
// If the widget is present in another sidebar, abort!
if ( isPresentInAnotherSidebar ) {
return;
}
removedControl = api.Widgets.getWidgetFormControlForWidget( removedWidgetId );
// Detect if widget control was dragged to another sidebar.
wasDraggedToAnotherSidebar = removedControl && $.contains( document, removedControl.container[0] ) && ! $.contains( self.$sectionContent[0], removedControl.container[0] );
// Delete any widget form controls for removed widgets.
if ( removedControl && ! wasDraggedToAnotherSidebar ) {
api.control.remove( removedControl.id );
removedControl.container.remove();
}
// Move widget to inactive widgets sidebar (move it to Trash) if has been previously saved.
// This prevents the inactive widgets sidebar from overflowing with throwaway widgets.
if ( api.Widgets.savedWidgetIds[removedWidgetId] ) {
inactiveWidgets = api.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice();
inactiveWidgets.push( removedWidgetId );
api.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactiveWidgets ).unique() );
}
// Make old single widget available for adding again.
removedIdBase = parseWidgetId( removedWidgetId ).id_base;
widget = api.Widgets.availableWidgets.findWhere( { id_base: removedIdBase } );
if ( widget && ! widget.get( 'is_multi' ) ) {
widget.set( 'is_disabled', false );
}
} );
} );
} );
},
/**
* Allow widgets in sidebar to be re-ordered, and for the order to be previewed
*/
_setupSortable: function() {
var self = this;
this.isReordering = false;
/**
* Update widget order setting when controls are re-ordered
*/
this.$sectionContent.sortable( {
items: '> .customize-control-widget_form',
handle: '.widget-top',
axis: 'y',
tolerance: 'pointer',
connectWith: '.accordion-section-content:has(.customize-control-sidebar_widgets)',
update: function() {
var widgetContainerIds = self.$sectionContent.sortable( 'toArray' ), widgetIds;
widgetIds = $.map( widgetContainerIds, function( widgetContainerId ) {
return $( '#' + widgetContainerId ).find( ':input[name=widget-id]' ).val();
} );
self.setting( widgetIds );
}
} );
/**
* Expand other Customizer sidebar section when dragging a control widget over it,
* allowing the control to be dropped into another section
*/
this.$controlSection.find( '.accordion-section-title' ).droppable({
accept: '.customize-control-widget_form',
over: function() {
var section = api.section( self.section.get() );
section.expand({
allowMultiple: true, // Prevent the section being dragged from to be collapsed.
completeCallback: function () {
// @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed.
api.section.each( function ( otherSection ) {
if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) {
otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' );
}
} );
}
});
}
});
/**
* Keyboard-accessible reordering
*/
this.container.find( '.reorder-toggle' ).on( 'click', function() {
self.toggleReordering( ! self.isReordering );
} );
},
/**
* Set up UI for adding a new widget
*/
_setupAddition: function() {
var self = this;
this.container.find( '.add-new-widget' ).on( 'click', function() {
var addNewWidgetBtn = $( this );
if ( self.$sectionContent.hasClass( 'reordering' ) ) {
return;
}
if ( ! $( 'body' ).hasClass( 'adding-widget' ) ) {
addNewWidgetBtn.attr( 'aria-expanded', 'true' );
api.Widgets.availableWidgetsPanel.open( self );
} else {
addNewWidgetBtn.attr( 'aria-expanded', 'false' );
api.Widgets.availableWidgetsPanel.close();
}
} );
},
/**
* Add classes to the widget_form controls to assist with styling
*/
_applyCardinalOrderClassNames: function() {
var widgetControls = [];
_.each( this.setting(), function ( widgetId ) {
var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId );
if ( widgetControl ) {
widgetControls.push( widgetControl );
}
});
if ( 0 === widgetControls.length || ( 1 === api.Widgets.registeredSidebars.length && widgetControls.length <= 1 ) ) {
this.container.find( '.reorder-toggle' ).hide();
return;
} else {
this.container.find( '.reorder-toggle' ).show();
}
$( widgetControls ).each( function () {
$( this.container )
.removeClass( 'first-widget' )
.removeClass( 'last-widget' )
.find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
});
_.first( widgetControls ).container
.addClass( 'first-widget' )
.find( '.move-widget-up' ).prop( 'tabIndex', -1 );
_.last( widgetControls ).container
.addClass( 'last-widget' )
.find( '.move-widget-down' ).prop( 'tabIndex', -1 );
},
/***********************************************************************
* Begin public API methods
**********************************************************************/
/**
* Enable/disable the reordering UI
*
* @param {boolean} showOrHide to enable/disable reordering
*
* @todo We should have a reordering state instead and rename this to onChangeReordering
*/
toggleReordering: function( showOrHide ) {
var addNewWidgetBtn = this.$sectionContent.find( '.add-new-widget' ),
reorderBtn = this.container.find( '.reorder-toggle' ),
widgetsTitle = this.$sectionContent.find( '.widget-title' );
showOrHide = Boolean( showOrHide );
if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) {
return;
}
this.isReordering = showOrHide;
this.$sectionContent.toggleClass( 'reordering', showOrHide );
if ( showOrHide ) {
_( this.getWidgetFormControls() ).each( function( formControl ) {
formControl.collapse();
} );
addNewWidgetBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
reorderBtn.attr( 'aria-label', l10n.reorderLabelOff );
wp.a11y.speak( l10n.reorderModeOn );
// Hide widget titles while reordering: title is already in the reorder controls.
widgetsTitle.attr( 'aria-hidden', 'true' );
} else {
addNewWidgetBtn.removeAttr( 'tabindex aria-hidden' );
reorderBtn.attr( 'aria-label', l10n.reorderLabelOn );
wp.a11y.speak( l10n.reorderModeOff );
widgetsTitle.attr( 'aria-hidden', 'false' );
}
},
/**
* Get the widget_form Customize controls associated with the current sidebar.
*
* @since 3.9.0
* @return {wp.customize.controlConstructor.widget_form[]}
*/
getWidgetFormControls: function() {
var formControls = [];
_( this.setting() ).each( function( widgetId ) {
var settingId = widgetIdToSettingId( widgetId ),
formControl = api.control( settingId );
if ( formControl ) {
formControls.push( formControl );
}
} );
return formControls;
},
/**
* @param {string} widgetId or an id_base for adding a previously non-existing widget.
* @return {Object|false} widget_form control instance, or false on error.
*/
addWidget: function( widgetId ) {
var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor,
parsedWidgetId = parseWidgetId( widgetId ),
widgetNumber = parsedWidgetId.number,
widgetIdBase = parsedWidgetId.id_base,
widget = api.Widgets.availableWidgets.findWhere( {id_base: widgetIdBase} ),
settingId, isExistingWidget, widgetFormControl, sidebarWidgets, settingArgs, setting;
if ( ! widget ) {
return false;
}
if ( widgetNumber && ! widget.get( 'is_multi' ) ) {
return false;
}
// Set up new multi widget.
if ( widget.get( 'is_multi' ) && ! widgetNumber ) {
widget.set( 'multi_number', widget.get( 'multi_number' ) + 1 );
widgetNumber = widget.get( 'multi_number' );
}
controlHtml = $( '#widget-tpl-' + widget.get( 'id' ) ).html().trim();
if ( widget.get( 'is_multi' ) ) {
controlHtml = controlHtml.replace( /<[^<>]+>/g, function( m ) {
return m.replace( /__i__|%i%/g, widgetNumber );
} );
} else {
widget.set( 'is_disabled', true ); // Prevent single widget from being added again now.
}
$widget = $( controlHtml );
controlContainer = $( '<li/>' )
.addClass( 'customize-control' )
.addClass( 'customize-control-' + controlType )
.append( $widget );
// Remove icon which is visible inside the panel.
controlContainer.find( '> .widget-icon' ).remove();
if ( widget.get( 'is_multi' ) ) {
controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber );
controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber );
}
widgetId = controlContainer.find( '[name="widget-id"]' ).val();
controlContainer.hide(); // To be slid-down below.
settingId = 'widget_' + widget.get( 'id_base' );
if ( widget.get( 'is_multi' ) ) {
settingId += '[' + widgetNumber + ']';
}
controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
// Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget).
isExistingWidget = api.has( settingId );
if ( ! isExistingWidget ) {
settingArgs = {
transport: api.Widgets.data.selectiveRefreshableWidgets[ widget.get( 'id_base' ) ] ? 'postMessage' : 'refresh',
previewer: this.setting.previewer
};
setting = api.create( settingId, settingId, '', settingArgs );
setting.set( {} ); // Mark dirty, changing from '' to {}.
}
controlConstructor = api.controlConstructor[controlType];
widgetFormControl = new controlConstructor( settingId, {
settings: {
'default': settingId
},
content: controlContainer,
sidebar_id: self.params.sidebar_id,
widget_id: widgetId,
widget_id_base: widget.get( 'id_base' ),
type: controlType,
is_new: ! isExistingWidget,
width: widget.get( 'width' ),
height: widget.get( 'height' ),
is_wide: widget.get( 'is_wide' )
} );
api.control.add( widgetFormControl );
// Make sure widget is removed from the other sidebars.
api.each( function( otherSetting ) {
if ( otherSetting.id === self.setting.id ) {
return;
}
if ( 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) ) {
return;
}
var otherSidebarWidgets = otherSetting().slice(),
i = _.indexOf( otherSidebarWidgets, widgetId );
if ( -1 !== i ) {
otherSidebarWidgets.splice( i );
otherSetting( otherSidebarWidgets );
}
} );
// Add widget to this sidebar.
sidebarWidgets = this.setting().slice();
if ( -1 === _.indexOf( sidebarWidgets, widgetId ) ) {
sidebarWidgets.push( widgetId );
this.setting( sidebarWidgets );
}
controlContainer.slideDown( function() {
if ( isExistingWidget ) {
widgetFormControl.updateWidget( {
instance: widgetFormControl.setting()
} );
}
} );
return widgetFormControl;
}
} );
// Register models for custom panel, section, and control types.
$.extend( api.panelConstructor, {
widgets: api.Widgets.WidgetsPanel
});
$.extend( api.sectionConstructor, {
sidebar: api.Widgets.SidebarSection
});
$.extend( api.controlConstructor, {
widget_form: api.Widgets.WidgetControl,
sidebar_widgets: api.Widgets.SidebarControl
});
/**
* Init Customizer for widgets.
*/
api.bind( 'ready', function() {
// Set up the widgets panel.
api.Widgets.availableWidgetsPanel = new api.Widgets.AvailableWidgetsPanelView({
collection: api.Widgets.availableWidgets
});
// Highlight widget control.
api.previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl );
// Open and focus widget control.
api.previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl );
} );
/**
* Highlight a widget control.
*
* @param {string} widgetId
*/
api.Widgets.highlightWidgetFormControl = function( widgetId ) {
var control = api.Widgets.getWidgetFormControlForWidget( widgetId );
if ( control ) {
control.highlightSectionAndControl();
}
},
/**
* Focus a widget control.
*
* @param {string} widgetId
*/
api.Widgets.focusWidgetFormControl = function( widgetId ) {
var control = api.Widgets.getWidgetFormControlForWidget( widgetId );
if ( control ) {
control.focus();
}
},
/**
* Given a widget control, find the sidebar widgets control that contains it.
* @param {string} widgetId
* @return {Object|null}
*/
api.Widgets.getSidebarWidgetControlContainingWidget = function( widgetId ) {
var foundControl = null;
// @todo This can use widgetIdToSettingId(), then pass into wp.customize.control( x ).getSidebarWidgetsControl().
api.control.each( function( control ) {
if ( control.params.type === 'sidebar_widgets' && -1 !== _.indexOf( control.setting(), widgetId ) ) {
foundControl = control;
}
} );
return foundControl;
};
/**
* Given a widget ID for a widget appearing in the preview, get the widget form control associated with it.
*
* @param {string} widgetId
* @return {Object|null}
*/
api.Widgets.getWidgetFormControlForWidget = function( widgetId ) {
var foundControl = null;
// @todo We can just use widgetIdToSettingId() here.
api.control.each( function( control ) {
if ( control.params.type === 'widget_form' && control.params.widget_id === widgetId ) {
foundControl = control;
}
} );
return foundControl;
};
/**
* Initialize Edit Menu button in Nav Menu widget.
*/
$( document ).on( 'widget-added', function( event, widgetContainer ) {
var parsedWidgetId, widgetControl, navMenuSelect, editMenuButton;
parsedWidgetId = parseWidgetId( widgetContainer.find( '> .widget-inside > .form > .widget-id' ).val() );
if ( 'nav_menu' !== parsedWidgetId.id_base ) {
return;
}
widgetControl = api.control( 'widget_nav_menu[' + String( parsedWidgetId.number ) + ']' );
if ( ! widgetControl ) {
return;
}
navMenuSelect = widgetContainer.find( 'select[name*="nav_menu"]' );
editMenuButton = widgetContainer.find( '.edit-selected-nav-menu > button' );
if ( 0 === navMenuSelect.length || 0 === editMenuButton.length ) {
return;
}
navMenuSelect.on( 'change', function() {
if ( api.section.has( 'nav_menu[' + navMenuSelect.val() + ']' ) ) {
editMenuButton.parent().show();
} else {
editMenuButton.parent().hide();
}
});
editMenuButton.on( 'click', function() {
var section = api.section( 'nav_menu[' + navMenuSelect.val() + ']' );
if ( section ) {
focusConstructWithBreadcrumb( section, widgetControl );
}
} );
} );
/**
* Focus (expand) one construct and then focus on another construct after the first is collapsed.
*
* This overrides the back button to serve the purpose of breadcrumb navigation.
*
* @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} focusConstruct - The object to initially focus.
* @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} returnConstruct - The object to return focus.
*/
function focusConstructWithBreadcrumb( focusConstruct, returnConstruct ) {
focusConstruct.focus();
function onceCollapsed( isExpanded ) {
if ( ! isExpanded ) {
focusConstruct.expanded.unbind( onceCollapsed );
returnConstruct.focus();
}
}
focusConstruct.expanded.bind( onceCollapsed );
}
/**
* @param {string} widgetId
* @return {Object}
*/
function parseWidgetId( widgetId ) {
var matches, parsed = {
number: null,
id_base: null
};
matches = widgetId.match( /^(.+)-(\d+)$/ );
if ( matches ) {
parsed.id_base = matches[1];
parsed.number = parseInt( matches[2], 10 );
} else {
// Likely an old single widget.
parsed.id_base = widgetId;
}
return parsed;
}
/**
* @param {string} widgetId
* @return {string} settingId
*/
function widgetIdToSettingId( widgetId ) {
var parsed = parseWidgetId( widgetId ), settingId;
settingId = 'widget_' + parsed.id_base;
if ( parsed.number ) {
settingId += '[' + parsed.number + ']';
}
return settingId;
}
})( window.wp, jQuery );
/**
* @output wp-admin/js/dashboard.js
*/
/* global pagenow, ajaxurl, postboxes, wpActiveEditor:true, ajaxWidgets */
/* global ajaxPopulateWidgets, quickPressLoad, */
window.wp = window.wp || {};
window.communityEventsData = window.communityEventsData || {};
/**
* Initializes the dashboard widget functionality.
*
* @since 2.7.0
*/
jQuery( function($) {
var welcomePanel = $( '#welcome-panel' ),
welcomePanelHide = $('#wp_welcome_panel-hide'),
updateWelcomePanel;
/**
* Saves the visibility of the welcome panel.
*
* @since 3.3.0
*
* @param {boolean} visible Should it be visible or not.
*
* @return {void}
*/
updateWelcomePanel = function( visible ) {
$.post(
ajaxurl,
{
action: 'update-welcome-panel',
visible: visible,
welcomepanelnonce: $( '#welcomepanelnonce' ).val()
},
function() {
wp.a11y.speak( wp.i18n.__( 'Screen Options updated.' ) );
}
);
};
// Unhide the welcome panel if the Welcome Option checkbox is checked.
if ( welcomePanel.hasClass('hidden') && welcomePanelHide.prop('checked') ) {
welcomePanel.removeClass('hidden');
}
// Hide the welcome panel when the dismiss button or close button is clicked.
$('.welcome-panel-close, .welcome-panel-dismiss a', welcomePanel).on( 'click', function(e) {
e.preventDefault();
welcomePanel.addClass('hidden');
updateWelcomePanel( 0 );
$('#wp_welcome_panel-hide').prop('checked', false);
});
// Set welcome panel visibility based on Welcome Option checkbox value.
welcomePanelHide.on( 'click', function() {
welcomePanel.toggleClass('hidden', ! this.checked );
updateWelcomePanel( this.checked ? 1 : 0 );
});
/**
* These widgets can be populated via ajax.
*
* @since 2.7.0
*
* @type {string[]}
*
* @global
*/
window.ajaxWidgets = ['dashboard_primary'];
/**
* Triggers widget updates via Ajax.
*
* @since 2.7.0
*
* @global
*
* @param {string} el Optional. Widget to fetch or none to update all.
*
* @return {void}
*/
window.ajaxPopulateWidgets = function(el) {
/**
* Fetch the latest representation of the widget via Ajax and show it.
*
* @param {number} i Number of half-seconds to use as the timeout.
* @param {string} id ID of the element which is going to be checked for changes.
*
* @return {void}
*/
function show(i, id) {
var p, e = $('#' + id + ' div.inside:visible').find('.widget-loading');
// If the element is found in the dom, queue to load latest representation.
if ( e.length ) {
p = e.parent();
setTimeout( function(){
// Request the widget content.
p.load( ajaxurl + '?action=dashboard-widgets&widget=' + id + '&pagenow=' + pagenow, '', function() {
// Hide the parent and slide it out for visual fanciness.
p.hide().slideDown('normal', function(){
$(this).css('display', '');
});
});
}, i * 500 );
}
}
// If we have received a specific element to fetch, check if it is valid.
if ( el ) {
el = el.toString();
// If the element is available as Ajax widget, show it.
if ( $.inArray(el, ajaxWidgets) !== -1 ) {
// Show element without any delay.
show(0, el);
}
} else {
// Walk through all ajaxWidgets, loading them after each other.
$.each( ajaxWidgets, show );
}
};
// Initially populate ajax widgets.
ajaxPopulateWidgets();
// Register ajax widgets as postbox toggles.
postboxes.add_postbox_toggles(pagenow, { pbshow: ajaxPopulateWidgets } );
/**
* Control the Quick Press (Quick Draft) widget.
*
* @since 2.7.0
*
* @global
*
* @return {void}
*/
window.quickPressLoad = function() {
var act = $('#quickpost-action'), t;
// Enable the submit buttons.
$( '#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]' ).prop( 'disabled' , false );
t = $('#quick-press').on( 'submit', function( e ) {
e.preventDefault();
// Show a spinner.
$('#dashboard_quick_press #publishing-action .spinner').show();
// Disable the submit button to prevent duplicate submissions.
$('#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]').prop('disabled', true);
// Post the entered data to save it.
$.post( t.attr( 'action' ), t.serializeArray(), function( data ) {
// Replace the form, and prepend the published post.
$('#dashboard_quick_press .inside').html( data );
$('#quick-press').removeClass('initial-form');
quickPressLoad();
highlightLatestPost();
// Focus the title to allow for quickly drafting another post.
$('#title').trigger( 'focus' );
});
/**
* Highlights the latest post for one second.
*
* @return {void}
*/
function highlightLatestPost () {
var latestPost = $('.drafts ul li').first();
latestPost.css('background', '#fffbe5');
setTimeout(function () {
latestPost.css('background', 'none');
}, 1000);
}
} );
// Change the QuickPost action to the publish value.
$('#publish').on( 'click', function() { act.val( 'post-quickpress-publish' ); } );
$('#quick-press').on( 'click focusin', function() {
wpActiveEditor = 'content';
});
autoResizeTextarea();
};
window.quickPressLoad();
// Enable the dragging functionality of the widgets.
$( '.meta-box-sortables' ).sortable( 'option', 'containment', '#wpwrap' );
/**
* Adjust the height of the textarea based on the content.
*
* @since 3.6.0
*
* @return {void}
*/
function autoResizeTextarea() {
// When IE8 or older is used to render this document, exit.
if ( document.documentMode && document.documentMode < 9 ) {
return;
}
// Add a hidden div. We'll copy over the text from the textarea to measure its height.
$('body').append( '<div class="quick-draft-textarea-clone" style="display: none;"></div>' );
var clone = $('.quick-draft-textarea-clone'),
editor = $('#content'),
editorHeight = editor.height(),
/*
* 100px roughly accounts for browser chrome and allows the
* save draft button to show on-screen at the same time.
*/
editorMaxHeight = $(window).height() - 100;
/*
* Match up textarea and clone div as much as possible.
* Padding cannot be reliably retrieved using shorthand in all browsers.
*/
clone.css({
'font-family': editor.css('font-family'),
'font-size': editor.css('font-size'),
'line-height': editor.css('line-height'),
'padding-bottom': editor.css('paddingBottom'),
'padding-left': editor.css('paddingLeft'),
'padding-right': editor.css('paddingRight'),
'padding-top': editor.css('paddingTop'),
'white-space': 'pre-wrap',
'word-wrap': 'break-word',
'display': 'none'
});
// The 'propertychange' is used in IE < 9.
editor.on('focus input propertychange', function() {
var $this = $(this),
// Add a non-breaking space to ensure that the height of a trailing newline is
// included.
textareaContent = $this.val() + '&nbsp;',
// Add 2px to compensate for border-top & border-bottom.
cloneHeight = clone.css('width', $this.css('width')).text(textareaContent).outerHeight() + 2;
// Default to show a vertical scrollbar, if needed.
editor.css('overflow-y', 'auto');
// Only change the height if it has changed and both heights are below the max.
if ( cloneHeight === editorHeight || ( cloneHeight >= editorMaxHeight && editorHeight >= editorMaxHeight ) ) {
return;
}
/*
* Don't allow editor to exceed the height of the window.
* This is also bound in CSS to a max-height of 1300px to be extra safe.
*/
if ( cloneHeight > editorMaxHeight ) {
editorHeight = editorMaxHeight;
} else {
editorHeight = cloneHeight;
}
// Disable scrollbars because we adjust the height to the content.
editor.css('overflow', 'hidden');
$this.css('height', editorHeight + 'px');
});
}
} );
jQuery( function( $ ) {
'use strict';
var communityEventsData = window.communityEventsData,
dateI18n = wp.date.dateI18n,
format = wp.date.format,
sprintf = wp.i18n.sprintf,
__ = wp.i18n.__,
_x = wp.i18n._x,
app;
/**
* Global Community Events namespace.
*
* @since 4.8.0
*
* @memberOf wp
* @namespace wp.communityEvents
*/
app = window.wp.communityEvents = /** @lends wp.communityEvents */{
initialized: false,
model: null,
/**
* Initializes the wp.communityEvents object.
*
* @since 4.8.0
*
* @return {void}
*/
init: function() {
if ( app.initialized ) {
return;
}
var $container = $( '#community-events' );
/*
* When JavaScript is disabled, the errors container is shown, so
* that "This widget requires JavaScript" message can be seen.
*
* When JS is enabled, the container is hidden at first, and then
* revealed during the template rendering, if there actually are
* errors to show.
*
* The display indicator switches from `hide-if-js` to `aria-hidden`
* here in order to maintain consistency with all the other fields
* that key off of `aria-hidden` to determine their visibility.
* `aria-hidden` can't be used initially, because there would be no
* way to set it to false when JavaScript is disabled, which would
* prevent people from seeing the "This widget requires JavaScript"
* message.
*/
$( '.community-events-errors' )
.attr( 'aria-hidden', 'true' )
.removeClass( 'hide-if-js' );
$container.on( 'click', '.community-events-toggle-location, .community-events-cancel', app.toggleLocationForm );
/**
* Filters events based on entered location.
*
* @return {void}
*/
$container.on( 'submit', '.community-events-form', function( event ) {
var location = $( '#community-events-location' ).val().trim();
event.preventDefault();
/*
* Don't trigger a search if the search field is empty or the
* search term was made of only spaces before being trimmed.
*/
if ( ! location ) {
return;
}
app.getEvents({
location: location
});
});
if ( communityEventsData && communityEventsData.cache && communityEventsData.cache.location && communityEventsData.cache.events ) {
app.renderEventsTemplate( communityEventsData.cache, 'app' );
} else {
app.getEvents();
}
app.initialized = true;
},
/**
* Toggles the visibility of the Edit Location form.
*
* @since 4.8.0
*
* @param {event|string} action 'show' or 'hide' to specify a state;
* or an event object to flip between states.
*
* @return {void}
*/
toggleLocationForm: function( action ) {
var $toggleButton = $( '.community-events-toggle-location' ),
$cancelButton = $( '.community-events-cancel' ),
$form = $( '.community-events-form' ),
$target = $();
if ( 'object' === typeof action ) {
// The action is the event object: get the clicked element.
$target = $( action.target );
/*
* Strict comparison doesn't work in this case because sometimes
* we explicitly pass a string as value of aria-expanded and
* sometimes a boolean as the result of an evaluation.
*/
action = 'true' == $toggleButton.attr( 'aria-expanded' ) ? 'hide' : 'show';
}
if ( 'hide' === action ) {
$toggleButton.attr( 'aria-expanded', 'false' );
$cancelButton.attr( 'aria-expanded', 'false' );
$form.attr( 'aria-hidden', 'true' );
/*
* If the Cancel button has been clicked, bring the focus back
* to the toggle button so users relying on screen readers don't
* lose their place.
*/
if ( $target.hasClass( 'community-events-cancel' ) ) {
$toggleButton.trigger( 'focus' );
}
} else {
$toggleButton.attr( 'aria-expanded', 'true' );
$cancelButton.attr( 'aria-expanded', 'true' );
$form.attr( 'aria-hidden', 'false' );
}
},
/**
* Sends REST API requests to fetch events for the widget.
*
* @since 4.8.0
*
* @param {Object} requestParams REST API Request parameters object.
*
* @return {void}
*/
getEvents: function( requestParams ) {
var initiatedBy,
app = this,
$spinner = $( '.community-events-form' ).children( '.spinner' );
requestParams = requestParams || {};
requestParams._wpnonce = communityEventsData.nonce;
requestParams.timezone = window.Intl ? window.Intl.DateTimeFormat().resolvedOptions().timeZone : '';
initiatedBy = requestParams.location ? 'user' : 'app';
$spinner.addClass( 'is-active' );
wp.ajax.post( 'get-community-events', requestParams )
.always( function() {
$spinner.removeClass( 'is-active' );
})
.done( function( response ) {
if ( 'no_location_available' === response.error ) {
if ( requestParams.location ) {
response.unknownCity = requestParams.location;
} else {
/*
* No location was passed, which means that this was an automatic query
* based on IP, locale, and timezone. Since the user didn't initiate it,
* it should fail silently. Otherwise, the error could confuse and/or
* annoy them.
*/
delete response.error;
}
}
app.renderEventsTemplate( response, initiatedBy );
})
.fail( function() {
app.renderEventsTemplate({
'location' : false,
'events' : [],
'error' : true
}, initiatedBy );
});
},
/**
* Renders the template for the Events section of the Events & News widget.
*
* @since 4.8.0
*
* @param {Object} templateParams The various parameters that will get passed to wp.template.
* @param {string} initiatedBy 'user' to indicate that this was triggered manually by the user;
* 'app' to indicate it was triggered automatically by the app itself.
*
* @return {void}
*/
renderEventsTemplate: function( templateParams, initiatedBy ) {
var template,
elementVisibility,
$toggleButton = $( '.community-events-toggle-location' ),
$locationMessage = $( '#community-events-location-message' ),
$results = $( '.community-events-results' );
templateParams.events = app.populateDynamicEventFields(
templateParams.events,
communityEventsData.time_format
);
/*
* Hide all toggleable elements by default, to keep the logic simple.
* Otherwise, each block below would have to turn hide everything that
* could have been shown at an earlier point.
*
* The exception to that is that the .community-events container is hidden
* when the page is first loaded, because the content isn't ready yet,
* but once we've reached this point, it should always be shown.
*/
elementVisibility = {
'.community-events' : true,
'.community-events-loading' : false,
'.community-events-errors' : false,
'.community-events-error-occurred' : false,
'.community-events-could-not-locate' : false,
'#community-events-location-message' : false,
'.community-events-toggle-location' : false,
'.community-events-results' : false
};
/*
* Determine which templates should be rendered and which elements
* should be displayed.
*/
if ( templateParams.location.ip ) {
/*
* If the API determined the location by geolocating an IP, it will
* provide events, but not a specific location.
*/
$locationMessage.text( __( 'Attend an upcoming event near you.' ) );
if ( templateParams.events.length ) {
template = wp.template( 'community-events-event-list' );
$results.html( template( templateParams ) );
} else {
template = wp.template( 'community-events-no-upcoming-events' );
$results.html( template( templateParams ) );
}
elementVisibility['#community-events-location-message'] = true;
elementVisibility['.community-events-toggle-location'] = true;
elementVisibility['.community-events-results'] = true;
} else if ( templateParams.location.description ) {
template = wp.template( 'community-events-attend-event-near' );
$locationMessage.html( template( templateParams ) );
if ( templateParams.events.length ) {
template = wp.template( 'community-events-event-list' );
$results.html( template( templateParams ) );
} else {
template = wp.template( 'community-events-no-upcoming-events' );
$results.html( template( templateParams ) );
}
if ( 'user' === initiatedBy ) {
wp.a11y.speak(
sprintf(
/* translators: %s: The name of a city. */
__( 'City updated. Listing events near %s.' ),
templateParams.location.description
),
'assertive'
);
}
elementVisibility['#community-events-location-message'] = true;
elementVisibility['.community-events-toggle-location'] = true;
elementVisibility['.community-events-results'] = true;
} else if ( templateParams.unknownCity ) {
template = wp.template( 'community-events-could-not-locate' );
$( '.community-events-could-not-locate' ).html( template( templateParams ) );
wp.a11y.speak(
sprintf(
/*
* These specific examples were chosen to highlight the fact that a
* state is not needed, even for cities whose name is not unique.
* It would be too cumbersome to include that in the instructions
* to the user, so it's left as an implication.
*/
/*
* translators: %s is the name of the city we couldn't locate.
* Replace the examples with cities related to your locale. Test that
* they match the expected location and have upcoming events before
* including them. If no cities related to your locale have events,
* then use cities related to your locale that would be recognizable
* to most users. Use only the city name itself, without any region
* or country. Use the endonym (native locale name) instead of the
* English name if possible.
*/
__( 'We couldn’t locate %s. Please try another nearby city. For example: Kansas City; Springfield; Portland.' ),
templateParams.unknownCity
)
);
elementVisibility['.community-events-errors'] = true;
elementVisibility['.community-events-could-not-locate'] = true;
} else if ( templateParams.error && 'user' === initiatedBy ) {
/*
* Errors messages are only shown for requests that were initiated
* by the user, not for ones that were initiated by the app itself.
* Showing error messages for an event that user isn't aware of
* could be confusing or unnecessarily distracting.
*/
wp.a11y.speak( __( 'An error occurred. Please try again.' ) );
elementVisibility['.community-events-errors'] = true;
elementVisibility['.community-events-error-occurred'] = true;
} else {
$locationMessage.text( __( 'Enter your closest city to find nearby events.' ) );
elementVisibility['#community-events-location-message'] = true;
elementVisibility['.community-events-toggle-location'] = true;
}
// Set the visibility of toggleable elements.
_.each( elementVisibility, function( isVisible, element ) {
$( element ).attr( 'aria-hidden', ! isVisible );
});
$toggleButton.attr( 'aria-expanded', elementVisibility['.community-events-toggle-location'] );
if ( templateParams.location && ( templateParams.location.ip || templateParams.location.latitude ) ) {
// Hide the form when there's a valid location.
app.toggleLocationForm( 'hide' );
if ( 'user' === initiatedBy ) {
/*
* When the form is programmatically hidden after a user search,
* bring the focus back to the toggle button so users relying
* on screen readers don't lose their place.
*/
$toggleButton.trigger( 'focus' );
}
} else {
app.toggleLocationForm( 'show' );
}
},
/**
* Populate event fields that have to be calculated on the fly.
*
* These can't be stored in the database, because they're dependent on
* the user's current time zone, locale, etc.
*
* @since 5.5.2
*
* @param {Array} rawEvents The events that should have dynamic fields added to them.
* @param {string} timeFormat A time format acceptable by `wp.date.dateI18n()`.
*
* @returns {Array}
*/
populateDynamicEventFields: function( rawEvents, timeFormat ) {
// Clone the parameter to avoid mutating it, so that this can remain a pure function.
var populatedEvents = JSON.parse( JSON.stringify( rawEvents ) );
$.each( populatedEvents, function( index, event ) {
var timeZone = app.getTimeZone( event.start_unix_timestamp * 1000 );
event.user_formatted_date = app.getFormattedDate(
event.start_unix_timestamp * 1000,
event.end_unix_timestamp * 1000,
timeZone
);
event.user_formatted_time = dateI18n(
timeFormat,
event.start_unix_timestamp * 1000,
timeZone
);
event.timeZoneAbbreviation = app.getTimeZoneAbbreviation( event.start_unix_timestamp * 1000 );
} );
return populatedEvents;
},
/**
* Returns the user's local/browser time zone, in a form suitable for `wp.date.i18n()`.
*
* @since 5.5.2
*
* @param startTimestamp
*
* @returns {string|number}
*/
getTimeZone: function( startTimestamp ) {
/*
* Prefer a name like `Europe/Helsinki`, since that automatically tracks daylight savings. This
* doesn't need to take `startTimestamp` into account for that reason.
*/
var timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
/*
* Fall back to an offset for IE11, which declares the property but doesn't assign a value.
*/
if ( 'undefined' === typeof timeZone ) {
/*
* It's important to use the _event_ time, not the _current_
* time, so that daylight savings time is accounted for.
*/
timeZone = app.getFlippedTimeZoneOffset( startTimestamp );
}
return timeZone;
},
/**
* Get intuitive time zone offset.
*
* `Data.prototype.getTimezoneOffset()` returns a positive value for time zones
* that are _behind_ UTC, and a _negative_ value for ones that are ahead.
*
* See https://stackoverflow.com/questions/21102435/why-does-javascript-date-gettimezoneoffset-consider-0500-as-a-positive-off.
*
* @since 5.5.2
*
* @param {number} startTimestamp
*
* @returns {number}
*/
getFlippedTimeZoneOffset: function( startTimestamp ) {
return new Date( startTimestamp ).getTimezoneOffset() * -1;
},
/**
* Get a short time zone name, like `PST`.
*
* @since 5.5.2
*
* @param {number} startTimestamp
*
* @returns {string}
*/
getTimeZoneAbbreviation: function( startTimestamp ) {
var timeZoneAbbreviation,
eventDateTime = new Date( startTimestamp );
/*
* Leaving the `locales` argument undefined is important, so that the browser
* displays the abbreviation that's most appropriate for the current locale. For
* some that will be `UTC{+|-}{n}`, and for others it will be a code like `PST`.
*
* This doesn't need to take `startTimestamp` into account, because a name like
* `America/Chicago` automatically tracks daylight savings.
*/
var shortTimeStringParts = eventDateTime.toLocaleTimeString( undefined, { timeZoneName : 'short' } ).split( ' ' );
if ( 3 === shortTimeStringParts.length ) {
timeZoneAbbreviation = shortTimeStringParts[2];
}
if ( 'undefined' === typeof timeZoneAbbreviation ) {
/*
* It's important to use the _event_ time, not the _current_
* time, so that daylight savings time is accounted for.
*/
var timeZoneOffset = app.getFlippedTimeZoneOffset( startTimestamp ),
sign = -1 === Math.sign( timeZoneOffset ) ? '' : '+';
// translators: Used as part of a string like `GMT+5` in the Events Widget.
timeZoneAbbreviation = _x( 'GMT', 'Events widget offset prefix' ) + sign + ( timeZoneOffset / 60 );
}
return timeZoneAbbreviation;
},
/**
* Format a start/end date in the user's local time zone and locale.
*
* @since 5.5.2
*
* @param {int} startDate The Unix timestamp in milliseconds when the the event starts.
* @param {int} endDate The Unix timestamp in milliseconds when the the event ends.
* @param {string} timeZone A time zone string or offset which is parsable by `wp.date.i18n()`.
*
* @returns {string}
*/
getFormattedDate: function( startDate, endDate, timeZone ) {
var formattedDate;
/*
* The `date_format` option is not used because it's important
* in this context to keep the day of the week in the displayed date,
* so that users can tell at a glance if the event is on a day they
* are available, without having to open the link.
*
* The case of crossing a year boundary is intentionally not handled.
* It's so rare in practice that it's not worth the complexity
* tradeoff. The _ending_ year should be passed to
* `multiple_month_event`, though, just in case.
*/
/* translators: Date format for upcoming events on the dashboard. Include the day of the week. See https://www.php.net/manual/datetime.format.php */
var singleDayEvent = __( 'l, M j, Y' ),
/* translators: Date string for upcoming events. 1: Month, 2: Starting day, 3: Ending day, 4: Year. */
multipleDayEvent = __( '%1$s %2$d–%3$d, %4$d' ),
/* translators: Date string for upcoming events. 1: Starting month, 2: Starting day, 3: Ending month, 4: Ending day, 5: Ending year. */
multipleMonthEvent = __( '%1$s %2$d – %3$s %4$d, %5$d' );
// Detect single-day events.
if ( ! endDate || format( 'Y-m-d', startDate ) === format( 'Y-m-d', endDate ) ) {
formattedDate = dateI18n( singleDayEvent, startDate, timeZone );
// Multiple day events.
} else if ( format( 'Y-m', startDate ) === format( 'Y-m', endDate ) ) {
formattedDate = sprintf(
multipleDayEvent,
dateI18n( _x( 'F', 'upcoming events month format' ), startDate, timeZone ),
dateI18n( _x( 'j', 'upcoming events day format' ), startDate, timeZone ),
dateI18n( _x( 'j', 'upcoming events day format' ), endDate, timeZone ),
dateI18n( _x( 'Y', 'upcoming events year format' ), endDate, timeZone )
);
// Multi-day events that cross a month boundary.
} else {
formattedDate = sprintf(
multipleMonthEvent,
dateI18n( _x( 'F', 'upcoming events month format' ), startDate, timeZone ),
dateI18n( _x( 'j', 'upcoming events day format' ), startDate, timeZone ),
dateI18n( _x( 'F', 'upcoming events month format' ), endDate, timeZone ),
dateI18n( _x( 'j', 'upcoming events day format' ), endDate, timeZone ),
dateI18n( _x( 'Y', 'upcoming events year format' ), endDate, timeZone )
);
}
return formattedDate;
}
};
if ( $( '#dashboard_primary' ).is( ':visible' ) ) {
app.init();
} else {
$( document ).on( 'postbox-toggled', function( event, postbox ) {
var $postbox = $( postbox );
if ( 'dashboard_primary' === $postbox.attr( 'id' ) && $postbox.is( ':visible' ) ) {
app.init();
}
});
}
});
/**
* Removed in 5.6.0, needed for back-compatibility.
*
* @since 4.8.0
* @deprecated 5.6.0
*
* @type {object}
*/
window.communityEventsData.l10n = window.communityEventsData.l10n || {
enter_closest_city: '',
error_occurred_please_try_again: '',
attend_event_near_generic: '',
could_not_locate_city: '',
city_updated: ''
};
window.communityEventsData.l10n = window.wp.deprecateL10nObject( 'communityEventsData.l10n', window.communityEventsData.l10n, '5.6.0' );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment