';
$label = esc_html__( 'TODO', '4wp-todo' );
$wp_admin_bar->add_node(
array(
'id' => 'forwp-todo-admin-bar',
'parent' => false,
'href' => $lists_url,
'title' => $icon . '' . $label . '',
'meta' => array(
'class' => 'forwp-todo-admin-bar-root',
'title' => esc_attr__( 'Open 4WP TODO lists.', '4wp-todo' ),
),
)
);
if ( self::is_active() ) {
$wp_admin_bar->add_node(
array(
'id' => 'forwp-todo-page-notes',
'parent' => 'forwp-todo-admin-bar',
'title' => esc_html__( 'Page notes', '4wp-todo' ),
'href' => '#forwp-todo-open',
'meta' => array(
'class' => 'forwp-todo-admin-bar-trigger',
),
)
);
}
}
/**
* @param bool $show Whether to show admin bar.
* @return bool
*/
public static function maybe_hide_admin_bar( bool $show ): bool {
if ( is_admin() || ! self::is_active() ) {
return $show;
}
return apply_filters( 'forwp_todo_hide_admin_bar', Settings::should_hide_admin_bar() ) ? false : $show;
}
/**
* @return bool
*/
public static function is_active(): bool {
return self::can_use_overlay() && Settings::is_frontend_enabled();
}
/**
* Whether the current user may load the page-notes overlay.
*
* Visibility is controlled solely by Settings::user_can_access_todo() — do not
* re-gate with edit_posts (e.g. WooCommerce customer has TODO access but no edit_posts).
*
* @return bool
*/
public static function can_use_overlay(): bool {
return is_user_logged_in() && Settings::user_can_access_todo();
}
/**
* @return bool
*/
public static function should_show_overlay(): bool {
if ( self::is_frontend_context() ) {
return true;
}
return self::is_block_editor_screen() && self::resolve_post_id() > 0;
}
/**
* @return bool
*/
public static function is_frontend_context(): bool {
if ( is_singular() ) {
return true;
}
return is_preview() && get_queried_object_id() > 0;
}
/**
* @return bool
*/
public static function is_block_editor_screen(): bool {
if ( ! is_admin() ) {
return false;
}
if ( function_exists( 'get_current_screen' ) ) {
$screen = get_current_screen();
if ( $screen && method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() ) {
return true;
}
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- admin screen routing only.
$action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- admin screen routing only.
$post_id = isset( $_GET['post'] ) ? absint( wp_unslash( $_GET['post'] ) ) : 0;
return 'edit' === $action && $post_id > 0;
}
/**
* @return int
*/
public static function resolve_post_id(): int {
if ( is_admin() ) {
return self::get_editing_post_id();
}
return (int) get_queried_object_id();
}
/**
* @return int
*/
private static function get_editing_post_id(): int {
global $post;
if ( $post instanceof \WP_Post ) {
return (int) $post->ID;
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only admin post ID from URL.
if ( isset( $_GET['post'] ) ) {
return absint( wp_unslash( $_GET['post'] ) );
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
return 0;
}
/**
* Canonical admin screen URL for pin context (current request, without pin query flag).
*
* @return string
*/
public static function resolve_admin_screen_url(): string {
if ( ! is_admin() ) {
return '';
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only routing context.
$request_uri = isset( $_SERVER['REQUEST_URI'] )
? sanitize_text_field( wp_unslash( (string) $_SERVER['REQUEST_URI'] ) )
: '';
if ( ! is_string( $request_uri ) || '' === $request_uri ) {
return '';
}
$path = wp_parse_url( $request_uri, PHP_URL_PATH );
$query = wp_parse_url( $request_uri, PHP_URL_QUERY );
if ( ! is_string( $path ) || '' === $path ) {
return '';
}
$args = array();
if ( is_string( $query ) && '' !== $query ) {
wp_parse_str( $query, $args );
}
unset( $args['forwp-todo-pin'] );
$url = home_url( $path );
if ( ! empty( $args ) ) {
$url = add_query_arg( $args, $url );
}
return esc_url_raw( $url );
}
/**
* Stable admin screen slug (page hook, post editor, or pagenow).
*
* Examples: page-forwp-seo, post-434-edit, plugins-php
*
* @return string
*/
public static function resolve_admin_screen_key(): string {
if ( ! is_admin() ) {
return '';
}
global $pagenow;
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only admin screen routing.
if ( isset( $_GET['page'] ) ) {
return 'page-' . sanitize_key( wp_unslash( (string) $_GET['page'] ) );
}
if ( isset( $_GET['post'] ) ) {
$post_id = absint( wp_unslash( $_GET['post'] ) );
$action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( (string) $_GET['action'] ) ) : 'edit';
return 'post-' . $post_id . '-' . $action;
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
return sanitize_key( (string) $pagenow );
}
/**
* @return void
*/
public static function enqueue_admin_assets(): void {
if ( ! self::is_active() || ! is_admin() ) {
return;
}
if ( self::is_block_editor_screen() && self::get_editing_post_id() > 0 ) {
return;
}
$post_id = self::get_editing_post_id();
$page_url = $post_id > 0 ? esc_url_raw( get_permalink( $post_id ) ) : self::resolve_admin_screen_url();
self::enqueue_assets( 'admin', $post_id, $page_url );
}
/**
* @return void
*/
public static function render_admin_root(): void {
if ( ! self::is_active() || ! is_admin() ) {
return;
}
if ( self::is_block_editor_screen() && self::get_editing_post_id() > 0 ) {
return;
}
self::render_root( 'admin' );
}
/**
* @return void
*/
public static function enqueue_frontend_assets(): void {
if ( ! self::is_active() || ! self::is_frontend_context() ) {
return;
}
self::enqueue_assets(
'frontend',
self::resolve_post_id(),
esc_url_raw( get_permalink() )
);
}
/**
* @return void
*/
public static function enqueue_editor_assets(): void {
if ( ! self::is_active() ) {
return;
}
$post_id = self::get_editing_post_id();
if ( $post_id <= 0 ) {
return;
}
self::enqueue_assets(
'editor',
$post_id,
esc_url_raw( get_permalink( $post_id ) )
);
}
/**
* @param string $context frontend|editor|admin
* @param int $post_id Current post ID.
* @param string $page_url Permalink for note context.
* @return void
*/
private static function enqueue_assets( string $context, int $post_id, string $page_url ): void {
$asset_file = FORWP_TODO_PATH . 'build/frontend/index.asset.php';
if ( ! is_readable( $asset_file ) ) {
return;
}
$asset = include $asset_file;
$handle = 'forwp-todo-frontend';
$version = $asset['version'];
wp_enqueue_style(
$handle,
forwp_todo_asset_url( 'build/frontend/style-index.css' ),
array(),
$version
);
wp_enqueue_script(
$handle,
forwp_todo_asset_url( 'build/frontend/index.js' ),
$asset['dependencies'],
$version,
true
);
wp_localize_script(
$handle,
'forwpTodoFrontend',
array(
'context' => $context,
'restRoot' => self::rest_root_for_browser(),
'nonce' => wp_create_nonce( 'wp_rest' ),
'postId' => $post_id,
'pageUrl' => $page_url,
'adminScreenUrl' => 'admin' === $context ? self::resolve_admin_screen_url() : '',
'adminScreenKey' => 'admin' === $context ? self::resolve_admin_screen_key() : '',
'storagePostId' => Settings::get_note_storage_post_id(),
'currentUser' => self::current_user_payload(),
'allTasksUrl' => Settings::resolve_all_tasks_url(),
'i18n' => self::i18n_strings(),
)
);
if ( in_array( $context, array( 'editor', 'admin' ), true ) ) {
wp_add_inline_script(
$handle,
'window.forwpTodoInit && window.forwpTodoInit();',
'after'
);
}
}
/**
* @return array
*/
private static function current_user_payload(): array {
$user = wp_get_current_user();
return array(
'id' => (int) $user->ID,
'name' => (string) $user->display_name,
);
}
/**
* @return array
*/
private static function i18n_strings(): array {
return array(
'checklist' => __( 'Page notes', '4wp-todo' ),
'allTasksList' => __( 'Tasks', '4wp-todo' ),
'fabLabel' => __( 'Page notes', '4wp-todo' ),
'fabLabelAdmin' => __( 'Tasks', '4wp-todo' ),
'fabTooltip' => __( 'Page notes — open list, pin to a section', '4wp-todo' ),
'fabTooltipPin' => __( 'Cancel selection (Esc)', '4wp-todo' ),
'panelIntro' => __( 'Pick a section on the page, then leave a short note — like IDE comments.', '4wp-todo' ),
'pinToSection' => __( 'Pin note to section…', '4wp-todo' ),
'shortNote' => __( 'Short note', '4wp-todo' ),
'closePanel' => __( 'Close', '4wp-todo' ),
'editDone' => __( 'Done', '4wp-todo' ),
'loading' => __( 'Loading notes…', '4wp-todo' ),
'addNoteFab' => __( 'Add page note', '4wp-todo' ),
'exitPin' => __( 'Exit selection mode', '4wp-todo' ),
'addNote' => __( 'Add note on page', '4wp-todo' ),
'cancelPin' => __( 'Cancel', '4wp-todo' ),
'pinHint' => __( 'Click a section to attach your note', '4wp-todo' ),
'pinEscHint' => __( 'Press Esc to cancel', '4wp-todo' ),
'pinnedTo' => __( 'Pinned to selected section', '4wp-todo' ),
'newNote' => __( 'New note', '4wp-todo' ),
'save' => __( 'Save note', '4wp-todo' ),
'close' => __( 'Close note', '4wp-todo' ),
'reopen' => __( 'Reopen note', '4wp-todo' ),
'empty' => __( 'No notes on this page yet.', '4wp-todo' ),
'addTask' => __( 'Add a task', '4wp-todo' ),
'taskScope' => __( 'Task scope', '4wp-todo' ),
'scopeAll' => __( 'All', '4wp-todo' ),
'scopeMine' => __( 'Mine', '4wp-todo' ),
'scopeForMe' => __( 'For me', '4wp-todo' ),
'openAllTasks' => __( 'Open all tasks', '4wp-todo' ),
'dragToReorder' => __( 'Drag to reorder', '4wp-todo' ),
'selectPage' => __( 'Page', '4wp-todo' ),
'noPages' => __( 'No pages available', '4wp-todo' ),
'takeElement' => __( 'Take element', '4wp-todo' ),
'taskMenu' => __( 'Task menu', '4wp-todo' ),
'editDetails' => __( 'Edit details', '4wp-todo' ),
'editTask' => __( 'Edit task', '4wp-todo' ),
'assign' => __( 'Assign', '4wp-todo' ),
'assignDepartment' => __( 'Set department…', '4wp-todo' ),
'clearDueDate' => __( 'Clear due date', '4wp-todo' ),
'pickDueDate' => __( 'Pick date…', '4wp-todo' ),
'routeTo' => __( 'Route to', '4wp-todo' ),
'cancelComposer' => __( 'Cancel', '4wp-todo' ),
'deleteTask' => __( 'Delete task', '4wp-todo' ),
'today' => __( 'Today', '4wp-todo' ),
'tomorrow' => __( 'Tomorrow', '4wp-todo' ),
'comment' => __( 'Comment', '4wp-todo' ),
'reply' => __( 'Reply', '4wp-todo' ),
'openList' => __( 'Open list', '4wp-todo' ),
'saveHint' => __( 'Enter in the note field also saves', '4wp-todo' ),
'placeholder' => __( 'What should change here?', '4wp-todo' ),
'openCount' => __( 'Open notes', '4wp-todo' ),
'openLabel' => __( 'open', '4wp-todo' ),
'closedLabel' => __( 'closed', '4wp-todo' ),
'jumpTo' => __( 'Jump to section', '4wp-todo' ),
'clickOpenList' => __( 'Click to open list', '4wp-todo' ),
/* translators: %d: number of notes in a section */
'notesCount' => __( '%d notes', '4wp-todo' ),
'markDone' => __( 'Mark as done', '4wp-todo' ),
'markOpen' => __( 'Mark as open', '4wp-todo' ),
'department' => __( 'Department', '4wp-todo' ),
'selectDepartment' => __( 'Select department', '4wp-todo' ),
'mapTo' => __( 'Map to', '4wp-todo' ),
'team' => __( 'Department', '4wp-todo' ),
'assignee' => __( 'Assignee', '4wp-todo' ),
'selectAssignee' => __( 'Select assignee', '4wp-todo' ),
'createdBy' => __( 'By', '4wp-todo' ),
'assignedTo' => __( '→', '4wp-todo' ),
'unassigned' => __( 'Unassigned', '4wp-todo' ),
'switchToDepartment' => __( 'Switch to department', '4wp-todo' ),
'switchToAssignee' => __( 'Switch to assignee', '4wp-todo' ),
'dueDate' => __( 'Due date', '4wp-todo' ),
'dueDateSoon' => __( 'Due date (coming soon)', '4wp-todo' ),
);
}
/**
* @return void
*/
public static function render_frontend_root(): void {
if ( ! self::is_active() || ! self::is_frontend_context() ) {
return;
}
self::render_root( 'frontend' );
}
/**
* @param string $context frontend|editor|admin
* @return void
*/
private static function render_root( string $context ): void {
$class = 'forwp-todo-frontend-root';
if ( 'editor' === $context ) {
$class .= ' forwp-todo-frontend-root--editor';
} elseif ( 'admin' === $context ) {
$class .= ' forwp-todo-frontend-root--admin';
}
printf(
'',
esc_attr( $class ),
esc_attr__( 'Page notes', '4wp-todo' ),
esc_attr__( 'Page notes — open list, pin to a section', '4wp-todo' )
);
}
/**
* @return string
*/
private static function rest_root_for_browser(): string {
$path = wp_parse_url( rest_url(), PHP_URL_PATH );
return trailingslashit( is_string( $path ) && '' !== $path ? $path : '/wp-json/' );
}
}