From 10559982f463f6f69fd86740516151f95e64dbc8 Mon Sep 17 00:00:00 2001 From: Omar ElHawary Date: Wed, 8 Apr 2026 13:34:15 +0200 Subject: [PATCH 01/73] feat(addons): add members-admin-menus addon - Add addon bootstrap, activator, and app layer for admin menu editing - Load per-role menu rules and integrate with Members addon system Made-with: Cursor --- addons/members-admin-menus/addon.php | 14 + addons/members-admin-menus/app/class-app.php | 46 + .../app/functions-admin.php | 711 +++++++++++++ addons/members-admin-menus/app/functions.php | 942 ++++++++++++++++++ addons/members-admin-menus/bootstrap/app.php | 35 + addons/members-admin-menus/config/app.php | 16 + addons/members-admin-menus/src/Activator.php | 55 + 7 files changed, 1819 insertions(+) create mode 100644 addons/members-admin-menus/addon.php create mode 100644 addons/members-admin-menus/app/class-app.php create mode 100644 addons/members-admin-menus/app/functions-admin.php create mode 100644 addons/members-admin-menus/app/functions.php create mode 100644 addons/members-admin-menus/bootstrap/app.php create mode 100644 addons/members-admin-menus/config/app.php create mode 100644 addons/members-admin-menus/src/Activator.php diff --git a/addons/members-admin-menus/addon.php b/addons/members-admin-menus/addon.php new file mode 100644 index 0000000..4377bea --- /dev/null +++ b/addons/members-admin-menus/addon.php @@ -0,0 +1,14 @@ +$key = $args[ $key ]; + } + } + } +} diff --git a/addons/members-admin-menus/app/functions-admin.php b/addons/members-admin-menus/app/functions-admin.php new file mode 100644 index 0000000..572b2e5 --- /dev/null +++ b/addons/members-admin-menus/app/functions-admin.php @@ -0,0 +1,711 @@ + $cfg ) { + if ( ! is_array( $cfg ) ) { + continue; + } + foreach ( array( 'submenu_order', 'overrides' ) as $sub ) { + if ( isset( $cfg[ $sub ] ) && is_array( $cfg[ $sub ] ) && empty( $cfg[ $sub ] ) ) { + $settings[ $key ][ $id ][ $sub ] = new \stdClass(); + } elseif ( isset( $cfg[ $sub ] ) && is_array( $cfg[ $sub ] ) ) { + $settings[ $key ][ $id ][ $sub ] = (object) $cfg[ $sub ]; + } + } + if ( isset( $cfg['capabilities'] ) && is_array( $cfg['capabilities'] ) ) { + $settings[ $key ][ $id ]['capabilities'] = empty( $cfg['capabilities'] ) ? new \stdClass() : (object) $cfg['capabilities']; + } + } + $settings[ $key ] = (object) $settings[ $key ]; + } + } + if ( isset( $settings['_meta'] ) && is_array( $settings['_meta'] ) ) { + $settings['_meta'] = (object) $settings['_meta']; + } + return $settings; +} + +/** + * Enqueue scripts and styles for the Admin Menus page. + * + * @return void + */ +function enqueue_admin_menus_assets() { + wp_enqueue_media(); + wp_enqueue_style( 'members-admin' ); + wp_enqueue_style( 'wp-color-picker' ); + wp_enqueue_style( + 'members-admin-menus-fa', + 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css', + array(), + '6.5.2' + ); + wp_enqueue_script( 'members-admin-menus' ); + + $settings = get_settings(); + $tree = build_menu_tree_for_js(); + + if ( empty( $settings['_defaults']['captured'] ) && ! empty( $tree ) ) { + $settings['_defaults'] = array( + 'captured' => true, + 'tree' => $tree, + ); + update_option( OPTION_KEY, $settings ); + } + + $roles = array(); + foreach ( \members_get_roles() as $role_obj ) { + $roles[] = array( + 'slug' => $role_obj->name, + 'label' => $role_obj->label, + ); + } + + $role_caps = array(); + foreach ( \members_get_roles() as $role_obj ) { + $wp_role = \get_role( $role_obj->name ); + if ( $wp_role && ! empty( $wp_role->capabilities ) ) { + $role_caps[ $role_obj->name ] = array_keys( array_filter( $wp_role->capabilities ) ); + } else { + $role_caps[ $role_obj->name ] = array(); + } + } + + wp_localize_script( + 'members-admin-menus', + 'membersAdminMenus', + array( + 'menuTree' => $tree, + 'settings' => ensure_objects_for_js( $settings ), + 'roles' => $roles, + 'roleCaps' => $role_caps, + 'adminEditable' => ! empty( $settings['_meta']['admin_editable'] ), + 'nonce' => wp_create_nonce( 'members_admin_menus' ), + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'exportUrl' => add_query_arg( + array( + 'action' => 'members_admin_menus_export', + 'nonce' => wp_create_nonce( 'members_admin_menus' ), + ), + admin_url( 'admin-ajax.php' ) + ), + 'i18n' => array( + 'save' => __( 'Save changes', 'members' ), + 'reset' => __( 'Reset', 'members' ), + 'resetAll' => __( 'Reset all roles', 'members' ), + 'resetRole' => __( 'Reset this role', 'members' ), + 'addItem' => __( 'Add custom item', 'members' ), + 'copyRole' => __( 'Copy from role', 'members' ), + 'import' => __( 'Import', 'members' ), + 'export' => __( 'Export', 'members' ), + 'adminEditable' => __( 'Allow editing administrator menus', 'members' ), + 'adminEditableWarn' => __( 'This can lock administrators out of menus. Continue?', 'members' ), + 'saved' => __( 'Settings saved.', 'members' ), + 'visibility' => __( 'Visibility per role', 'members' ), + 'title' => __( 'Title', 'members' ), + 'url' => __( 'URL', 'members' ), + 'selectRole' => __( 'Select source role', 'members' ), + 'of' => __( 'of', 'members' ), + ), + ) + ); +} + +/** + * Render the standalone Admin Menus page. + * + * @return void + */ +function render_admin_menus_page() { + ?> +
+

+
+ + + + + + + + + + + + + + + + + +
+ +

+ + 🔒 no access +

+ +
+ + + + + + +
+ $menu, + 'submenu' => $submenu, + ); +} + +/** + * Convert a menu slug to a full admin URL. + * + * @param string $slug Menu slug. + * @return string + */ +function slug_to_admin_url( $slug ) { + if ( 0 === strpos( $slug, 'http' ) || 0 === strpos( $slug, '//' ) ) { + return $slug; + } + if ( false !== strpos( $slug, '.php' ) ) { + return admin_url( $slug ); + } + return admin_url( 'admin.php?page=' . $slug ); +} + +/** + * Build hierarchical menu tree for JS (run once snapshot captured). + * + * @return array + */ +function build_menu_tree_for_js() { + global $members_admin_menus_snapshot; + if ( empty( $members_admin_menus_snapshot['menu'] ) || ! is_array( $members_admin_menus_snapshot['menu'] ) ) { + return array(); + } + // Sort by numeric key to match WordPress's rendering order (ksort). + $sorted_menu = $members_admin_menus_snapshot['menu']; + ksort( $sorted_menu ); + $tree = array(); + foreach ( $sorted_menu as $key => $item ) { + if ( ! isset( $item[2] ) ) { + continue; + } + $slug = $item[2]; + if ( false !== strpos( $slug, 'separator' ) || false !== strpos( $slug, 'wp-menu-separator' ) ) { + continue; + } + $raw = isset( $item[0] ) ? $item[0] : $slug; + $title = trim( wp_strip_all_tags( preg_replace( '/ ?]*class="[^"]*(?:update-plugins|awaiting-mod|pending-count|count-\d|menu-counter)[^"]*"[^>]*>.*<\/span>/si', '', $raw ) ) ); + // Skip top-level items with empty titles (hidden/internal pages). + if ( '' === $title ) { + continue; + } + $icon = isset( $item[6] ) ? $item[6] : ''; + $icon_type = 'dashicon'; + if ( '' === $icon || 'none' === $icon || 'div' === $icon ) { + $icon_type = 'none'; + $icon = ''; + } elseif ( 0 === strpos( $icon, 'data:image/' ) ) { + $icon_type = 'svg'; + } elseif ( 0 === strpos( $icon, 'http' ) || 0 === strpos( $icon, '//' ) ) { + $icon_type = 'image'; + } + $node = array( + 'id' => $slug, + 'title' => $title, + 'url' => slug_to_admin_url( $slug ), + 'icon' => $icon, + 'icon_type' => $icon_type, + 'type' => 'top', + 'cap' => isset( $item[1] ) ? $item[1] : 'read', + 'children' => array(), + ); + if ( ! empty( $members_admin_menus_snapshot['submenu'][ $slug ] ) ) { + $sorted_subs = $members_admin_menus_snapshot['submenu'][ $slug ]; + ksort( $sorted_subs ); + $seen_subslugs = array(); + foreach ( $sorted_subs as $subitem ) { + if ( empty( $subitem[2] ) ) { + continue; + } + $subslug = $subitem[2]; + // Skip the redundant first child that matches the parent slug (WP convention). + if ( $subslug === $slug ) { + continue; + } + // Skip duplicate submenu slugs (some plugins register multiple entries for the same slug). + if ( isset( $seen_subslugs[ $subslug ] ) ) { + continue; + } + $seen_subslugs[ $subslug ] = true; + // Compute the clean title. + $sub_raw = isset( $subitem[0] ) ? $subitem[0] : ''; + $sub_title = trim( wp_strip_all_tags( preg_replace( '/ ?]*class="[^"]*(?:update-plugins|awaiting-mod|pending-count|count-\d|menu-counter)[^"]*"[^>]*>.*<\/span>/si', '', $sub_raw ) ) ); + // Skip items with empty titles — these are hidden/internal pages registered by plugins. + if ( '' === $sub_title ) { + continue; + } + $cid = $slug . '::' . $subslug; + $node['children'][] = array( + 'id' => $cid, + 'title' => $sub_title, + 'url' => slug_to_admin_url( $subslug ), + 'type' => 'sub', + 'cap' => isset( $subitem[1] ) ? $subitem[1] : 'read', + ); + } + } + $tree[] = $node; + } + return $tree; +} + +/** + * AJAX: save full settings JSON. + * + * @return void + */ +function ajax_save_settings() { + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'members_admin_menus' ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid security token.', 'members' ) ), 403 ); + } + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'members' ) ), 403 ); + } + $raw = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : ''; + if ( is_string( $raw ) ) { + $data = json_decode( $raw, true ); + } else { + $data = $raw; + } + if ( ! is_array( $data ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid data.', 'members' ) ), 400 ); + } + $sanitized = sanitize_settings_payload( $data ); + update_option( OPTION_KEY, $sanitized ); + wp_send_json_success( array( 'message' => __( 'Settings saved.', 'members' ) ) ); +} + +/** + * Sanitize settings array from client. + * + * @param array $data Raw data. + * @return array + */ +function sanitize_settings_payload( $data ) { + if ( ! is_array( $data ) ) { + return get_default_settings(); + } + + $defaults = get_default_settings(); + $allowed = array( '_meta', 'roles', 'users', 'custom_items', 'capabilities', '_defaults' ); + $filtered = array(); + foreach ( $allowed as $key ) { + if ( array_key_exists( $key, $data ) ) { + $filtered[ $key ] = $data[ $key ]; + } + } + + $out = wp_parse_args( $filtered, $defaults ); + + $out['_meta'] = array( + 'version' => isset( $out['_meta']['version'] ) ? absint( $out['_meta']['version'] ) : 3, + 'admin_editable' => ! empty( $out['_meta']['admin_editable'] ), + ); + + if ( isset( $out['_defaults'] ) && is_array( $out['_defaults'] ) ) { + $d = $out['_defaults']; + $out['_defaults'] = array( + 'captured' => ! empty( $d['captured'] ), + ); + if ( isset( $d['tree'] ) && is_array( $d['tree'] ) ) { + $out['_defaults']['tree'] = $d['tree']; + } + } + + if ( isset( $out['roles'] ) && is_array( $out['roles'] ) ) { + $roles = array(); + foreach ( $out['roles'] as $role_key => $cfg ) { + $sanitized = sanitize_key( $role_key ); + if ( ! $sanitized ) { + continue; + } + $roles[ $sanitized ] = sanitize_role_config( $cfg ); + } + $out['roles'] = $roles; + } + + if ( isset( $out['users'] ) && is_array( $out['users'] ) ) { + $users = array(); + foreach ( $out['users'] as $uid => $cfg ) { + $uid = absint( $uid ); + if ( $uid < 1 ) { + continue; + } + $users[ $uid ] = sanitize_role_config( $cfg ); + } + $out['users'] = $users; + } + + if ( isset( $out['custom_items'] ) && is_array( $out['custom_items'] ) ) { + $out['custom_items'] = array_map( __NAMESPACE__ . '\sanitize_custom_item', $out['custom_items'] ); + } + + if ( isset( $out['capabilities'] ) && is_array( $out['capabilities'] ) ) { + $caps = array(); + foreach ( $out['capabilities'] as $slug => $cap ) { + $caps[ sanitize_text_field( $slug ) ] = sanitize_key( $cap ); + } + $out['capabilities'] = $caps; + } + + return $out; +} + +/** + * Sanitize role or user config block. + * + * @param mixed $cfg Config. + * @return array + */ +function sanitize_role_config( $cfg ) { + if ( ! is_array( $cfg ) ) { + return array(); + } + $out = array(); + if ( isset( $cfg['hidden'] ) && is_array( $cfg['hidden'] ) ) { + $out['hidden'] = array_map( 'sanitize_text_field', $cfg['hidden'] ); + } + if ( isset( $cfg['order'] ) && is_array( $cfg['order'] ) ) { + $out['order'] = array_map( 'sanitize_text_field', $cfg['order'] ); + } + if ( isset( $cfg['submenu_order'] ) && is_array( $cfg['submenu_order'] ) ) { + $so = array(); + foreach ( $cfg['submenu_order'] as $parent => $children ) { + $p = sanitize_text_field( $parent ); + if ( ! $p || ! is_array( $children ) ) { + continue; + } + $so[ $p ] = array_map( 'sanitize_text_field', $children ); + } + $out['submenu_order'] = $so; + } + if ( isset( $cfg['capabilities'] ) && is_array( $cfg['capabilities'] ) ) { + $out['capabilities'] = array(); + foreach ( $cfg['capabilities'] as $slug => $cap ) { + $s = sanitize_text_field( $slug ); + if ( ! $s ) { + continue; + } + $out['capabilities'][ $s ] = sanitize_key( $cap ); + } + } + if ( isset( $cfg['overrides'] ) && is_array( $cfg['overrides'] ) ) { + $out['overrides'] = array(); + foreach ( $cfg['overrides'] as $slug => $ov ) { + $s = sanitize_text_field( $slug ); + if ( ! $s || ! is_array( $ov ) ) { + continue; + } + $out['overrides'][ $s ] = array( + 'label' => isset( $ov['label'] ) ? sanitize_text_field( $ov['label'] ) : '', + 'icon_type' => isset( $ov['icon_type'] ) ? sanitize_key( $ov['icon_type'] ) : '', + 'icon' => isset( $ov['icon'] ) ? sanitize_text_field( $ov['icon'] ) : '', + 'url' => isset( $ov['url'] ) ? esc_url_raw( $ov['url'] ) : '', + 'color_bg' => isset( $ov['color_bg'] ) ? sanitize_hex_color( $ov['color_bg'] ) : '', + 'color_text' => isset( $ov['color_text'] ) ? sanitize_hex_color( $ov['color_text'] ) : '', + 'color_icon' => isset( $ov['color_icon'] ) ? sanitize_hex_color( $ov['color_icon'] ) : '', + 'parent' => isset( $ov['parent'] ) ? sanitize_text_field( $ov['parent'] ) : '', + ); + } + } + return $out; +} + +/** + * Sanitize one custom menu item. + * + * @param mixed $item Item. + * @return array + */ +function sanitize_custom_item( $item ) { + if ( ! is_array( $item ) ) { + return array(); + } + return array( + 'id' => isset( $item['id'] ) ? sanitize_key( $item['id'] ) : wp_unique_id( 'c' ), + 'label' => isset( $item['label'] ) ? sanitize_text_field( $item['label'] ) : '', + 'url' => isset( $item['url'] ) ? esc_url_raw( $item['url'] ) : '', + 'icon_type' => isset( $item['icon_type'] ) ? sanitize_key( $item['icon_type'] ) : 'dashicon', + 'icon' => isset( $item['icon'] ) ? sanitize_text_field( $item['icon'] ) : 'dashicons-admin-generic', + 'parent' => isset( $item['parent'] ) ? sanitize_text_field( $item['parent'] ) : '', + 'position' => isset( $item['position'] ) ? absint( $item['position'] ) : 99, + 'cap' => isset( $item['cap'] ) ? sanitize_key( $item['cap'] ) : 'read', + ); +} + +/** + * AJAX reset. + * + * @return void + */ +function ajax_reset_settings() { + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'members_admin_menus' ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid security token.', 'members' ) ), 403 ); + } + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'members' ) ), 403 ); + } + $scope = isset( $_POST['scope'] ) ? sanitize_key( wp_unslash( $_POST['scope'] ) ) : 'all'; + $role = isset( $_POST['role'] ) ? sanitize_key( wp_unslash( $_POST['role'] ) ) : ''; + $settings = get_settings(); + if ( 'role' === $scope && $role ) { + unset( $settings['roles'][ $role ] ); + } else { + $settings['roles'] = array(); + $settings['users'] = array(); + $settings['custom_items'] = array(); + $settings['capabilities'] = array(); + } + update_option( OPTION_KEY, $settings ); + wp_send_json_success( array( 'message' => __( 'Reset complete.', 'members' ) ) ); +} + +/** + * Export JSON download via AJAX redirect or direct. + * + * @return void + */ +function ajax_export_settings() { + if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'members_admin_menus' ) ) { + wp_die( esc_html__( 'Invalid security token.', 'members' ) ); + } + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'Permission denied.', 'members' ) ); + } + $data = get_settings(); + nocache_headers(); + header( 'Content-Type: application/json; charset=utf-8' ); + header( 'Content-Disposition: attachment; filename="members-admin-menus-export.json"' ); + echo wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE ); + exit; +} + +/** + * Import settings from uploaded JSON. + * + * @return void + */ +function ajax_import_settings() { + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'members_admin_menus' ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid security token.', 'members' ) ), 403 ); + } + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'members' ) ), 403 ); + } + $raw = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : ''; + $data = json_decode( $raw, true ); + if ( ! is_array( $data ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid JSON.', 'members' ) ), 400 ); + } + update_option( OPTION_KEY, sanitize_settings_payload( $data ) ); + wp_send_json_success( array( 'message' => __( 'Settings imported.', 'members' ) ) ); +} + +/** + * User search for per-user column (Phase 3 UI). + * + * @return void + */ +function ajax_user_search() { + if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'members_admin_menus' ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid security token.', 'members' ) ), 403 ); + } + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'members' ) ), 403 ); + } + $term = isset( $_GET['term'] ) ? sanitize_text_field( wp_unslash( $_GET['term'] ) ) : ''; + if ( strlen( $term ) < 2 ) { + wp_send_json_success( array() ); + } + $query = new \WP_User_Query( + array( + 'number' => 20, + 'search' => '*' . $term . '*', + 'search_columns' => array( 'user_login', 'user_nicename', 'user_email', 'display_name' ), + 'fields' => array( 'ID', 'user_login', 'display_name' ), + ) + ); + $out = array(); + foreach ( $query->get_results() as $u ) { + $user_obj = get_userdata( $u->ID ); + $out[] = array( + 'id' => (int) $u->ID, + 'label' => $u->display_name . ' (' . $u->user_login . ')', + 'roles' => $user_obj ? $user_obj->roles : array(), + ); + } + wp_send_json_success( $out ); +} diff --git a/addons/members-admin-menus/app/functions.php b/addons/members-admin-menus/app/functions.php new file mode 100644 index 0000000..52b8590 --- /dev/null +++ b/addons/members-admin-menus/app/functions.php @@ -0,0 +1,942 @@ + $child_order ) { + if ( ! isset( $submenu[ $parent ] ) || ! is_array( $child_order ) ) { + continue; + } + $submenu[ $parent ] = reorder_submenu_items( $submenu[ $parent ], $child_order ); + } + } + + // Phase 2: label, icon, URL, colors. + if ( ! empty( $cfg['overrides'] ) && is_array( $cfg['overrides'] ) ) { + apply_menu_overrides( $cfg['overrides'] ); + apply_color_overrides( $cfg['overrides'] ); + apply_level_moves( $cfg['overrides'] ); + } + + // Phase 3: capability-based hiding (independent of role hidden lists). + $cap_map = isset( $cfg['capabilities'] ) ? $cfg['capabilities'] : array(); + if ( ! empty( $cap_map ) && is_array( $cap_map ) ) { + foreach ( $cap_map as $slug => $cap ) { + $slug = sanitize_text_field( $slug ); + $cap = sanitize_key( $cap ); + if ( ! $slug || ! $cap || current_user_can( $cap ) || members_admin_menus_is_protected_slug( $slug ) ) { + continue; + } + if ( false !== strpos( $slug, '::' ) ) { + $parts = explode( '::', $slug, 2 ); + if ( count( $parts ) === 2 ) { + remove_submenu_page( $parts[0], $parts[1] ); + } + } else { + remove_menu_page( $slug ); + } + } + } + + // Hide items (last). + $hidden = isset( $cfg['hidden'] ) ? $cfg['hidden'] : array(); + if ( empty( $hidden ) || ! is_array( $hidden ) ) { + return; + } + + foreach ( $hidden as $slug ) { + $slug = sanitize_text_field( $slug ); + if ( ! $slug || members_admin_menus_is_protected_slug( $slug ) ) { + continue; + } + if ( false !== strpos( $slug, '::' ) ) { + $parts = explode( '::', $slug, 2 ); + if ( count( $parts ) === 2 ) { + remove_submenu_page( $parts[0], $parts[1] ); + } + } else { + remove_menu_page( $slug ); + } + } +} + +/** + * Late registration for custom menu entries (after core menus). + * + * @return void + */ +function inject_custom_menu_items_late() { + if ( ! is_admin() || ! get_current_user_id() || is_user_exempt( get_current_user_id() ) ) { + return; + } + $cfg = get_resolved_config_for_user( get_current_user_id() ); + if ( empty( $cfg['custom_items'] ) ) { + return; + } + inject_custom_menu_items( $cfg['custom_items'] ); +} + +/** + * Reorder $menu array by slug list. + * + * @param array $menu Admin menu global. + * @param array $order Ordered slugs. + * @return array + */ +function reorder_menu_by_slug_list( $menu, $order ) { + $by_slug = array(); + foreach ( $menu as $key => $item ) { + if ( isset( $item[2] ) ) { + $by_slug[ $item[2] ] = $item; + } + } + $new = array(); + $used = array(); + $pos = 1; + $order = array_map( 'strval', $order ); + foreach ( $order as $slug ) { + if ( ! isset( $by_slug[ $slug ] ) || isset( $used[ $slug ] ) ) { + continue; + } + $new[ $pos ] = $by_slug[ $slug ]; + $used[ $slug ] = true; + $pos++; + } + // Append items not in our order list. + foreach ( $menu as $k => $item ) { + if ( isset( $item[2] ) && ! isset( $used[ $item[2] ] ) ) { + $new[ $pos ] = $item; + $pos++; + } elseif ( ! isset( $item[2] ) ) { + // Separators and other items without a slug. + $new[ $pos ] = $item; + $pos++; + } + } + return $new; +} + +/** + * Reorder submenu items array. + * + * @param array $items Submenu items. + * @param array $child_order Slugs in order. + * @return array + */ +function reorder_submenu_items( $items, $child_order ) { + $by_slug = array(); + foreach ( $items as $idx => $item ) { + if ( isset( $item[2] ) ) { + $by_slug[ $item[2] ] = array( 'idx' => $idx, 'item' => $item ); + } + } + $new = array(); + $seen = array(); + foreach ( $child_order as $slug ) { + if ( isset( $by_slug[ $slug ] ) ) { + $i = $by_slug[ $slug ]['idx']; + if ( ! isset( $seen[ $i ] ) ) { + $new[] = $by_slug[ $slug ]['item']; + $seen[ $i ] = true; + } + } + } + foreach ( $items as $idx => $item ) { + if ( empty( $seen[ $idx ] ) ) { + $new[] = $item; + } + } + return $new; +} + +/** + * Inject real WordPress separator entries into $menu based on sep-* tokens + * in the order array. + * + * Walks through the order array and, whenever a sep-* token is encountered, + * inserts a proper WP separator entry at the corresponding position in $menu. + * + * @param array $menu The admin menu array (already reordered by slug list). + * @param array $order The full order array including sep-* tokens. + * @return array Modified menu array with separators injected. + */ +function inject_separators( $menu, $order ) { + $sep_positions = array(); + $real_idx = 0; + + foreach ( $order as $token ) { + $token = (string) $token; + if ( 0 === strpos( $token, 'sep-' ) ) { + $sep_positions[] = $real_idx; + } else { + $real_idx++; + } + } + + if ( empty( $sep_positions ) ) { + return $menu; + } + + $items = array_values( $menu ); + $result = array(); + $pos = 1; + $item_i = 0; + $sep_i = 0; + $total = count( $items ); + $placed = 0; + + for ( $slot = 0; $placed < $total || $sep_i < count( $sep_positions ); $slot++ ) { + if ( $sep_i < count( $sep_positions ) && $sep_positions[ $sep_i ] === $item_i ) { + $result[ $pos ] = array( + '', + 'read', + 'separator-members-am-' . $sep_i, + '', + 'wp-menu-separator', + ); + $pos++; + $sep_i++; + } elseif ( $item_i < $total ) { + $result[ $pos ] = $items[ $item_i ]; + $pos++; + $item_i++; + $placed++; + } else { + break; + } + } + + while ( $item_i < $total ) { + $result[ $pos ] = $items[ $item_i ]; + $pos++; + $item_i++; + } + + return $result; +} + +/** + * Apply label, icon, URL, colors to menu globals. + * + * @param array $overrides Overrides keyed by canonical slug. + * @return void + */ +function apply_menu_overrides( $overrides ) { + global $menu, $submenu; + $fa_icons = array(); + + foreach ( $menu as $k => $item ) { + if ( empty( $item[2] ) ) { + continue; + } + $slug = $item[2]; + if ( empty( $overrides[ $slug ] ) || ! is_array( $overrides[ $slug ] ) ) { + continue; + } + $o = $overrides[ $slug ]; + if ( ! empty( $o['label'] ) ) { + $menu[ $k ][0] = wp_strip_all_tags( $o['label'] ); + } + if ( ! empty( $o['url'] ) ) { + $menu[ $k ][2] = esc_url_raw( $o['url'] ); + } + if ( ! empty( $o['icon_type'] ) && ! empty( $o['icon'] ) ) { + $icon_type = $o['icon_type']; + $icon = $o['icon']; + + if ( 'dashicon' === $icon_type ) { + $menu[ $k ][6] = sanitize_text_field( $icon ); + } elseif ( 'fontawesome' === $icon_type ) { + $menu[ $k ][6] = 'none'; + $id = isset( $item[5] ) ? sanitize_html_class( $item[5] ) : ''; + if ( $id ) { + $fa_icons[ $id ] = esc_attr( $icon ); + } + } elseif ( 'custom' === $icon_type || 'image' === $icon_type ) { + $menu[ $k ][6] = esc_url( $icon ); + } + } + } + + foreach ( $submenu as $parent => $items ) { + foreach ( $items as $idx => $item ) { + if ( empty( $item[2] ) ) { + continue; + } + $canon = $parent . '::' . $item[2]; + if ( empty( $overrides[ $canon ] ) || ! is_array( $overrides[ $canon ] ) ) { + continue; + } + $o = $overrides[ $canon ]; + if ( ! empty( $o['label'] ) ) { + $submenu[ $parent ][ $idx ][0] = wp_strip_all_tags( $o['label'] ); + } + if ( ! empty( $o['url'] ) ) { + $submenu[ $parent ][ $idx ][2] = esc_url_raw( $o['url'] ); + } + } + } + + if ( ! empty( $fa_icons ) ) { + $GLOBALS['members_am_fa_icons'] = $fa_icons; + add_action( 'admin_head', __NAMESPACE__ . '\output_fa_icon_styles', 998 ); + } +} + +/** + * Output CSS + HTML to render Font Awesome icons in the admin sidebar. + * + * Hides the default dashicon/image and places the FA icon via ::before pseudo-element. + * + * @return void + */ +function output_fa_icon_styles() { + if ( empty( $GLOBALS['members_am_fa_icons'] ) ) { + return; + } + $css = ''; + $js = ''; + foreach ( $GLOBALS['members_am_fa_icons'] as $menu_id => $fa_class ) { + $sel = '#adminmenu #' . $menu_id . ' .wp-menu-image'; + $css .= $sel . ':before { content: "" !important; }' . "\n"; + $css .= $sel . ' img { display: none !important; }' . "\n"; + $css .= $sel . ' .members-am-fa { font-size: 20px; line-height: 1; }' . "\n"; + $js .= 'jQuery("#' . esc_js( $menu_id ) . ' .wp-menu-image").html(\'\');' . "\n"; + } + echo '\n"; + echo '' . "\n"; +} + +/** + * Move items between menu levels based on 'parent' override field. + * + * - If a submenu item has parent = '' (empty string), promote it to top-level. + * - If a top-level item has a parent slug set, demote it to a submenu of that parent. + * + * @param array $overrides Overrides keyed by canonical slug. + * @return void + */ +function apply_level_moves( $overrides ) { + global $menu, $submenu; + + foreach ( $overrides as $slug => $o ) { + if ( ! is_array( $o ) || ! array_key_exists( 'parent', $o ) ) { + continue; + } + $target_parent = $o['parent']; + $is_submenu = ( false !== strpos( $slug, '::' ) ); + + if ( $is_submenu && '' === $target_parent ) { + $parts = explode( '::', $slug, 2 ); + if ( count( $parts ) !== 2 ) { + continue; + } + $old_parent = $parts[0]; + $child_slug = $parts[1]; + + $label = $child_slug; + $cap = 'read'; + if ( isset( $submenu[ $old_parent ] ) && is_array( $submenu[ $old_parent ] ) ) { + foreach ( $submenu[ $old_parent ] as $idx => $sub ) { + if ( isset( $sub[2] ) && $sub[2] === $child_slug ) { + $label = $sub[0]; + $cap = isset( $sub[1] ) ? $sub[1] : 'read'; + unset( $submenu[ $old_parent ][ $idx ] ); + break; + } + } + } + if ( ! empty( $o['label'] ) ) { + $label = $o['label']; + } + $icon = 'dashicons-admin-generic'; + if ( ! empty( $o['icon'] ) ) { + $icon = $o['icon']; + } + add_menu_page( wp_strip_all_tags( $label ), wp_strip_all_tags( $label ), $cap, $child_slug, '', $icon ); + + } elseif ( ! $is_submenu && is_string( $target_parent ) && '' !== $target_parent ) { + $found_key = false; + $found_item = null; + foreach ( $menu as $k => $item ) { + if ( isset( $item[2] ) && $item[2] === $slug ) { + $found_key = $k; + $found_item = $item; + break; + } + } + if ( false === $found_key ) { + continue; + } + $label = isset( $found_item[0] ) ? $found_item[0] : $slug; + $cap = isset( $found_item[1] ) ? $found_item[1] : 'read'; + if ( ! empty( $o['label'] ) ) { + $label = $o['label']; + } + remove_menu_page( $slug ); + add_submenu_page( $target_parent, wp_strip_all_tags( $label ), wp_strip_all_tags( $label ), $cap, $slug ); + } + } +} + +/** + * Apply color overrides via admin_head CSS rules. + * + * Instead of wrapping titles in styled spans (which only affects text), + * this injects a \n"; + } +} + +/** + * Register custom top/sub menu items from stored config. + * + * @param array $items Custom items. + * @return void + */ +function inject_custom_menu_items( $items ) { + if ( ! is_array( $items ) ) { + return; + } + global $members_am_custom_redirects; + if ( ! is_array( $members_am_custom_redirects ) ) { + $members_am_custom_redirects = array(); + } + foreach ( $items as $item ) { + if ( empty( $item['id'] ) || empty( $item['label'] ) ) { + continue; + } + $cap = ! empty( $item['cap'] ) ? $item['cap'] : 'read'; + $url = ! empty( $item['url'] ) ? esc_url_raw( $item['url'] ) : admin_url(); + $hook = 'members-am-' . sanitize_key( $item['id'] ); + $members_am_custom_redirects[ $hook ] = $url; + if ( empty( $item['parent'] ) ) { + add_menu_page( + $item['label'], + $item['label'], + $cap, + $hook, + __NAMESPACE__ . '\members_am_custom_menu_callback', + ! empty( $item['icon'] ) ? $item['icon'] : 'dashicons-admin-generic', + isset( $item['position'] ) ? (int) $item['position'] : null + ); + } else { + add_submenu_page( + $item['parent'], + $item['label'], + $item['label'], + $cap, + $hook, + __NAMESPACE__ . '\members_am_custom_menu_callback' + ); + } + } +} + +/** + * Redirects custom menu items to their target URL. + * + * @return void + */ +function members_am_custom_menu_callback() { + global $members_am_custom_redirects; + if ( empty( $_GET['page'] ) || ! is_array( $members_am_custom_redirects ) ) { + return; + } + $page = sanitize_key( wp_unslash( $_GET['page'] ) ); + if ( isset( $members_am_custom_redirects[ $page ] ) ) { + wp_safe_redirect( $members_am_custom_redirects[ $page ] ); + exit; + } +} + +/** + * Block direct access to hidden admin pages. + * + * @return void + */ +function block_restricted_pages() { + if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) { + return; + } + if ( isset( $_GET['page'] ) && 'members-settings' === sanitize_key( wp_unslash( $_GET['page'] ) ) ) { + return; + } + $user_id = get_current_user_id(); + if ( ! $user_id || is_user_exempt( $user_id ) ) { + return; + } + + $cfg = get_resolved_config_for_user( $user_id ); + + $cap_map = isset( $cfg['capabilities'] ) ? $cfg['capabilities'] : array(); + if ( ! empty( $cap_map ) && is_array( $cap_map ) ) { + $current = get_current_screen_slugs(); + foreach ( $current as $cslug ) { + if ( members_admin_menus_is_protected_slug( $cslug ) ) { + continue; + } + foreach ( $cap_map as $slug => $cap ) { + if ( ! $slug || ! $cap || current_user_can( $cap ) ) { + continue; + } + if ( $slug === $cslug || members_admin_menus_slug_matches( $cslug, $slug ) ) { + $url = apply_filters( app()->namespace . '/redirect_url', admin_url(), $user_id ); + wp_safe_redirect( $url ); + exit; + } + } + } + } + + $hidden = isset( $cfg['hidden'] ) ? $cfg['hidden'] : array(); + if ( empty( $hidden ) || ! is_array( $hidden ) ) { + return; + } + + $current = get_current_screen_slugs(); + foreach ( $current as $cslug ) { + if ( members_admin_menus_is_protected_slug( $cslug ) ) { + continue; + } + foreach ( $hidden as $h ) { + if ( $h === $cslug || members_admin_menus_slug_matches( $cslug, $h ) ) { + $url = apply_filters( app()->namespace . '/redirect_url', admin_url(), $user_id ); + wp_safe_redirect( $url ); + exit; + } + } + } +} + +/** + * Loose match for submenu vs top-level. + * + * @param string $current Current screen id. + * @param string $stored Stored hidden id. + * @return bool + */ +function members_admin_menus_slug_matches( $current, $stored ) { + if ( $stored === $current ) { + return true; + } + if ( false !== strpos( $stored, '::' ) ) { + $parts = explode( '::', $stored, 2 ); + if ( isset( $parts[1] ) && ( $parts[1] === $current || false !== strpos( $current, $parts[1] ) ) ) { + return true; + } + } + return false; +} + +/** + * Build list of slug identifiers for the current admin screen. + * + * @return array + */ +function get_current_screen_slugs() { + global $pagenow; + $slugs = array(); + + if ( ! empty( $_GET['page'] ) && is_string( $_GET['page'] ) ) { + $page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); + $slugs[] = $page; + if ( ! empty( $pagenow ) ) { + $slugs[] = $pagenow . '?page=' . $page; + } + } + + if ( ! empty( $_GET['post_type'] ) && in_array( $pagenow, array( 'edit.php', 'post-new.php', 'post.php' ), true ) ) { + $pt = sanitize_key( wp_unslash( $_GET['post_type'] ) ); + $slugs[] = 'edit.php?post_type=' . $pt; + } elseif ( ! empty( $pagenow ) && empty( $_GET['page'] ) ) { + $slugs[] = $pagenow; + } + + if ( ! empty( $_GET['taxonomy'] ) && in_array( $pagenow, array( 'edit-tags.php', 'term.php' ), true ) ) { + $tax = sanitize_key( wp_unslash( $_GET['taxonomy'] ) ); + $slugs[] = 'edit-tags.php?taxonomy=' . $tax; + } + + // Also block related pages when a top-level menu is hidden. + // e.g. if edit.php is hidden, also block post-new.php and post.php (for default post type). + if ( in_array( $pagenow, array( 'post-new.php', 'post.php' ), true ) && empty( $_GET['post_type'] ) ) { + $slugs[] = 'edit.php'; + } + + /** + * Filter current screen slug list for URL blocking. + * + * @param array $slugs Slugs. + */ + return array_unique( array_filter( apply_filters( app()->namespace . '/current_screen_slugs', $slugs ) ) ); +} + +/** + * Slugs that cannot be hidden (Members settings / safety). + * + * @param string $slug Slug. + * @return bool + */ +function members_admin_menus_is_protected_slug( $slug ) { + $s = (string) $slug; + return ( false !== stripos( $s, 'members-settings' ) || false !== stripos( $s, 'page=members' ) ); +} + +/** + * Default option structure. + * + * @return array + */ +function get_default_settings() { + return array( + '_meta' => array( + 'version' => 3, + 'admin_editable' => false, + ), + 'roles' => array(), + 'users' => array(), + 'custom_items' => array(), + 'capabilities' => array(), + '_defaults' => array( + 'captured' => false, + ), + ); +} + +/** + * Get plugin settings. + * + * @return array + */ +function get_settings() { + static $cache = null; + if ( null !== $cache ) { + return $cache; + } + $settings = get_option( OPTION_KEY, array() ); + if ( ! is_array( $settings ) ) { + $settings = array(); + } + $cache = wp_parse_args( $settings, get_default_settings() ); + return $cache; +} + +/** + * Whether user is exempt from all restrictions. + * + * @param int $user_id User ID. + * @return bool + */ +function is_user_exempt( $user_id ) { + $user = get_userdata( $user_id ); + if ( ! $user ) { + return true; + } + if ( is_multisite() && is_super_admin( $user_id ) ) { + return true; + } + + $meta = get_settings(); + $admin_editable = ! empty( $meta['_meta']['admin_editable'] ); + + if ( in_array( 'administrator', (array) $user->roles, true ) && ! $admin_editable ) { + return true; + } + + return (bool) apply_filters( app()->namespace . '/is_user_exempt', false, $user_id ); +} + +/** + * Resolved config for a user: roles merged + user overrides + hidden intersect + merged order. + * + * @param int $user_id User ID. + * @return array + */ +function get_resolved_config_for_user( $user_id ) { + $settings = get_settings(); + $user = get_userdata( $user_id ); + if ( ! $user ) { + return array(); + } + + $roles = (array) $user->roles; + sort( $roles ); + + $base = get_resolved_config_for_user_from_roles_only( $settings, $roles ); + + // Phase 3: user-specific overrides replace role-merged blocks. + if ( ! empty( $settings['users'][ $user_id ] ) && is_array( $settings['users'][ $user_id ] ) ) { + $u = $settings['users'][ $user_id ]; + foreach ( array( 'hidden', 'order', 'submenu_order', 'overrides', 'custom_items', 'capabilities' ) as $k ) { + if ( isset( $u[ $k ] ) ) { + $base[ $k ] = $u[ $k ]; + } + } + } + + return $base; +} + +/** + * Resolve hidden from roles only (for merge helper). + * + * @param array $settings Settings. + * @param array $roles Role slugs. + * @return array + */ +function get_resolved_config_for_user_from_roles_only( $settings, $roles ) { + sort( $roles ); + $merged_hidden = array(); + $first = true; + foreach ( $roles as $role ) { + $rh = isset( $settings['roles'][ $role ]['hidden'] ) ? (array) $settings['roles'][ $role ]['hidden'] : array(); + if ( $first ) { + $merged_hidden = $rh; + $first = false; + } else { + $merged_hidden = array_values( array_intersect( $merged_hidden, $rh ) ); + } + } + $order = array(); + $submenu_order = array(); + $overrides = array(); + foreach ( $roles as $role ) { + if ( empty( $settings['roles'][ $role ] ) ) { + continue; + } + $r = $settings['roles'][ $role ]; + if ( ! empty( $r['order'] ) && empty( $order ) ) { + $order = (array) $r['order']; + } + if ( ! empty( $r['submenu_order'] ) && empty( $submenu_order ) ) { + $submenu_order = (array) $r['submenu_order']; + } + } + foreach ( $roles as $role ) { + if ( ! empty( $settings['roles'][ $role ]['overrides'] ) ) { + $overrides = array_merge( $overrides, (array) $settings['roles'][ $role ]['overrides'] ); + } + } + return array( + 'hidden' => $merged_hidden, + 'order' => $order, + 'submenu_order' => $submenu_order, + 'overrides' => $overrides, + 'custom_items' => isset( $settings['custom_items'] ) ? (array) $settings['custom_items'] : array(), + 'capabilities' => isset( $settings['capabilities'] ) ? (array) $settings['capabilities'] : array(), + ); +} diff --git a/addons/members-admin-menus/bootstrap/app.php b/addons/members-admin-menus/bootstrap/app.php new file mode 100644 index 0000000..3fe8030 --- /dev/null +++ b/addons/members-admin-menus/bootstrap/app.php @@ -0,0 +1,35 @@ +dir . 'app/functions.php'; + +if ( is_admin() ) { + require_once app()->dir . 'app/functions-admin.php'; +} diff --git a/addons/members-admin-menus/config/app.php b/addons/members-admin-menus/config/app.php new file mode 100644 index 0000000..e3acfbc --- /dev/null +++ b/addons/members-admin-menus/config/app.php @@ -0,0 +1,16 @@ + trailingslashit( realpath( trailingslashit( __DIR__ ) . '../' ) ), + 'namespace' => 'members/addons/admin_menus', +); diff --git a/addons/members-admin-menus/src/Activator.php b/addons/members-admin-menus/src/Activator.php new file mode 100644 index 0000000..5624de5 --- /dev/null +++ b/addons/members-admin-menus/src/Activator.php @@ -0,0 +1,55 @@ + Add-Ons. + * + * @package Members + * @subpackage AddOns + */ + +namespace Members\AddOns\AdminMenus; + +defined( 'ABSPATH' ) || exit; + +/** + * Activator. + */ +class Activator { + + /** + * Default option version. + */ + const OPTION_VERSION = 3; + + /** + * On activation, seed empty settings if missing. + * + * @return void + */ + public static function activate() { + $existing = get_option( 'members_admin_menus_settings', null ); + if ( null === $existing || ! is_array( $existing ) ) { + update_option( 'members_admin_menus_settings', self::get_default_option() ); + } + } + + /** + * Default empty structure. + * + * @return array + */ + public static function get_default_option() { + return array( + '_meta' => array( + 'version' => self::OPTION_VERSION, + 'admin_editable' => false, + ), + 'roles' => array(), + 'users' => array(), + 'custom_items' => array(), + 'capabilities' => array(), + '_defaults' => array( + 'captured' => false, + ), + ); + } +} From 0ba5b5774b248fcbe459aeeaae081b3ac1f81512 Mon Sep 17 00:00:00 2001 From: Omar ElHawary Date: Wed, 8 Apr 2026 13:34:20 +0200 Subject: [PATCH 02/73] feat(admin): register admin menus addon and settings view fallback - Register members-admin-menus in addon catalog with title and excerpt - Register members-admin-menus script handle with dependencies and filemtime - Fall back to General settings view when the requested view slug is missing Made-with: Cursor --- admin/class-settings.php | 13 ++++++++++++- admin/config/addons.php | 6 ++++++ admin/functions-admin.php | 10 ++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/admin/class-settings.php b/admin/class-settings.php index 4ff107e..24da95e 100644 --- a/admin/class-settings.php +++ b/admin/class-settings.php @@ -371,7 +371,18 @@ public function settings_page() { ?>
filter_links(); ?>
- get_view( members_get_current_settings_view() )->template(); ?> + get_view( members_get_current_settings_view() ); + if ( $_current_view ) { + $_current_view->template(); + } else { + // Fallback to General view if requested view doesn't exist. + $_fallback_view = $this->get_view( 'general' ); + if ( $_fallback_view ) { + $_fallback_view->template(); + } + } + ?> 'Allows site administrators to control which users have access to the WordPress admin via role.', ), + 'members-admin-menus' => array( + 'url' => '', + 'title' => 'Members - Admin Menus', + 'excerpt' => 'Full admin menu editor: hide, reorder, rename, and customize menu items per role with a visual sidebar editor.', + ), + 'members-core-create-caps' => array( 'url' => '', 'title' => 'Members - Core Create Caps', diff --git a/admin/functions-admin.php b/admin/functions-admin.php index 6641c3c..c419940 100644 --- a/admin/functions-admin.php +++ b/admin/functions-admin.php @@ -52,6 +52,16 @@ function members_admin_register_scripts() { $edit_role_ver = file_exists( $edit_role_file ) ? filemtime( $edit_role_file ) : false; wp_register_script( 'members-edit-role', members_plugin()->uri . "js/edit-role{$min}.js", array( 'postbox', 'wp-util' ), $edit_role_ver, true ); + $admin_menus_file = members_plugin()->dir . "js/admin-menus{$min}.js"; + $admin_menus_ver = file_exists( $admin_menus_file ) ? filemtime( $admin_menus_file ) : false; + wp_register_script( + 'members-admin-menus', + members_plugin()->uri . "js/admin-menus{$min}.js", + array( 'jquery', 'wp-color-picker', 'wp-util' ), + $admin_menus_ver, + true + ); + // Localize our script with some text we want to pass in. $i18n = array( 'button_role_edit' => esc_html__( 'Edit', 'members' ), From 332b5855bb998207c56a5b62964ac2f171a59593 Mon Sep 17 00:00:00 2001 From: Omar ElHawary Date: Wed, 8 Apr 2026 13:34:22 +0200 Subject: [PATCH 03/73] feat(admin): add admin menus editor styles and scripts - Add sidebar editor layout, drag handles, and form styling in admin.css - Add admin-menus.js for the visual menu editor UI and interactions Made-with: Cursor --- css/admin.css | 456 +++++++++++++ css/admin.min.css | 2 +- js/admin-menus.js | 1448 +++++++++++++++++++++++++++++++++++++++++ js/admin-menus.min.js | 1 + 4 files changed, 1906 insertions(+), 1 deletion(-) create mode 100644 js/admin-menus.js create mode 100644 js/admin-menus.min.js diff --git a/css/admin.css b/css/admin.css index ccee2c0..5f507cd 100644 --- a/css/admin.css +++ b/css/admin.css @@ -1053,4 +1053,460 @@ float: none; margin-top: 0; vertical-align: middle; +} + +/* Members — Admin Menus add-on */ +.members-admin-menus-wrap { + max-width: 1400px; +} + +.members-admin-menus-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin-bottom: 16px; +} + +.members-am-copy-wrap label { + margin-right: 8px; +} + +.members-am-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; +} + +.members-am-chip { + border-radius: 999px; + padding: 4px 12px; + border: 1px solid #c3c4c7; + background: #f6f7f7; + cursor: pointer; +} + +.members-am-chip.is-active { + background: #2271b1; + border-color: #2271b1; + color: #fff; +} + +.members-am-carousel-wrap { + display: flex; + align-items: stretch; + gap: 8px; + margin-bottom: 8px; +} + +.members-am-carousel-prev, +.members-am-carousel-next { + align-self: center; + width: 36px; + height: 36px; + border-radius: 50%; + border: 1px solid #c3c4c7; + background: #fff; + cursor: pointer; +} + +.members-am-columns { + display: flex; + flex: 1; + gap: 12px; + min-height: 320px; + overflow: hidden; +} + +.members-am-column { + flex: 1; + min-width: 0; + background: #1d2327; + border-radius: 4px; + color: #f0f0f1; + display: flex; + flex-direction: column; +} + +.members-am-user-column { + border: 2px dashed #8c8f94; +} + +.members-am-sidebar-head { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 10px; + background: #2c3338; + border-radius: 4px 4px 0 0; + font-weight: 600; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.members-am-col-move button { + margin-left: 4px; + padding: 0 6px; + min-height: 24px; + line-height: 22px; + font-size: 11px; +} + +.members-am-sidebar-list { + padding: 6px 0 12px; + overflow-y: auto; + max-height: 480px; +} + +.members-am-item { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 10px 6px 8px; + cursor: pointer; + border-left: 3px solid transparent; + font-size: 13px; +} + +.members-am-item.is-sub { + padding-left: 22px; + font-size: 12px; + opacity: 0.95; +} + +.members-am-item.is-hidden { + opacity: 0.18; +} + +.members-am-item.is-no-cap { + opacity: 0.35; +} + +.members-am-item.is-no-cap.is-hidden { + opacity: 0.15; +} + +.members-am-item.is-selected { + border-left-color: #2271b1; + background: rgba(34, 113, 177, 0.15); +} + +.members-am-item-main { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.members-am-item-main .dashicons { + font-size: 18px; + width: 18px; + height: 18px; +} + +.members-am-item-label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.members-am-item-actions { + display: none; + gap: 2px; +} + +.members-am-item:hover .members-am-item-actions { + display: flex; +} + +.members-am-item-actions button { + min-height: 22px; + padding: 0 4px; + line-height: 20px; + font-size: 11px; +} + +.members-am-sep { + text-align: center; + opacity: 0.35; + font-size: 11px; + padding: 4px 0; +} + +.members-am-carousel-dots { + text-align: center; + margin-bottom: 4px; +} + +.members-am-dot { + width: 8px; + height: 8px; + border-radius: 50%; + border: none; + margin: 0 4px; + padding: 0; + background: #c3c4c7; + cursor: pointer; +} + +.members-am-dot.is-active { + background: #2271b1; +} + +.members-am-carousel-status { + text-align: center; + color: #646970; + font-size: 12px; + margin-top: 0; +} + +.members-am-badge { + font-size: 9px; + text-transform: uppercase; + padding: 1px 4px; + border-radius: 2px; + margin-right: 4px; + vertical-align: middle; +} + +.members-am-badge-new { + background: #2271b1; + color: #fff; +} + +.members-am-badge-edit { + background: #dba617; + color: #1d2327; +} + +.members-am-badge-nocap { + background: #8c8f94; + color: #fff; +} + +.members-am-legend { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin: 0 0 8px; + padding: 0; + font-size: 12px; + color: #646970; +} + +.members-am-legend-item { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.members-am-edit-panel { + margin-top: 20px; + padding: 16px; + border: 1px solid #c3c4c7; + background: #fff; + border-radius: 4px; +} + +.members-am-edit-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.members-am-edit-panel-header h2 { + margin: 0; + font-size: 16px; +} + +.members-am-edit-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.members-am-edit-target-wrap select { + margin-left: 8px; +} + +.members-am-level-actions .button { + margin-right: 6px; +} + +.members-am-edit-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; +} + +.members-am-edit-col label { + display: block; + font-weight: 600; + margin-bottom: 4px; +} + +.members-am-icon-tabs { + margin-bottom: 8px; +} + +.members-am-icon-tabs .button { + margin-right: 4px; + margin-bottom: 4px; +} + +.members-am-icon-tabs .button.is-active { + background: #2271b1; + border-color: #2271b1; + color: #fff; +} + +.members-am-icon-grid { + display: flex; + flex-wrap: wrap; + gap: 4px; + max-height: 160px; + overflow-y: auto; + border: 1px solid #dcdcde; + padding: 8px; + background: #f6f7f7; + margin-bottom: 8px; +} + +.members-am-icon-pick { + width: 36px; + height: 36px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid #c3c4c7; + background: #fff; + cursor: pointer; +} + +.members-am-vis-row { + display: block; + margin-bottom: 6px; + font-weight: normal; +} + +.members-am-reset-dropdown { + position: absolute; + z-index: 100; + background: #fff; + border: 1px solid #c3c4c7; + border-radius: 6px; + padding: 0; + display: flex; + flex-direction: column; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + min-width: 280px; + top: 100%; + left: 0; + margin-top: 4px; + overflow: hidden; +} + +.members-am-reset-title { + padding: 10px 14px; + font-weight: 600; + font-size: 13px; + border-bottom: 1px solid #f0f0f0; + color: #1d2327; +} + +.members-am-reset-option { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 14px; + border: none; + background: none; + cursor: pointer; + text-align: left; + width: 100%; + border-bottom: 1px solid #f0f0f0; + transition: background 0.15s; +} + +.members-am-reset-option:last-child { + border-bottom: none; +} + +.members-am-reset-option:hover { + background: #f6f7f7; +} + +.members-am-reset-option .dashicons { + margin-top: 2px; + color: #646970; +} + +.members-am-reset-option-text { + display: flex; + flex-direction: column; + gap: 2px; +} + +.members-am-reset-option-text strong { + font-size: 13px; + color: #1d2327; +} + +.members-am-reset-option-text small { + font-size: 12px; + color: #646970; +} + +.members-am-reset-danger:hover { + background: #fcf0f1; +} + +.members-am-reset-danger .dashicons { + color: #d63638; +} + +.members-am-reset-danger:hover strong { + color: #d63638; +} + +@media screen and (max-width: 782px) { + .members-am-columns { + flex-direction: column; + } +} + +.members-am-user-suggestions { + position: absolute; + z-index: 100; + background: #fff; + border: 1px solid #c3c4c7; + border-radius: 4px; + max-height: 200px; + overflow-y: auto; + width: 100%; + box-shadow: 0 2px 6px rgba(0,0,0,0.15); + top: 100%; + left: 0; +} + +.members-am-user-suggestion { + padding: 8px 12px; + cursor: pointer; + font-size: 13px; + border-bottom: 1px solid #f0f0f0; +} + +.members-am-user-suggestion:hover { + background: #f0f6fc; +} + +.members-am-user-suggestion:last-child { + border-bottom: none; } \ No newline at end of file diff --git a/css/admin.min.css b/css/admin.min.css index 8c6c729..a12880e 100644 --- a/css/admin.min.css +++ b/css/admin.min.css @@ -1 +1 @@ -#members-cp h2.hndle,#members-cp-side h2.hndle{border-bottom:none !important;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}#members-cp h2.hndle:before,#members-cp-side h2.hndle:before{content:url("data:image/svg+xml,%3Csvg width='15px' aria-hidden='true' focusable='false' data-prefix='fas' data-icon='users-cog' class='svg-inline--fa fa-users-cog fa-w-20' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 512'%3E%3Cpath fill='currentColor' d='M610.5 341.3c2.6-14.1 2.6-28.5 0-42.6l25.8-14.9c3-1.7 4.3-5.2 3.3-8.5-6.7-21.6-18.2-41.2-33.2-57.4-2.3-2.5-6-3.1-9-1.4l-25.8 14.9c-10.9-9.3-23.4-16.5-36.9-21.3v-29.8c0-3.4-2.4-6.4-5.7-7.1-22.3-5-45-4.8-66.2 0-3.3.7-5.7 3.7-5.7 7.1v29.8c-13.5 4.8-26 12-36.9 21.3l-25.8-14.9c-2.9-1.7-6.7-1.1-9 1.4-15 16.2-26.5 35.8-33.2 57.4-1 3.3.4 6.8 3.3 8.5l25.8 14.9c-2.6 14.1-2.6 28.5 0 42.6l-25.8 14.9c-3 1.7-4.3 5.2-3.3 8.5 6.7 21.6 18.2 41.1 33.2 57.4 2.3 2.5 6 3.1 9 1.4l25.8-14.9c10.9 9.3 23.4 16.5 36.9 21.3v29.8c0 3.4 2.4 6.4 5.7 7.1 22.3 5 45 4.8 66.2 0 3.3-.7 5.7-3.7 5.7-7.1v-29.8c13.5-4.8 26-12 36.9-21.3l25.8 14.9c2.9 1.7 6.7 1.1 9-1.4 15-16.2 26.5-35.8 33.2-57.4 1-3.3-.4-6.8-3.3-8.5l-25.8-14.9zM496 368.5c-26.8 0-48.5-21.8-48.5-48.5s21.8-48.5 48.5-48.5 48.5 21.8 48.5 48.5-21.7 48.5-48.5 48.5zM96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm224 32c1.9 0 3.7-.5 5.6-.6 8.3-21.7 20.5-42.1 36.3-59.2 7.4-8 17.9-12.6 28.9-12.6 6.9 0 13.7 1.8 19.6 5.3l7.9 4.6c.8-.5 1.6-.9 2.4-1.4 7-14.6 11.2-30.8 11.2-48 0-61.9-50.1-112-112-112S208 82.1 208 144c0 61.9 50.1 112 112 112zm105.2 194.5c-2.3-1.2-4.6-2.6-6.8-3.9-8.2 4.8-15.3 9.8-27.5 9.8-10.9 0-21.4-4.6-28.9-12.6-18.3-19.8-32.3-43.9-40.2-69.6-10.7-34.5 24.9-49.7 25.8-50.3-.1-2.6-.1-5.2 0-7.8l-7.9-4.6c-3.8-2.2-7-5-9.8-8.1-3.3.2-6.5.6-9.8.6-24.6 0-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h255.4c-3.7-6-6.2-12.8-6.2-20.3v-9.2zM173.1 274.6C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z'%3E%3C/path%3E%3C/svg%3E");margin-right:5px}#members-cp-side .postbox-header{border-bottom:none !important}#members-cp-side .handle-actions .handle-order-higher,#members-cp-side .handle-actions .handle-order-lower{display:none}#members-cp-side .handle-actions .handlediv{position:absolute;right:5px}.members-upgrade-header{text-align:center;padding:5px}.members-upgrade-header a{color:#f59431}.memberpress-paid-memberships{background-color:#f5f5f5;padding:50px 10px;text-align:center}.memberpress-paid-memberships a{background-color:#0e4598;display:inline-block;padding:10px 20px;border-radius:300px;color:#fff;text-decoration:none;margin-top:20px}#members-cp-side .members-tabs{position:relative}#members-cp-side .members-tab-nav{background-color:transparent;margin-bottom:20px;border-right:none !important}#members-cp-side .members-tab-nav .members-tab-title{width:33.333%;height:50px;border-right:none !important}#members-cp-side .members-tab-nav .members-tab-title a{border:0}#members-cp-side .members-tab-content h3{font-weight:bold;font-size:15px;margin-top:0;color:#000;margin-bottom:10px}#members-cp-side .members-tab-content .memberpress-paid-memberships{text-align:left;padding:20px}#members-cp-side .members-tab-content .memberpress-paid-memberships a{display:block;text-align:center}#members-cp-side .members-tabs .members-tab-nav li a,#members-cp-side .members-tabs .members-tab-nav li[aria-selected="false"] a i{color:#555}#members-cp-side .members-tabs .members-tab-nav li a svg,#members-cp-side .members-tabs .members-tab-nav li[aria-selected="false"] a svg{fill:#555}#members-cp-side .members-tabs .members-tab-nav li[aria-selected="true"] a{background-color:transparent;border-bottom:2px solid #3582ae}#members-cp-side .members-tabs .members-tab-nav li[aria-selected="true"] a i{color:#3582ae}#members-cp-side .members-tabs .members-tab-nav li[aria-selected="true"] a svg{fill:#3582ae}.members-tab-title svg,.members-tab-title[aria-selected="false"] svg{fill:#0073aa}.members-tab-title[aria-selected="true"] svg{fill:#555}#wpcontent #members-admin-header{margin-left:-20px;padding:0;padding-left:22px;background-color:#f9f9f9;background-repeat:repeat;background-size:1200px 75px;min-height:36px;position:relative}#wpcontent #members-admin-header img.members-logo{height:90px}.members-upgrade-header #close-members-upgrade-header{position:absolute;right:20px;top:3px}.members-upgrade-header #close-members-upgrade-header:hover{cursor:pointer}.memberpress-welcome-panel.welcome-panel{margin-top:0;padding:40px 20px;background:#fff}.memberpress-welcome-panel.welcome-panel:before{background:0}.memberpress-welcome-panel.welcome-panel .about-description{margin:20px 0}.memberpress-welcome-panel.welcome-panel-content{margin:0}.memberpress-welcome-panel.welcome-panel-content .mp-logo-wrap>a{display:block}@media screen and (min-width:870px){.members_page_members-settings .members-short-p{max-width:612px;margin-left:auto;margin-right:auto}.members_page_members-settings .welcome-panel-content{text-align:center}}.members_page_members-settings .welcome-panel .button.button-hero{margin-left:auto;margin-right:auto}.members_page_members-settings .members-svg-wrap{display:inline-block;margin:0 auto 20px}@media screen and (max-width:870px){.members_page_members-settings .welcome-panel{padding:20px}.members_page_members-settings .members-svg-wrap{float:left;margin-right:28px}.admin_page_members-settings.rtl .members-svg-wrap{float:right;margin-right:0;margin-left:28px}}.members_page_members-settings .members-svg-link{display:table-cell;text-align:center;width:128px;height:128px;background:#363b3f;color:#fff;padding:24px 16px 16px;border-radius:50%;-webkit-box-sizing:border-box;box-sizing:border-box;border:4px solid #fff;-webkit-box-shadow:0 0 0 4px #363b3f;box-shadow:0 0 0 4px #363b3f}.members_page_members-settings .members-svg-link svg{max-width:84px;max-height:64px;width:auto;height:auto}.members-addons{display:grid;grid-template-columns:1fr 1fr;grid-row-gap:20px;grid-column-gap:20px}.members-addons .plugin-card{margin:0}.members-addons .plugin-card-top{width:100%}.members-addon{width:100%;position:relative;float:none;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.members-addon .desc{margin-bottom:80px}.addon-activate{text-align:center;border:1px solid #ddd;border-radius:3px;padding:5px 10px 10px;display:inline-block;width:100px;position:absolute;right:20px;bottom:20px}.activate-toggle:hover{cursor:pointer}.activate-toggle svg{max-width:20px;position:relative;top:5px}.activate-toggle svg{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.activate-toggle svg.active{-webkit-transform:rotate(0);transform:rotate(0)}.activate-toggle svg path{fill:#ccc}.activate-toggle svg.active path{fill:#8cbd5a}.activate-toggle.processing{cursor:not-allowed;pointer-events:none;opacity:.5}.activate-toggle .action-label{display:inline-block;width:50px;text-align:left}.mepr_dummy_txns{-webkit-filter:blur(3px);filter:blur(3px);position:absolute;top:0;left:0;width:100%;z-index:5;pointer-events:none}.mepr_dummy_txns::after{background-color:rgba(0,0,0,.2);position:absolute;top:0;left:0;width:100%;height:100%;z-index:6}.mepr-upgrade-table{position:relative}.mepr-upgrade-table .mepr-upgrade{position:relative;top:100px;z-index:10;background:rgba(0,0,0,.2);width:100%;height:100%;max-width:600px;margin:0 auto;-webkit-box-shadow:0 0 30px #ccc;box-shadow:0 0 30px #ccc}.mepr-upgrade-content{background-color:#fff;padding:20px;text-align:center}.mepr-upgrade-content h2{font-size:24px}.mepr-upgrade-content h4{font-size:18px}.mepr-upgrade-content .features{display:grid;grid-template-columns:1fr 1fr;grid-column-gap:30px;grid-row-gap:10px;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;text-align:left;padding:10px}.features li:before{content:"\f147";font-family:dashicons;color:green;font-size:20px;position:relative;top:4px;margin-right:10px}.mepr-upgrade-cta{text-align:center;background-color:#f2f2f2;padding:20px}.mepr-upgrade-cta .mepr-cta-button{display:block;max-width:300px;margin:0 auto 20px;background-color:#fd9c27;color:#fff;padding:15px;font-size:18px;font-weight:bold;text-decoration:none;border-radius:300px}.members-plugin-card.plugin-card:nth-child(even){margin-right:inherit}.members-plugin-card.plugin-card:nth-child(odd){clear:none;margin-left:inherit}.members-plugin-card.plugin-card .plugin-card-top{min-height:0;padding:30px 20px}.members-plugin-card.plugin-card .plugin-icon{top:30px}.members-plugin-card.plugin-card .plugin-card-bottom .column-status{font-weight:bold}.members-plugin-card.plugin-card .plugin-card-bottom .column-status .active{color:#8cbd5a}.members-plugin-card.plugin-card .plugin-card-bottom .column-status .inactive{color:red}.members-plugin-card.plugin-card .plugin-icon{width:64px;height:auto}.plugin-icon-small{width:32px;height:auto}.members-plugin-card.plugin-card .name,.members-plugin-card.plugin-card .desc{margin-right:0;margin-left:84px}.members-addons .plugin-card .name,.members-addons .plugin-card .desc p{margin-right:0}@media screen and (max-width:1100px){.members-addons{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}@media screen and (max-width:600px){.members_page_members-settings .members-svg-link{width:64px;height:64px}.members_page_members-settings .members-svg-link svg{position:absolute;top:14px;left:15px;max-width:30px;max-height:30px}.members_page_members-settings .plugin-card-members-edd-integration .members-svg-link svg,.members_page_members-settings .plugin-card-members-acf-integration .members-svg-link svg,.members_page_members-settings .plugin-card-members-givewp-integration .members-svg-link svg,.members_page_members-settings .plugin-card-members-meta-box-integration .members-svg-link svg,.members_page_members-settings .plugin-card-members-woocommerce-integration .members-svg-link svg{top:10px;left:8px;max-width:40px;max-height:40px}.members_page_members-settings .plugin-card-members-block-permissions .members-svg-link svg,.members_page_members-settings .plugin-card-members-role-hierarchy .members-svg-link svg{left:14px}.members-addons .plugin-card .desc>p,.members-addons .plugin-card .name{margin-left:84px}}.memberpress-about .welcome-panel-column-container{display:-webkit-box;display:-ms-flexbox;display:flex}.memberpress-about .mp-logo{max-width:256px;margin:50px}.members-about-addons{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));grid-column-gap:20px}.members-about-addons .members-plugin-card{margin:0;width:100%}.members-about-addons .members-plugin-card .name h3 a{text-decoration:none}.members-about-addons .members-plugin-card .plugin-card-bottom{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:1099px){.members-about-addons{grid-template-columns:1fr;grid-row-gap:20px}}@media screen and (min-width:1100px) and (max-width:1600px){.members-about-addons .members-plugin-card{float:none;width:auto;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.members-plugin-card.plugin-card .desc{margin-left:0;margin-top:50px}.members-plugin-card.plugin-card .desc p{margin-left:0;margin-right:0}.members-plugin-card.plugin-card .plugin-card-bottom .column-status{float:none}.members-plugin-card.plugin-card .plugin-card-bottom .column-updated{float:none;width:auto}}@media screen and (max-width:1200px){.welcome-panel .welcome-panel-column-container{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse;gap:0;padding:0}.memberpress-about .mp-logo{margin:0 0 20px}}.members-plugin-card.plugin-card .desc p{margin-left:0;margin-right:0}@media only screen and (min-width:783px){.members_page_roles .column-users,.members_page_roles .column-granted_caps,.members_page_roles .column-denied_caps{width:100px;text-align:center}}.members-title-div #titlewrap input{padding:0 8px;font-size:1.7em;line-height:normal;height:1.7em;width:100%;outline:0;margin:0 0 3px;background-color:#fff}.members-title-div input[name="role"]{font-size:13px;height:22px;margin:0;width:16em}#tabcapsdiv{margin-top:1em}#tabcapsdiv>.hndle{padding:10px;border-bottom:1px solid #eee}#tabcapsdiv .inside,#members-cp .inside{margin:0;padding:0}.members-cap-tabs,.members-tabs{overflow:hidden;background:#fff;background:-webkit-gradient(linear,left top,right top,from(#fafafa),color-stop(20%,#fafafa),color-stop(20%,#fff),to(#fff));background:linear-gradient(90deg,#fafafa 0,#fafafa 20%,#fff 20%,#fff 100%)}@media only screen and (max-width:782px){.members-cap-tabs,.members-tabs{background:linear-gradient(90deg,#fafafa 0,#fafafa 48px,#fff 48px,#fff 100%)}}.members-cap-tabs .members-tab-nav,.members-tabs .members-tab-nav{position:relative;float:left;list-style:none;width:20%;line-height:1em;margin:0 0 -1px 0;padding:0;background-color:#fafafa;border-right:1px solid #eee;-webkit-box-sizing:border-box;box-sizing:border-box}.members-cap-tabs .members-tab-nav li,.members-tabs .members-tab-nav li{display:block;position:relative;margin:0;padding:0;line-height:20px}.members-cap-tabs .members-tab-nav li a,.members-tabs .members-tab-nav li a{display:block;margin:0;padding:10px;line-height:20px !important;text-decoration:none;border-bottom:1px solid #eee;-webkit-box-shadow:none;box-shadow:none}.members-cap-tabs .members-tab-nav li a .dashicons,.members-tabs .members-tab-nav li a .dashicons{line-height:20px;margin-right:3px}.members-cap-tabs .members-tab-nav li[aria-selected="true"] a,.members-tabs .members-tab-nav li[aria-selected="true"] a{position:relative;font-weight:bold;color:#555;background-color:#e0e0e0}@media only screen and (max-width:782px){.members-cap-tabs .members-tab-nav,.members-tabs .members-tab-nav{width:48px}.members-cap-tabs .members-tab-nav li a .dashicons,.members-tabs .members-tab-nav li a .dashicons{width:24px;height:24px;font-size:24px;line-height:24px}.members-tab-nav li .dashicons::before,.members-tab-nav li .dashicons::before{width:24px;height:24px}.members-tab-nav li .label{overflow:hidden;position:absolute;top:-1000em;left:-1000em;width:1px;height:1px}}.members-cap-tabs .members-tab-wrap,.members-tabs .members-tab-wrap{float:left;width:80%;margin-left:-1px}@media only screen and (max-width:782px){.members-cap-tabs .members-tab-wrap,.members-tabs .members-tab-wrap{width:calc(100% - 48px)}}#members-cp .members-tab-content{padding:10px;border-left:1px solid #e5e5e5}.members-roles-select th,.members-roles-select td{width:190px;overflow:hidden}.members-roles-select .check-all-th{text-align:center}.members-roles-select .check-all-th label{padding-left:5px}@media only screen and (min-width:850px){#side-sortables .members-tabs{background:#fff}#side-sortables .members-tabs .members-tab-wrap{width:100%}#side-sortables .members-tabs .members-tab-nav{display:table;width:100%}#side-sortables .members-tabs .members-tab-nav li{display:table-cell;text-align:center;border-right:1px solid #eee}#side-sortables .members-tabs .members-tab-nav li:last-of-type{border-right:0}#side-sortables .members-tabs .members-tab-nav li a{padding:10px 0}#side-sortables .members-tabs .members-tab-nav .dashicons{width:24px;height:24px;font-size:24px;line-height:24px}#side-sortables .members-tabs .members-tab-nav .dashicons::before{width:24px;height:24px}#side-sortables .members-tabs .members-tab-nav .label{overflow:hidden;position:absolute;top:-1000em;left:-1000em;width:1px;height:1px}}.members-tabs .members-tabs-label{display:block !important;font-weight:bold;display:inline-block;margin-bottom:4px}.members-tabs .butterbean-control-checkbox .members-tabs-label{display:inline !important}.members-tabs .members-tabs-description{display:block;font-style:italic;margin-top:4px}.members-tabs .members-tabs-label+.members-tabs-description{margin-top:0;margin-bottom:4px}#tabcapsdiv table{border-right:0;border-top:0;border-bottom:0}#tabcapsdiv table td,#tabcapsdiv table th{padding:10px;padding-bottom:10px;border-bottom:1px solid #eee;font-size:13px;line-height:20px}#tabcapsdiv table td{padding:9px}#tabcapsdiv tbody tr:last-of-type td{border-bottom:0}#tabcapsdiv tfoot th{border-color:#eee}@media only screen and (max-width:782px){#tabcapsdiv table td,#tabcapsdiv table th{line-height:24px}}.members-roles-select .column-grant,.members-roles-select .column-deny{width:70px !important;text-align:left;display:table-cell !important;clear:none !important}.members-roles-select thead th,.members-roles-select tfoot th{padding:9px !important}.members-roles-select .column-grant input,.members-roles-select .column-deny input{margin:0 5px 0 0;vertical-align:middle}.members-roles-select tbody tr:nth-child(even){background:#f2f2f2}.members-cap-tabs button{display:inline;margin:-4px;line-height:inherit;padding:4px 8px;border:1px solid transparent;background:transparent;border-radius:0;outline:0;-webkit-transition:all .25s ease-out;transition:all .25s ease-out}.members-cap-tabs button:hover,.members-cap-tabs button:focus{border-color:#eee;background:#fafafa;cursor:pointer}.members-cap-tabs button:active{color:#0073aa;border-color:#0073aa}.members-cap-tabs button+.dashicons{display:none;margin-top:1px;margin-bottom:-1px;line-height:inherit}.members-cap-tabs button:hover+.dashicons,.members-cap-tabs button:focus+.dashicons{display:inline-block}.members-tab-content .members-highlight{background-color:rgba(0,115,170,0.05)}.members-tab-content tbody{-webkit-transition:all 2s ease-in-out;transition:all 2s ease-in-out}.members-cp-role-list-wrap{overflow:auto;min-height:42px;max-height:200px;padding:0 .9em;border:solid 1px #dfdfdf;background-color:#fdfdfd}#wpbody-content{padding-bottom:200px}.members-footer-promotion{text-align:center;font-weight:400;font-size:13px;line-height:16px;color:#787c82;padding:20px 0 30px 0;margin-bottom:20px}.members-footer-promotion p{font-weight:600}.members-footer-promotion-links,.members-footer-promotion-social{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.members-footer-promotion-links{margin:9px 0 0}.members-footer-promotion-links span{color:#c3c4c7;padding:0 7px}.members-footer-promotion-social{margin:10px 0 0 0;gap:10px}.members-footer-promotion-social li{margin-bottom:0}.members-footer-promotion-social a{display:block;height:16px}.members_page_members-settings.rtl{.addon-activate{left:20px;right:unset}.members-addons .plugin-card{.name,.desc p{margin-left:0;margin-right:148px}}}.button.button-warning{background:#dc3232;border-color:#dc3232;color:#fff}.button.button-warning:hover,.button.button-warning:focus{background:#c92424;border-color:#c92424;color:#fff}.members-reset-spinner{float:none;margin-top:0;vertical-align:middle} \ No newline at end of file +#members-cp h2.hndle,#members-cp-side h2.hndle{border-bottom:none !important;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}#members-cp h2.hndle:before,#members-cp-side h2.hndle:before{content:url("data:image/svg+xml,%3Csvg width='15px' aria-hidden='true' focusable='false' data-prefix='fas' data-icon='users-cog' class='svg-inline--fa fa-users-cog fa-w-20' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 512'%3E%3Cpath fill='currentColor' d='M610.5 341.3c2.6-14.1 2.6-28.5 0-42.6l25.8-14.9c3-1.7 4.3-5.2 3.3-8.5-6.7-21.6-18.2-41.2-33.2-57.4-2.3-2.5-6-3.1-9-1.4l-25.8 14.9c-10.9-9.3-23.4-16.5-36.9-21.3v-29.8c0-3.4-2.4-6.4-5.7-7.1-22.3-5-45-4.8-66.2 0-3.3.7-5.7 3.7-5.7 7.1v29.8c-13.5 4.8-26 12-36.9 21.3l-25.8-14.9c-2.9-1.7-6.7-1.1-9 1.4-15 16.2-26.5 35.8-33.2 57.4-1 3.3.4 6.8 3.3 8.5l25.8 14.9c-2.6 14.1-2.6 28.5 0 42.6l-25.8 14.9c-3 1.7-4.3 5.2-3.3 8.5 6.7 21.6 18.2 41.1 33.2 57.4 2.3 2.5 6 3.1 9 1.4l25.8-14.9c10.9 9.3 23.4 16.5 36.9 21.3v29.8c0 3.4 2.4 6.4 5.7 7.1 22.3 5 45 4.8 66.2 0 3.3-.7 5.7-3.7 5.7-7.1v-29.8c13.5-4.8 26-12 36.9-21.3l25.8 14.9c2.9 1.7 6.7 1.1 9-1.4 15-16.2 26.5-35.8 33.2-57.4 1-3.3-.4-6.8-3.3-8.5l-25.8-14.9zM496 368.5c-26.8 0-48.5-21.8-48.5-48.5s21.8-48.5 48.5-48.5 48.5 21.8 48.5 48.5-21.7 48.5-48.5 48.5zM96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm224 32c1.9 0 3.7-.5 5.6-.6 8.3-21.7 20.5-42.1 36.3-59.2 7.4-8 17.9-12.6 28.9-12.6 6.9 0 13.7 1.8 19.6 5.3l7.9 4.6c.8-.5 1.6-.9 2.4-1.4 7-14.6 11.2-30.8 11.2-48 0-61.9-50.1-112-112-112S208 82.1 208 144c0 61.9 50.1 112 112 112zm105.2 194.5c-2.3-1.2-4.6-2.6-6.8-3.9-8.2 4.8-15.3 9.8-27.5 9.8-10.9 0-21.4-4.6-28.9-12.6-18.3-19.8-32.3-43.9-40.2-69.6-10.7-34.5 24.9-49.7 25.8-50.3-.1-2.6-.1-5.2 0-7.8l-7.9-4.6c-3.8-2.2-7-5-9.8-8.1-3.3.2-6.5.6-9.8.6-24.6 0-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h255.4c-3.7-6-6.2-12.8-6.2-20.3v-9.2zM173.1 274.6C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z'%3E%3C/path%3E%3C/svg%3E");margin-right:5px}#members-cp-side .postbox-header{border-bottom:none !important}#members-cp-side .handle-actions .handle-order-higher,#members-cp-side .handle-actions .handle-order-lower{display:none}#members-cp-side .handle-actions .handlediv{position:absolute;right:5px}.members-upgrade-header{text-align:center;padding:5px}.members-upgrade-header a{color:#f59431}.memberpress-paid-memberships{background-color:#f5f5f5;padding:50px 10px;text-align:center}.memberpress-paid-memberships a{background-color:#0e4598;display:inline-block;padding:10px 20px;border-radius:300px;color:#fff;text-decoration:none;margin-top:20px}#members-cp-side .members-tabs{position:relative}#members-cp-side .members-tab-nav{background-color:transparent;margin-bottom:20px;border-right:none !important}#members-cp-side .members-tab-nav .members-tab-title{width:33.333%;height:50px;border-right:none !important}#members-cp-side .members-tab-nav .members-tab-title a{border:0}#members-cp-side .members-tab-content h3{font-weight:bold;font-size:15px;margin-top:0;color:#000;margin-bottom:10px}#members-cp-side .members-tab-content .memberpress-paid-memberships{text-align:left;padding:20px}#members-cp-side .members-tab-content .memberpress-paid-memberships a{display:block;text-align:center}#members-cp-side .members-tabs .members-tab-nav li a,#members-cp-side .members-tabs .members-tab-nav li[aria-selected="false"] a i{color:#555}#members-cp-side .members-tabs .members-tab-nav li a svg,#members-cp-side .members-tabs .members-tab-nav li[aria-selected="false"] a svg{fill:#555}#members-cp-side .members-tabs .members-tab-nav li[aria-selected="true"] a{background-color:transparent;border-bottom:2px solid #3582ae}#members-cp-side .members-tabs .members-tab-nav li[aria-selected="true"] a i{color:#3582ae}#members-cp-side .members-tabs .members-tab-nav li[aria-selected="true"] a svg{fill:#3582ae}.members-tab-title svg,.members-tab-title[aria-selected="false"] svg{fill:#0073aa}.members-tab-title[aria-selected="true"] svg{fill:#555}#wpcontent #members-admin-header{margin-left:-20px;padding:0;padding-left:22px;background-color:#f9f9f9;background-repeat:repeat;background-size:1200px 75px;min-height:36px;position:relative}#wpcontent #members-admin-header img.members-logo{height:90px}.members-upgrade-header #close-members-upgrade-header{position:absolute;right:20px;top:3px}.members-upgrade-header #close-members-upgrade-header:hover{cursor:pointer}.memberpress-welcome-panel.welcome-panel{margin-top:0;padding:40px 20px;background:#fff}.memberpress-welcome-panel.welcome-panel:before{background:0}.memberpress-welcome-panel.welcome-panel .about-description{margin:20px 0}.memberpress-welcome-panel.welcome-panel-content{margin:0}.memberpress-welcome-panel.welcome-panel-content .mp-logo-wrap>a{display:block}@media screen and (min-width:870px){.members_page_members-settings .members-short-p{max-width:612px;margin-left:auto;margin-right:auto}.members_page_members-settings .welcome-panel-content{text-align:center}}.members_page_members-settings .welcome-panel .button.button-hero{margin-left:auto;margin-right:auto}.members_page_members-settings .members-svg-wrap{display:inline-block;margin:0 auto 20px}@media screen and (max-width:870px){.members_page_members-settings .welcome-panel{padding:20px}.members_page_members-settings .members-svg-wrap{float:left;margin-right:28px}.admin_page_members-settings.rtl .members-svg-wrap{float:right;margin-right:0;margin-left:28px}}.members_page_members-settings .members-svg-link{display:table-cell;text-align:center;width:128px;height:128px;background:#363b3f;color:#fff;padding:24px 16px 16px;border-radius:50%;-webkit-box-sizing:border-box;box-sizing:border-box;border:4px solid #fff;-webkit-box-shadow:0 0 0 4px #363b3f;box-shadow:0 0 0 4px #363b3f}.members_page_members-settings .members-svg-link svg{max-width:84px;max-height:64px;width:auto;height:auto}.members-addons{display:grid;grid-template-columns:1fr 1fr;grid-row-gap:20px;grid-column-gap:20px}.members-addons .plugin-card{margin:0}.members-addons .plugin-card-top{width:100%}.members-addon{width:100%;position:relative;float:none;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.members-addon .desc{margin-bottom:80px}.addon-activate{text-align:center;border:1px solid #ddd;border-radius:3px;padding:5px 10px 10px;display:inline-block;width:100px;position:absolute;right:20px;bottom:20px}.activate-toggle:hover{cursor:pointer}.activate-toggle svg{max-width:20px;position:relative;top:5px}.activate-toggle svg{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.activate-toggle svg.active{-webkit-transform:rotate(0);transform:rotate(0)}.activate-toggle svg path{fill:#ccc}.activate-toggle svg.active path{fill:#8cbd5a}.activate-toggle.processing{cursor:not-allowed;pointer-events:none;opacity:.5}.activate-toggle .action-label{display:inline-block;width:50px;text-align:left}.mepr_dummy_txns{-webkit-filter:blur(3px);filter:blur(3px);position:absolute;top:0;left:0;width:100%;z-index:5;pointer-events:none}.mepr_dummy_txns::after{background-color:rgba(0,0,0,.2);position:absolute;top:0;left:0;width:100%;height:100%;z-index:6}.mepr-upgrade-table{position:relative}.mepr-upgrade-table .mepr-upgrade{position:relative;top:100px;z-index:10;background:rgba(0,0,0,.2);width:100%;height:100%;max-width:600px;margin:0 auto;-webkit-box-shadow:0 0 30px #ccc;box-shadow:0 0 30px #ccc}.mepr-upgrade-content{background-color:#fff;padding:20px;text-align:center}.mepr-upgrade-content h2{font-size:24px}.mepr-upgrade-content h4{font-size:18px}.mepr-upgrade-content .features{display:grid;grid-template-columns:1fr 1fr;grid-column-gap:30px;grid-row-gap:10px;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;text-align:left;padding:10px}.features li:before{content:"\f147";font-family:dashicons;color:green;font-size:20px;position:relative;top:4px;margin-right:10px}.mepr-upgrade-cta{text-align:center;background-color:#f2f2f2;padding:20px}.mepr-upgrade-cta .mepr-cta-button{display:block;max-width:300px;margin:0 auto 20px;background-color:#fd9c27;color:#fff;padding:15px;font-size:18px;font-weight:bold;text-decoration:none;border-radius:300px}.members-plugin-card.plugin-card:nth-child(even){margin-right:inherit}.members-plugin-card.plugin-card:nth-child(odd){clear:none;margin-left:inherit}.members-plugin-card.plugin-card .plugin-card-top{min-height:0;padding:30px 20px}.members-plugin-card.plugin-card .plugin-icon{top:30px}.members-plugin-card.plugin-card .plugin-card-bottom .column-status{font-weight:bold}.members-plugin-card.plugin-card .plugin-card-bottom .column-status .active{color:#8cbd5a}.members-plugin-card.plugin-card .plugin-card-bottom .column-status .inactive{color:red}.members-plugin-card.plugin-card .plugin-icon{width:64px;height:auto}.plugin-icon-small{width:32px;height:auto}.members-plugin-card.plugin-card .name,.members-plugin-card.plugin-card .desc{margin-right:0;margin-left:84px}.members-addons .plugin-card .name,.members-addons .plugin-card .desc p{margin-right:0}@media screen and (max-width:1100px){.members-addons{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}@media screen and (max-width:600px){.members_page_members-settings .members-svg-link{width:64px;height:64px}.members_page_members-settings .members-svg-link svg{position:absolute;top:14px;left:15px;max-width:30px;max-height:30px}.members_page_members-settings .plugin-card-members-edd-integration .members-svg-link svg,.members_page_members-settings .plugin-card-members-acf-integration .members-svg-link svg,.members_page_members-settings .plugin-card-members-givewp-integration .members-svg-link svg,.members_page_members-settings .plugin-card-members-meta-box-integration .members-svg-link svg,.members_page_members-settings .plugin-card-members-woocommerce-integration .members-svg-link svg{top:10px;left:8px;max-width:40px;max-height:40px}.members_page_members-settings .plugin-card-members-block-permissions .members-svg-link svg,.members_page_members-settings .plugin-card-members-role-hierarchy .members-svg-link svg{left:14px}.members-addons .plugin-card .desc>p,.members-addons .plugin-card .name{margin-left:84px}}.memberpress-about .welcome-panel-column-container{display:-webkit-box;display:-ms-flexbox;display:flex}.memberpress-about .mp-logo{max-width:256px;margin:50px}.members-about-addons{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));grid-column-gap:20px}.members-about-addons .members-plugin-card{margin:0;width:100%}.members-about-addons .members-plugin-card .name h3 a{text-decoration:none}.members-about-addons .members-plugin-card .plugin-card-bottom{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:1099px){.members-about-addons{grid-template-columns:1fr;grid-row-gap:20px}}@media screen and (min-width:1100px) and (max-width:1600px){.members-about-addons .members-plugin-card{float:none;width:auto;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.members-plugin-card.plugin-card .desc{margin-left:0;margin-top:50px}.members-plugin-card.plugin-card .desc p{margin-left:0;margin-right:0}.members-plugin-card.plugin-card .plugin-card-bottom .column-status{float:none}.members-plugin-card.plugin-card .plugin-card-bottom .column-updated{float:none;width:auto}}@media screen and (max-width:1200px){.welcome-panel .welcome-panel-column-container{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse;gap:0;padding:0}.memberpress-about .mp-logo{margin:0 0 20px}}.members-plugin-card.plugin-card .desc p{margin-left:0;margin-right:0}@media only screen and (min-width:783px){.members_page_roles .column-users,.members_page_roles .column-granted_caps,.members_page_roles .column-denied_caps{width:100px;text-align:center}}.members-title-div #titlewrap input{padding:0 8px;font-size:1.7em;line-height:normal;height:1.7em;width:100%;outline:0;margin:0 0 3px;background-color:#fff}.members-title-div input[name="role"]{font-size:13px;height:22px;margin:0;width:16em}#tabcapsdiv{margin-top:1em}#tabcapsdiv>.hndle{padding:10px;border-bottom:1px solid #eee}#tabcapsdiv .inside,#members-cp .inside{margin:0;padding:0}.members-cap-tabs,.members-tabs{overflow:hidden;background:#fff;background:-webkit-gradient(linear,left top,right top,from(#fafafa),color-stop(20%,#fafafa),color-stop(20%,#fff),to(#fff));background:linear-gradient(90deg,#fafafa 0,#fafafa 20%,#fff 20%,#fff 100%)}@media only screen and (max-width:782px){.members-cap-tabs,.members-tabs{background:linear-gradient(90deg,#fafafa 0,#fafafa 48px,#fff 48px,#fff 100%)}}.members-cap-tabs .members-tab-nav,.members-tabs .members-tab-nav{position:relative;float:left;list-style:none;width:20%;line-height:1em;margin:0 0 -1px 0;padding:0;background-color:#fafafa;border-right:1px solid #eee;-webkit-box-sizing:border-box;box-sizing:border-box}.members-cap-tabs .members-tab-nav li,.members-tabs .members-tab-nav li{display:block;position:relative;margin:0;padding:0;line-height:20px}.members-cap-tabs .members-tab-nav li a,.members-tabs .members-tab-nav li a{display:block;margin:0;padding:10px;line-height:20px !important;text-decoration:none;border-bottom:1px solid #eee;-webkit-box-shadow:none;box-shadow:none}.members-cap-tabs .members-tab-nav li a .dashicons,.members-tabs .members-tab-nav li a .dashicons{line-height:20px;margin-right:3px}.members-cap-tabs .members-tab-nav li[aria-selected="true"] a,.members-tabs .members-tab-nav li[aria-selected="true"] a{position:relative;font-weight:bold;color:#555;background-color:#e0e0e0}@media only screen and (max-width:782px){.members-cap-tabs .members-tab-nav,.members-tabs .members-tab-nav{width:48px}.members-cap-tabs .members-tab-nav li a .dashicons,.members-tabs .members-tab-nav li a .dashicons{width:24px;height:24px;font-size:24px;line-height:24px}.members-tab-nav li .dashicons::before,.members-tab-nav li .dashicons::before{width:24px;height:24px}.members-tab-nav li .label{overflow:hidden;position:absolute;top:-1000em;left:-1000em;width:1px;height:1px}}.members-cap-tabs .members-tab-wrap,.members-tabs .members-tab-wrap{float:left;width:80%;margin-left:-1px}@media only screen and (max-width:782px){.members-cap-tabs .members-tab-wrap,.members-tabs .members-tab-wrap{width:calc(100% - 48px)}}#members-cp .members-tab-content{padding:10px;border-left:1px solid #e5e5e5}.members-roles-select th,.members-roles-select td{width:190px;overflow:hidden}.members-roles-select .check-all-th{text-align:center}.members-roles-select .check-all-th label{padding-left:5px}@media only screen and (min-width:850px){#side-sortables .members-tabs{background:#fff}#side-sortables .members-tabs .members-tab-wrap{width:100%}#side-sortables .members-tabs .members-tab-nav{display:table;width:100%}#side-sortables .members-tabs .members-tab-nav li{display:table-cell;text-align:center;border-right:1px solid #eee}#side-sortables .members-tabs .members-tab-nav li:last-of-type{border-right:0}#side-sortables .members-tabs .members-tab-nav li a{padding:10px 0}#side-sortables .members-tabs .members-tab-nav .dashicons{width:24px;height:24px;font-size:24px;line-height:24px}#side-sortables .members-tabs .members-tab-nav .dashicons::before{width:24px;height:24px}#side-sortables .members-tabs .members-tab-nav .label{overflow:hidden;position:absolute;top:-1000em;left:-1000em;width:1px;height:1px}}.members-tabs .members-tabs-label{display:block !important;font-weight:bold;display:inline-block;margin-bottom:4px}.members-tabs .butterbean-control-checkbox .members-tabs-label{display:inline !important}.members-tabs .members-tabs-description{display:block;font-style:italic;margin-top:4px}.members-tabs .members-tabs-label+.members-tabs-description{margin-top:0;margin-bottom:4px}#tabcapsdiv table{border-right:0;border-top:0;border-bottom:0}#tabcapsdiv table td,#tabcapsdiv table th{padding:10px;padding-bottom:10px;border-bottom:1px solid #eee;font-size:13px;line-height:20px}#tabcapsdiv table td{padding:9px}#tabcapsdiv tbody tr:last-of-type td{border-bottom:0}#tabcapsdiv tfoot th{border-color:#eee}@media only screen and (max-width:782px){#tabcapsdiv table td,#tabcapsdiv table th{line-height:24px}}.members-roles-select .column-grant,.members-roles-select .column-deny{width:70px !important;text-align:left;display:table-cell !important;clear:none !important}.members-roles-select thead th,.members-roles-select tfoot th{padding:9px !important}.members-roles-select .column-grant input,.members-roles-select .column-deny input{margin:0 5px 0 0;vertical-align:middle}.members-roles-select tbody tr:nth-child(even){background:#f2f2f2}.members-cap-tabs button{display:inline;margin:-4px;line-height:inherit;padding:4px 8px;border:1px solid transparent;background:transparent;border-radius:0;outline:0;-webkit-transition:all .25s ease-out;transition:all .25s ease-out}.members-cap-tabs button:hover,.members-cap-tabs button:focus{border-color:#eee;background:#fafafa;cursor:pointer}.members-cap-tabs button:active{color:#0073aa;border-color:#0073aa}.members-cap-tabs button+.dashicons{display:none;margin-top:1px;margin-bottom:-1px;line-height:inherit}.members-cap-tabs button:hover+.dashicons,.members-cap-tabs button:focus+.dashicons{display:inline-block}.members-tab-content .members-highlight{background-color:rgba(0,115,170,0.05)}.members-tab-content tbody{-webkit-transition:all 2s ease-in-out;transition:all 2s ease-in-out}.members-cp-role-list-wrap{overflow:auto;min-height:42px;max-height:200px;padding:0 .9em;border:solid 1px #dfdfdf;background-color:#fdfdfd}#wpbody-content{padding-bottom:200px}.members-footer-promotion{text-align:center;font-weight:400;font-size:13px;line-height:16px;color:#787c82;padding:20px 0 30px 0;margin-bottom:20px}.members-footer-promotion p{font-weight:600}.members-footer-promotion-links,.members-footer-promotion-social{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.members-footer-promotion-links{margin:9px 0 0}.members-footer-promotion-links span{color:#c3c4c7;padding:0 7px}.members-footer-promotion-social{margin:10px 0 0 0;gap:10px}.members-footer-promotion-social li{margin-bottom:0}.members-footer-promotion-social a{display:block;height:16px}.members_page_members-settings.rtl{.addon-activate{left:20px;right:unset}.members-addons .plugin-card{.name,.desc p{margin-left:0;margin-right:148px}}}.button.button-warning{background:#dc3232;border-color:#dc3232;color:#fff}.button.button-warning:hover,.button.button-warning:focus{background:#c92424;border-color:#c92424;color:#fff}.members-reset-spinner{float:none;margin-top:0;vertical-align:middle}.members-admin-menus-wrap{max-width:1400px}.members-admin-menus-toolbar{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;margin-bottom:16px}.members-am-copy-wrap label{margin-right:8px}.members-am-chips{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;gap:8px;margin-bottom:12px}.members-am-chip{border-radius:999px;padding:4px 12px;border:1px solid #c3c4c7;background:#f6f7f7;cursor:pointer}.members-am-chip.is-active{background:#2271b1;border-color:#2271b1;color:#fff}.members-am-carousel-wrap{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;gap:8px;margin-bottom:8px}.members-am-carousel-prev,.members-am-carousel-next{-ms-flex-item-align:center;align-self:center;width:36px;height:36px;border-radius:50%;border:1px solid #c3c4c7;background:#fff;cursor:pointer}.members-am-columns{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;gap:12px;min-height:320px;overflow:hidden}.members-am-column{-webkit-box-flex:1;-ms-flex:1;flex:1;min-width:0;background:#1d2327;border-radius:4px;color:#f0f0f1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.members-am-user-column{border:2px dashed #8c8f94}.members-am-sidebar-head{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:8px 10px;background:#2c3338;border-radius:4px 4px 0 0;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em}.members-am-col-move button{margin-left:4px;padding:0 6px;min-height:24px;line-height:22px;font-size:11px}.members-am-sidebar-list{padding:6px 0 12px;overflow-y:auto;max-height:480px}.members-am-item{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:6px 10px 6px 8px;cursor:pointer;border-left:3px solid transparent;font-size:13px}.members-am-item.is-sub{padding-left:22px;font-size:12px;opacity:.95}.members-am-item.is-hidden{opacity:.18}.members-am-item.is-no-cap{opacity:.35}.members-am-item.is-no-cap.is-hidden{opacity:.15}.members-am-item.is-selected{border-left-color:#2271b1;background:rgba(34,113,177,0.15)}.members-am-item-main{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;min-width:0}.members-am-item-main .dashicons{font-size:18px;width:18px;height:18px}.members-am-item-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.members-am-item-actions{display:none;gap:2px}.members-am-item:hover .members-am-item-actions{display:-webkit-box;display:-ms-flexbox;display:flex}.members-am-item-actions button{min-height:22px;padding:0 4px;line-height:20px;font-size:11px}.members-am-sep{text-align:center;opacity:.35;font-size:11px;padding:4px 0}.members-am-carousel-dots{text-align:center;margin-bottom:4px}.members-am-dot{width:8px;height:8px;border-radius:50%;border:0;margin:0 4px;padding:0;background:#c3c4c7;cursor:pointer}.members-am-dot.is-active{background:#2271b1}.members-am-carousel-status{text-align:center;color:#646970;font-size:12px;margin-top:0}.members-am-badge{font-size:9px;text-transform:uppercase;padding:1px 4px;border-radius:2px;margin-right:4px;vertical-align:middle}.members-am-badge-new{background:#2271b1;color:#fff}.members-am-badge-edit{background:#dba617;color:#1d2327}.members-am-badge-nocap{background:#8c8f94;color:#fff}.members-am-legend{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;gap:16px;margin:0 0 8px;padding:0;font-size:12px;color:#646970}.members-am-legend-item{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:4px}.members-am-edit-panel{margin-top:20px;padding:16px;border:1px solid #c3c4c7;background:#fff;border-radius:4px}.members-am-edit-panel-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:12px}.members-am-edit-panel-header h2{margin:0;font-size:16px}.members-am-edit-toolbar{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:12px;margin-bottom:12px}.members-am-edit-target-wrap select{margin-left:8px}.members-am-level-actions .button{margin-right:6px}.members-am-edit-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px}.members-am-edit-col label{display:block;font-weight:600;margin-bottom:4px}.members-am-icon-tabs{margin-bottom:8px}.members-am-icon-tabs .button{margin-right:4px;margin-bottom:4px}.members-am-icon-tabs .button.is-active{background:#2271b1;border-color:#2271b1;color:#fff}.members-am-icon-grid{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;gap:4px;max-height:160px;overflow-y:auto;border:1px solid #dcdcde;padding:8px;background:#f6f7f7;margin-bottom:8px}.members-am-icon-pick{width:36px;height:36px;padding:0;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;border:1px solid #c3c4c7;background:#fff;cursor:pointer}.members-am-vis-row{display:block;margin-bottom:6px;font-weight:normal}.members-am-reset-dropdown{position:absolute;z-index:100;background:#fff;border:1px solid #c3c4c7;border-radius:6px;padding:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-shadow:0 4px 12px rgba(0,0,0,0.15);box-shadow:0 4px 12px rgba(0,0,0,0.15);min-width:280px;top:100%;left:0;margin-top:4px;overflow:hidden}.members-am-reset-title{padding:10px 14px;font-weight:600;font-size:13px;border-bottom:1px solid #f0f0f0;color:#1d2327}.members-am-reset-option{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;gap:10px;padding:12px 14px;border:0;background:0;cursor:pointer;text-align:left;width:100%;border-bottom:1px solid #f0f0f0;-webkit-transition:background .15s;transition:background .15s}.members-am-reset-option:last-child{border-bottom:0}.members-am-reset-option:hover{background:#f6f7f7}.members-am-reset-option .dashicons{margin-top:2px;color:#646970}.members-am-reset-option-text{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;gap:2px}.members-am-reset-option-text strong{font-size:13px;color:#1d2327}.members-am-reset-option-text small{font-size:12px;color:#646970}.members-am-reset-danger:hover{background:#fcf0f1}.members-am-reset-danger .dashicons{color:#d63638}.members-am-reset-danger:hover strong{color:#d63638}@media screen and (max-width:782px){.members-am-columns{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.members-am-user-suggestions{position:absolute;z-index:100;background:#fff;border:1px solid #c3c4c7;border-radius:4px;max-height:200px;overflow-y:auto;width:100%;-webkit-box-shadow:0 2px 6px rgba(0,0,0,0.15);box-shadow:0 2px 6px rgba(0,0,0,0.15);top:100%;left:0}.members-am-user-suggestion{padding:8px 12px;cursor:pointer;font-size:13px;border-bottom:1px solid #f0f0f0}.members-am-user-suggestion:hover{background:#f0f6fc}.members-am-user-suggestion:last-child{border-bottom:0} \ No newline at end of file diff --git a/js/admin-menus.js b/js/admin-menus.js new file mode 100644 index 0000000..bc0504b --- /dev/null +++ b/js/admin-menus.js @@ -0,0 +1,1448 @@ +/** + * Members — Admin Menus add-on settings UI. + */ +(function ($) { + 'use strict'; + + var state = { + settings: $.extend(true, {}, membersAdminMenus.settings), + tree: [], + activeRoleSlugs: [], + carouselPage: 0, + columnsPerPage: 3, + selectedId: null, + iconTab: 'dashicons', + previewUserId: null, + previewUserRoles: [], + userSuggestions: [], + mediaFrame: null, + syncScroll: (function () { + try { return localStorage.getItem('members_am_sync_scroll') !== '0'; } catch (e) { return true; } + })(), + }; + + var DASHICONS = [ + 'dashicons-menu', 'dashicons-admin-dashboard', 'dashicons-admin-post', 'dashicons-admin-page', + 'dashicons-admin-media', 'dashicons-admin-comments', 'dashicons-admin-appearance', 'dashicons-admin-plugins', + 'dashicons-admin-users', 'dashicons-admin-tools', 'dashicons-admin-settings', 'dashicons-admin-generic', + 'dashicons-edit', 'dashicons-plus', 'dashicons-chart-bar', 'dashicons-cart', 'dashicons-products', + 'dashicons-email', 'dashicons-groups', 'dashicons-heart', 'dashicons-star-filled', 'dashicons-smiley', + 'dashicons-info', 'dashicons-lock', 'dashicons-unlock', 'dashicons-visibility', 'dashicons-hidden', + 'dashicons-arrow-up', 'dashicons-arrow-down', 'dashicons-admin-network', 'dashicons-performance', + ]; + + var FA_ICONS = [ + 'fa-solid fa-house', 'fa-solid fa-user', 'fa-solid fa-gear', 'fa-solid fa-file', 'fa-solid fa-image', + 'fa-solid fa-cart-shopping', 'fa-solid fa-chart-line', 'fa-solid fa-envelope', 'fa-solid fa-book', + 'fa-solid fa-link', 'fa-solid fa-bell', 'fa-solid fa-star', 'fa-solid fa-heart', 'fa-solid fa-lock', + 'fa-solid fa-unlock', 'fa-solid fa-pen', 'fa-solid fa-trash', 'fa-solid fa-plus', 'fa-solid fa-minus', + ]; + + var VIEW_STORAGE_KEY = 'members_am_view_state'; + + function saveViewState() { + try { + localStorage.setItem(VIEW_STORAGE_KEY, JSON.stringify({ + activeRoleSlugs: state.activeRoleSlugs, + carouselPage: state.carouselPage, + })); + } catch (e) {} + } + + function loadViewState() { + try { + var raw = localStorage.getItem(VIEW_STORAGE_KEY); + if (raw) { + return JSON.parse(raw); + } + } catch (e) {} + return null; + } + + function deepClone(o) { + return JSON.parse(JSON.stringify(o)); + } + + function getRolesList() { + return membersAdminMenus.roles || []; + } + + function ensureSettings() { + if (!state.settings._meta || Array.isArray(state.settings._meta)) { + state.settings._meta = { version: 3, admin_editable: false }; + } + if (!state.settings.roles || Array.isArray(state.settings.roles)) { + state.settings.roles = {}; + } + if (!state.settings.users || Array.isArray(state.settings.users)) { + state.settings.users = {}; + } + if (!Array.isArray(state.settings.custom_items)) { + state.settings.custom_items = []; + } + if (!state.settings.capabilities || Array.isArray(state.settings.capabilities)) { + state.settings.capabilities = {}; + } + } + + function getRoleConfig(role) { + ensureSettings(); + if (!state.settings.roles[role]) { + state.settings.roles[role] = { hidden: [], order: [], submenu_order: {}, overrides: {} }; + } + var r = state.settings.roles[role]; + if (!r.hidden || !Array.isArray(r.hidden)) { + r.hidden = []; + } + if (!r.order || !Array.isArray(r.order)) { + r.order = []; + } + if (!r.submenu_order || Array.isArray(r.submenu_order)) { + r.submenu_order = {}; + } + if (!r.overrides || Array.isArray(r.overrides)) { + r.overrides = {}; + } + return r; + } + + function getUserConfig(uid) { + ensureSettings(); + if (!state.settings.users[uid]) { + state.settings.users[uid] = {}; + } + var u = state.settings.users[uid]; + if (!u.hidden || !Array.isArray(u.hidden)) u.hidden = []; + if (!u.order || !Array.isArray(u.order)) u.order = []; + if (!u.overrides || Array.isArray(u.overrides)) u.overrides = {}; + if (!u.submenu_order || Array.isArray(u.submenu_order)) u.submenu_order = {}; + return u; + } + + function getTopOrderForUser(uid) { + var ucfg = getUserConfig(uid); + if (ucfg.order && ucfg.order.length) return ucfg.order; + return defaultTopOrder(); + } + + function getSubOrderForUser(uid, parentId, children) { + var ucfg = getUserConfig(uid); + if (ucfg.submenu_order && ucfg.submenu_order[parentId]) { + return ucfg.submenu_order[parentId]; + } + return children.map(function (c) { return c.id; }); + } + + function isUserHidden(uid, itemId) { + var ucfg = getUserConfig(uid); + if (ucfg.hidden.indexOf(itemId) !== -1) return true; + var parentId = findParentId(itemId); + if (parentId && ucfg.hidden.indexOf(parentId) !== -1) return true; + return false; + } + + function toggleUserHidden(uid, itemId) { + var ucfg = getUserConfig(uid); + var idx = ucfg.hidden.indexOf(itemId); + var node = findNode(itemId); + if (idx === -1) { + ucfg.hidden.push(itemId); + if (node && node.children) { + node.children.forEach(function (c) { + if (ucfg.hidden.indexOf(c.id) === -1) ucfg.hidden.push(c.id); + }); + } + } else { + ucfg.hidden.splice(idx, 1); + if (node && node.children) { + node.children.forEach(function (c) { + var ci = ucfg.hidden.indexOf(c.id); + if (ci !== -1) ucfg.hidden.splice(ci, 1); + }); + } + } + } + + function moveUserItem(uid, itemId, parentId, direction) { + var ucfg = getUserConfig(uid); + var arr; + if (parentId) { + if (!ucfg.submenu_order[parentId]) { + var node = findNode(parentId); + ucfg.submenu_order[parentId] = (node && node.children) ? node.children.map(function (c) { return c.id; }) : []; + } + arr = ucfg.submenu_order[parentId]; + } else { + if (!ucfg.order.length) { + ucfg.order = defaultTopOrder(); + } + arr = ucfg.order; + } + var idx = arr.indexOf(itemId); + if (idx === -1) return; + var newIdx = idx + direction; + if (newIdx < 0 || newIdx >= arr.length) return; + arr.splice(idx, 1); + arr.splice(newIdx, 0, itemId); + } + + function customHookId(item) { + var id = item.id || 'c'; + return 'members-am-' + String(id).replace(/[^a-z0-9_-]/gi, '-').toLowerCase(); + } + + function buildTreeWithCustoms() { + var base = $.extend(true, [], membersAdminMenus.menuTree || []); + // Build a set of existing IDs so we don't add duplicates. + var existingIds = {}; + base.forEach(function (n) { existingIds[n.id] = true; }); + (state.settings.custom_items || []).forEach(function (item) { + if (!item || !item.id) { + return; + } + var hookId = customHookId(item); + if (existingIds[hookId]) { + // Already present in the base tree (injected by PHP). + // Just flag it as custom so badges and remove work. + for (var i = 0; i < base.length; i++) { + if (base[i].id === hookId) { + base[i].custom = true; + base[i].customId = item.id; + break; + } + } + return; + } + base.push({ + id: hookId, + title: item.label || 'Custom', + icon: item.icon || 'dashicons-admin-generic', + type: 'top', + custom: true, + customId: item.id, + children: [], + }); + }); + return base; + } + + function findNode(id, nodes) { + nodes = nodes || state.tree; + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].id === id) { + return nodes[i]; + } + if (nodes[i].children && nodes[i].children.length) { + var f = findNode(id, nodes[i].children); + if (f) { + return f; + } + } + } + return null; + } + + function findParentId(childId) { + for (var i = 0; i < state.tree.length; i++) { + var n = state.tree[i]; + if (!n.children) { + continue; + } + for (var j = 0; j < n.children.length; j++) { + if (n.children[j].id === childId) { + return n.id; + } + } + } + return null; + } + + function findChildNode(parent, childId) { + if (!parent || !parent.children) return null; + for (var i = 0; i < parent.children.length; i++) { + if (parent.children[i].id === childId) return parent.children[i]; + } + return null; + } + + function defaultTopOrder() { + return state.tree.map(function (n) { + return n.id; + }); + } + + function defaultChildSlugs(parentId) { + var node = findNode(parentId); + if (!node || !node.children) { + return []; + } + return node.children.map(function (c) { + var parts = String(c.id).split('::'); + return parts.length > 1 ? parts[1] : c.id; + }); + } + + function getTopOrder(role) { + var def = defaultTopOrder(); + var o = getRoleConfig(role).order; + if (!o || !o.length) { + return def.slice(); + } + var merged = o.filter(function (id) { + return id.indexOf('sep-') === 0 || findNode(id); + }); + def.forEach(function (id) { + if (merged.indexOf(id) === -1) { + merged.push(id); + } + }); + return merged; + } + + function getChildOrder(role, parentId) { + var def = defaultChildSlugs(parentId); + var so = getRoleConfig(role).submenu_order[parentId]; + if (!so || !so.length) { + return def.slice(); + } + var merged = so.filter(function (slug) { + return def.indexOf(slug) !== -1; + }); + def.forEach(function (slug) { + if (merged.indexOf(slug) === -1) { + merged.push(slug); + } + }); + return merged; + } + + function childFullId(parentId, childSlug) { + return parentId + '::' + childSlug; + } + + function isHidden(role, itemId) { + var h = getRoleConfig(role).hidden; + if (h.indexOf(itemId) !== -1) { + return true; + } + // If this is a sub-item, also check if its parent is hidden. + var parentId = findParentId(itemId); + if (parentId && h.indexOf(parentId) !== -1) { + return true; + } + return false; + } + + function roleHasCap(role, cap) { + if (!cap || cap === 'read') return true; + if (role === 'administrator') return true; + var caps = membersAdminMenus.roleCaps && membersAdminMenus.roleCaps[role]; + if (!caps) return false; + return caps.indexOf(cap) !== -1; + } + + function userHasCap(cap) { + if (!cap || cap === 'read') return true; + var roles = state.previewUserRoles || []; + for (var i = 0; i < roles.length; i++) { + if (roleHasCap(roles[i], cap)) return true; + } + return false; + } + + function toggleHidden(role, itemId) { + var h = getRoleConfig(role).hidden; + var i = h.indexOf(itemId); + if (i === -1) { + // Hiding: add this item. + h.push(itemId); + // If it's a top-level item, also hide all its children. + var node = findNode(itemId); + if (node && node.children && node.children.length) { + node.children.forEach(function (child) { + if (h.indexOf(child.id) === -1) { + h.push(child.id); + } + }); + } + } else { + // Showing: remove this item. + h.splice(i, 1); + // If it's a top-level item, also show all its children. + var node = findNode(itemId); + if (node && node.children && node.children.length) { + node.children.forEach(function (child) { + var ci = h.indexOf(child.id); + if (ci !== -1) { + h.splice(ci, 1); + } + }); + } + } + } + + function getTargetRole() { + var v = $('#members-am-edit-target-role').val(); + return v || (state.activeRoleSlugs[0] || ''); + } + + function getTargetRoles() { + var v = $('#members-am-edit-target-role').val(); + if (v && v.indexOf('__user__') === 0) { + return []; + } + if (v === '__all__') { + return getRolesList().map(function (r) { return r.slug; }); + } + return [v || (state.activeRoleSlugs[0] || '')]; + } + + function getTargetUserId() { + var v = $('#members-am-edit-target-role').val(); + if (v && v.indexOf('__user__') === 0) { + return parseInt(v.replace('__user__', ''), 10); + } + return null; + } + + function getOverrideForEdit() { + if (!state.selectedId) { + return null; + } + var targetUser = getTargetUserId(); + if (targetUser) { + var ucfg = getUserConfig(targetUser); + return (ucfg.overrides && ucfg.overrides[state.selectedId]) || {}; + } + var roles = getTargetRoles(); + var role = roles[0]; + if (!role) { + return null; + } + var o = getRoleConfig(role).overrides[state.selectedId]; + return o || {}; + } + + function setOverrideField(field, value) { + if (!state.selectedId) { + return; + } + var targetUser = getTargetUserId(); + if (targetUser) { + var ucfg = getUserConfig(targetUser); + if (!ucfg.overrides[state.selectedId]) { + ucfg.overrides[state.selectedId] = {}; + } + if (value === '' || value === null) { + delete ucfg.overrides[state.selectedId][field]; + } else { + ucfg.overrides[state.selectedId][field] = value; + } + renderColumns(); + return; + } + var roles = getTargetRoles(); + if (!roles.length) { + return; + } + roles.forEach(function (role) { + var rc = getRoleConfig(role); + if (!rc.overrides[state.selectedId]) { + rc.overrides[state.selectedId] = {}; + } + if (value === '' || value === null) { + delete rc.overrides[state.selectedId][field]; + } else { + rc.overrides[state.selectedId][field] = value; + } + }); + } + + function initActiveRoles() { + var saved = loadViewState(); + var allSlugs = getRolesList().map(function (r) { return r.slug; }); + var adminOk = !!state.settings._meta.admin_editable; + + if (saved && Array.isArray(saved.activeRoleSlugs) && saved.activeRoleSlugs.length) { + // Restore saved selection, but filter out roles that no longer exist + // or administrator if editing is now disabled. + var restored = saved.activeRoleSlugs.filter(function (slug) { + if (allSlugs.indexOf(slug) === -1) { + return false; + } + if (slug === 'administrator' && !adminOk) { + return false; + } + return true; + }); + if (restored.length) { + state.activeRoleSlugs = restored; + state.carouselPage = (typeof saved.carouselPage === 'number') ? saved.carouselPage : 0; + // Clamp carousel page to valid range. + var maxPage = Math.max(0, Math.ceil(state.activeRoleSlugs.length / state.columnsPerPage) - 1); + if (state.carouselPage > maxPage) { + state.carouselPage = maxPage; + } + return; + } + } + + // Default: all eligible roles active. + state.activeRoleSlugs = allSlugs.filter(function (slug) { + if (slug === 'administrator') { + return adminOk; + } + return true; + }); + if (!state.activeRoleSlugs.length) { + state.activeRoleSlugs = ['subscriber']; + } + } + + function renderChips() { + var $c = $('#members-am-role-chips').empty(); + getRolesList().forEach(function (r) { + if (r.slug === 'administrator' && !state.settings._meta.admin_editable) { + return; + } + var on = state.activeRoleSlugs.indexOf(r.slug) !== -1; + var $chip = $('') + .on('click', function () { + state.previewUserId = null; + state.previewUserLabel = null; + state.previewUserRoles = []; + renderAll(); + }) + ); + $uc.append($head); + + var $list = $('
'); + var ucfg = getUserConfig(uid); + var topOrder = getTopOrderForUser(uid); + + topOrder.forEach(function (nodeId) { + if (nodeId.indexOf('sep-') === 0) { + $list.append($('
').text('——')); + return; + } + var node = findNode(nodeId); + if (!node) return; + $list.append(renderUserItemRow(node, null, uid, ucfg)); + var children = node.children || []; + var subOrder = getSubOrderForUser(uid, nodeId, children); + subOrder.forEach(function (cid) { + var child = findChildNode(node, cid); + if (!child) return; + $list.append(renderUserItemRow(child, nodeId, uid, ucfg)); + }); + }); + + $uc.append($list); + $cols.append($uc); + } + if (state.syncScroll) { + var $lists = $cols.find('.members-am-sidebar-list'); + var syncing = false; + $lists.on('scroll', function () { + if (syncing) return; + syncing = true; + var scrollTop = $(this).scrollTop(); + $lists.not(this).scrollTop(scrollTop); + syncing = false; + }); + } + + renderCarouselStatus(); + } + + function renderCopySelect() { + var $from = $('#members-am-copy-from').empty(); + var $to = $('#members-am-copy-to').empty(); + var roles = getRolesList(); + roles.forEach(function (r) { + $from.append($('