Admin Menu Editor - Version 1.7.2

Version Description

  • Added capability suggestions and access preview to the "Extra capability" dropdown.
  • The plugin now remembers the last selected menu item and re-selects it after you save changes.
  • Fixed a layout issue where menus with very long titles would appear incorrectly in the menu editor.
  • When you change the menu title, the window title will also be changed to match it. You can still edit the window title separately if necessary.
  • Moved the "Icon URL" field up and moved "Window title" down.
Download this release

Release Info

Developer whiteshadow
Plugin Icon 128x128 Admin Menu Editor
Version 1.7.2
Comparing to
See all releases

Code changes from version 1.7.1 to 1.7.2

css/menu-editor.css CHANGED
@@ -1336,6 +1336,30 @@ a#ws-ame-delete-color-preset:hover {
1336
  #ws-ame-copy-permissions-dialog select {
1337
  min-width: 280px; }
1338
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1339
  #ws_sidebar_pro_ad {
1340
  min-width: 225px;
1341
  margin-top: 5px;
1336
  #ws-ame-copy-permissions-dialog select {
1337
  min-width: 280px; }
1338
 
1339
+ /*********************************************
1340
+ Capability suggestions and preview
1341
+ **********************************************/
1342
+ #ws_capability_suggestions {
1343
+ padding: 4px;
1344
+ width: 350px;
1345
+ border: 1px solid #cdd5d5;
1346
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
1347
+ background: #fff;
1348
+ border-top-right-radius: 3px;
1349
+ border-bottom-right-radius: 3px; }
1350
+ #ws_capability_suggestions #ws_previewed_caps {
1351
+ margin-top: 0;
1352
+ margin-bottom: 6px; }
1353
+ #ws_capability_suggestions td, #ws_capability_suggestions th {
1354
+ padding-top: 3px;
1355
+ padding-bottom: 3px; }
1356
+ #ws_capability_suggestions tr.ws_preview_has_access .ws_ame_role_name {
1357
+ background-color: lightgreen; }
1358
+ #ws_capability_suggestions .ws_ame_suggested_capability {
1359
+ cursor: pointer; }
1360
+ #ws_capability_suggestions .ws_ame_suggested_capability:hover {
1361
+ background-color: #d0f2d0; }
1362
+
1363
  #ws_sidebar_pro_ad {
1364
  min-width: 225px;
1365
  margin-top: 5px;
css/menu-editor.scss CHANGED
@@ -692,10 +692,11 @@ select.ws_dropdown {
692
  font-size: 12px;
693
  }
694
 
 
695
  select.ws_dropdown option {
696
  font-family : "Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif;
697
  font-size: 12px;
698
- padding: 3px;
699
  }
700
 
701
  select.ws_dropdown optgroup option {
@@ -1878,6 +1879,44 @@ $userSelectionPanelPadding: 10px;
1878
  min-width: 280px;
1879
  }
1880
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1881
 
1882
 
1883
  #ws_sidebar_pro_ad {
692
  font-size: 12px;
693
  }
694
 
695
+ $dropdownOptionPaddingTop: 3px;
696
  select.ws_dropdown option {
697
  font-family : "Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif;
698
  font-size: 12px;
699
+ padding: $dropdownOptionPaddingTop;
700
  }
701
 
702
  select.ws_dropdown optgroup option {
1879
  min-width: 280px;
1880
  }
1881
 
1882
+ /*********************************************
1883
+ Capability suggestions and preview
1884
+ **********************************************/
1885
+
1886
+ #ws_capability_suggestions {
1887
+ padding: 4px;
1888
+ width: 350px;
1889
+
1890
+ border: 1px solid #cdd5d5;
1891
+ box-shadow: 0 1px 1px rgba(0,0,0,0.04);
1892
+ background: #fff;
1893
+
1894
+ border-top-right-radius: 3px;
1895
+ border-bottom-right-radius: 3px;
1896
+
1897
+ #ws_previewed_caps {
1898
+ margin-top: 0;
1899
+ margin-bottom: 6px;
1900
+ }
1901
+
1902
+ td, th {
1903
+ //For consistency, padding should match the capability dropdown.
1904
+ padding-top: $dropdownOptionPaddingTop;
1905
+ padding-bottom: $dropdownOptionPaddingTop;
1906
+ }
1907
+
1908
+ tr.ws_preview_has_access .ws_ame_role_name{
1909
+ background-color: lightgreen;
1910
+ }
1911
+
1912
+ .ws_ame_suggested_capability {
1913
+ cursor: pointer;
1914
+
1915
+ &:hover {
1916
+ background-color: #d0f2d0;
1917
+ }
1918
+ }
1919
+ }
1920
 
1921
 
1922
  #ws_sidebar_pro_ad {
includes/cap-suggestion-box.php ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div id="ws_capability_suggestions" style="display: none;">
2
+ <p id="ws_previewed_caps">&nbsp;</p>
3
+ <table class="widefat striped">
4
+ <thead>
5
+ <tr>
6
+ <th class="ws_ame_role_name">Role</th>
7
+ <th>Suggestion</th>
8
+ </tr>
9
+ </thead>
10
+ <tbody>
11
+ <tr><td colspan="2">This table will be populated by JavaScript</td></tr>
12
+ </tbody>
13
+ </table>
14
+ </div>
includes/capabilities/cap-power.csv ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Capability;Power;Super Admin;Administrator;Editor;Author;Contributor;Subscriber
2
+ manage_network;20;Y;;;;;
3
+ manage_sites;20;Y;;;;;
4
+ manage_network_users;20;Y;;;;;
5
+ manage_network_plugins;20;Y;;;;;
6
+ manage_network_themes;20;Y;;;;;
7
+ manage_network_options;20;Y;;;;;
8
+ install_plugins;10;Y;Y (single site);;;;
9
+ install_themes;10;Y;Y (single site);;;;
10
+ edit_plugins;10;Y;Y (single site);;;;
11
+ edit_themes;10;Y;Y (single site);;;;
12
+ delete_plugins;8;Y;Y (single site);;;;
13
+ delete_themes;8;Y;Y (single site);;;;
14
+ update_core;7;Y;Y (single site);;;;
15
+ update_plugins;7;Y;Y (single site);;;;
16
+ update_themes;7;Y;Y (single site);;;;
17
+ create_users;7;Y;Y (single site);;;;
18
+ delete_users;7;Y;Y (single site);;;;
19
+ edit_users;7;Y;Y (single site);;;;
20
+ activate_plugins;6;Y;Y (single site or enabled by network setting);;;;
21
+ edit_theme_options;5;Y;Y;;;;
22
+ export;5;Y;Y;;;;
23
+ import;5;Y;Y;;;;
24
+ list_users;5;Y;Y;;;;
25
+ manage_options;5;Y;Y;;;;
26
+ promote_users;5;Y;Y;;;;
27
+ remove_users;5;Y;Y;;;;
28
+ switch_themes;5;Y;Y;;;;
29
+ moderate_comments;4;Y;Y;Y;;;
30
+ manage_categories;4;Y;Y;Y;;;
31
+ manage_links;4;Y;Y;Y;;;
32
+ edit_others_posts;4;Y;Y;Y;;;
33
+ edit_pages;4;Y;Y;Y;;;
34
+ edit_others_pages;4;Y;Y;Y;;;
35
+ edit_published_pages;4;Y;Y;Y;;;
36
+ publish_pages;4;Y;Y;Y;;;
37
+ delete_pages;4;Y;Y;Y;;;
38
+ delete_others_pages;4;Y;Y;Y;;;
39
+ delete_published_pages;4;Y;Y;Y;;;
40
+ delete_others_posts;4;Y;Y;Y;;;
41
+ delete_private_posts;4;Y;Y;Y;;;
42
+ edit_private_posts;4;Y;Y;Y;;;
43
+ read_private_posts;4;Y;Y;Y;;;
44
+ delete_private_pages;4;Y;Y;Y;;;
45
+ edit_private_pages;4;Y;Y;Y;;;
46
+ read_private_pages;4;Y;Y;Y;;;
47
+ unfiltered_html;4,2;Y;Y;Y;;;
48
+ edit_published_posts;3;Y;Y;Y;Y;;
49
+ upload_files;3;Y;Y;Y;Y;;
50
+ publish_posts;3;Y;Y;Y;Y;;
51
+ delete_published_posts;3;Y;Y;Y;Y;;
52
+ edit_posts;2;Y;Y;Y;Y;Y;
53
+ delete_posts;2;Y;Y;Y;Y;Y;
54
+ read;1;Y;Y;Y;Y;Y;Y
includes/editor-page.php CHANGED
@@ -236,6 +236,13 @@ function ame_output_sort_buttons($icons) {
236
  <input type="hidden" name="data" id="ws_data" value="">
237
  <input type="hidden" name="data_length" id="ws_data_length" value="">
238
  <input type="hidden" name="selected_actor" id="ws_selected_actor" value="">
 
 
 
 
 
 
 
239
  <input type="button" id='ws_save_menu' class="button-primary ws_main_button" value="Save Changes" />
240
  </form>
241
 
@@ -264,15 +271,16 @@ function ame_output_sort_buttons($icons) {
264
 
265
  if ( $show_pro_benefits ):
266
  $benefit_variations = array(
267
- 'Simplified, role-based permissions.',
268
- 'Role-based menu permissions.',
269
- 'Simpler, role-based permissions.',
 
270
  );
271
  //Pseudo-randomly select one phrase based on the site URL.
272
- $variation_index = hexdec( substr(md5(get_site_url()), -1) ) % count($benefit_variations);
273
  $selected_variation = $benefit_variations[$variation_index];
274
 
275
- $pro_version_link = 'http://adminmenueditor.com/upgrade-to-pro/?utm_source=Admin%2BMenu%2BEditor%2Bfree&utm_medium=text_link&utm_content=sidebar_link_cv' . $variation_index . '&utm_campaign=Plugins';
276
  ?>
277
  <div class="clear"></div>
278
 
@@ -281,9 +289,11 @@ function ame_output_sort_buttons($icons) {
281
  <div class="ws_hint_content">
282
  <strong>Upgrade to Pro:</strong>
283
  <ul>
 
 
 
 
284
  <li><?php echo $selected_variation; ?></li>
285
- <li>Drag items between menu levels.</li>
286
- <li>Menu export &amp; import.</li>
287
  </ul>
288
  <a href="<?php echo esc_attr($pro_version_link); ?>" target="_blank">Learn more</a>
289
  |
@@ -551,6 +561,8 @@ function ame_output_sort_buttons($icons) {
551
  </div>
552
  </div>
553
 
 
 
554
  <?php
555
  if ( $is_pro_version ) {
556
  include $extrasDirectory . '/page-dropdown.php';
236
  <input type="hidden" name="data" id="ws_data" value="">
237
  <input type="hidden" name="data_length" id="ws_data_length" value="">
238
  <input type="hidden" name="selected_actor" id="ws_selected_actor" value="">
239
+
240
+ <input type="hidden" name="selected_menu_url" id="ws_selected_menu_url" value="">
241
+ <input type="hidden" name="selected_submenu_url" id="ws_selected_submenu_url" value="">
242
+
243
+ <input type="hidden" name="expand_menu" id="ws_expand_selected_menu" value="">
244
+ <input type="hidden" name="expand_submenu" id="ws_expand_selected_submenu" value="">
245
+
246
  <input type="button" id='ws_save_menu' class="button-primary ws_main_button" value="Save Changes" />
247
  </form>
248
 
271
 
272
  if ( $show_pro_benefits ):
273
  $benefit_variations = array(
274
+ 'Drag items between menu levels.',
275
+ 'More menu icons.',
276
+ 'Make menus open in a new tab or an iframe.',
277
+ 'Prevent users from deleting a specific user.',
278
  );
279
  //Pseudo-randomly select one phrase based on the site URL.
280
+ $variation_index = hexdec( substr(md5(get_site_url() . 'ab'), -2) ) % count($benefit_variations);
281
  $selected_variation = $benefit_variations[$variation_index];
282
 
283
+ $pro_version_link = 'http://adminmenueditor.com/upgrade-to-pro/?utm_source=Admin%2BMenu%2BEditor%2Bfree&utm_medium=text_link&utm_content=sidebar_link_nv' . $variation_index . '&utm_campaign=Plugins';
284
  ?>
285
  <div class="clear"></div>
286
 
289
  <div class="ws_hint_content">
290
  <strong>Upgrade to Pro:</strong>
291
  <ul>
292
+ <li>Role-based menu permissions.</li>
293
+ <li>Hide items from specific users.</li>
294
+ <li>Menu import and export.</li>
295
+ <li>Change menu colors.</li>
296
  <li><?php echo $selected_variation; ?></li>
 
 
297
  </ul>
298
  <a href="<?php echo esc_attr($pro_version_link); ?>" target="_blank">Learn more</a>
299
  |
561
  </div>
562
  </div>
563
 
564
+ <?php include dirname(__FILE__) . '/cap-suggestion-box.php'; ?>
565
+
566
  <?php
567
  if ( $is_pro_version ) {
568
  include $extrasDirectory . '/page-dropdown.php';
includes/menu-editor-core.php CHANGED
@@ -754,6 +754,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
754
  'roles' => $roles,
755
  'users' => $users,
756
  'isMultisite' => is_multisite(),
 
757
  );
758
  wp_localize_script('ame-actor-manager', 'wsAmeActorData', $actor_data);
759
 
@@ -821,6 +822,11 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
821
  'getPagesNonce' => wp_create_nonce('ws_ame_get_pages'),
822
  'getPageDetailsNonce' => wp_create_nonce('ws_ame_get_page_details'),
823
 
 
 
 
 
 
824
  'isDemoMode' => defined('IS_DEMO_MODE'),
825
  'isMasterMode' => defined('IS_MASTER_MODE'),
826
  );
@@ -1083,15 +1089,34 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1083
  return $admin_title;
1084
  }
1085
 
 
 
1086
  //Check if the we have a custom title for this page.
1087
  $default_title = isset($item['defaults']['page_title']) ? $item['defaults']['page_title'] : '';
1088
  if ( !empty($item['page_title']) && $item['page_title'] != $default_title ) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1089
  if ( empty($title) ) {
1090
- $admin_title = $item['page_title'] . $admin_title;
1091
  } else {
1092
  //Replace the first occurrence of the default title with the custom one.
1093
  $title_pos = strpos($admin_title, $title);
1094
- $admin_title = substr_replace($admin_title, $item['page_title'], $title_pos, strlen($title));
1095
  }
1096
  }
1097
 
@@ -1918,11 +1943,19 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1918
  $this->set_custom_menu($menu);
1919
 
1920
  //Redirect back to the editor and display the success message.
1921
- //Also, automatically select the last selected actor (convenience feature).
1922
  $query = array('message' => 1);
1923
- if ( isset($post['selected_actor']) && !empty($post['selected_actor']) ) {
1924
- $query['selected_actor'] = rawurlencode(strval($post['selected_actor']));
 
 
 
 
 
 
 
 
1925
  }
 
1926
  wp_redirect( add_query_arg($query, $url) );
1927
  die();
1928
  } else {
@@ -3394,6 +3427,36 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
3394
  return '';
3395
  }
3396
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3397
  } //class
3398
 
3399
 
@@ -3493,13 +3556,23 @@ class ameMenuTemplateBuilder {
3493
 
3494
  /**
3495
  * Sanitize a menu title for display.
3496
- * Removes HTML tags and update notification bubbles.
3497
  *
3498
  * @param string $title
3499
  * @return string
3500
  */
3501
  private function sanitizeMenuTitle($title) {
3502
- return strip_tags( preg_replace('@<span[^>]*>.*</span>@i', '', $title) );
 
 
 
 
 
 
 
 
 
 
3503
  }
3504
 
3505
  public function getRelativeTemplateOrder() {
754
  'roles' => $roles,
755
  'users' => $users,
756
  'isMultisite' => is_multisite(),
757
+ 'capPower' => $this->load_cap_power(),
758
  );
759
  wp_localize_script('ame-actor-manager', 'wsAmeActorData', $actor_data);
760
 
822
  'getPagesNonce' => wp_create_nonce('ws_ame_get_pages'),
823
  'getPageDetailsNonce' => wp_create_nonce('ws_ame_get_page_details'),
824
 
825
+ 'selectedMenu' => isset($this->get['selected_menu_url']) ? strval($this->get['selected_menu_url']) : null,
826
+ 'selectedSubmenu' => isset($this->get['selected_submenu_url']) ? strval($this->get['selected_submenu_url']) : null,
827
+ 'expandSelectedMenu' => isset($this->get['expand_menu']) && ($this->get['expand_menu'] === '1'),
828
+ 'expandSelectedSubmenu' => isset($this->get['expand_submenu']) && ($this->get['expand_submenu'] === '1'),
829
+
830
  'isDemoMode' => defined('IS_DEMO_MODE'),
831
  'isMasterMode' => defined('IS_MASTER_MODE'),
832
  );
1089
  return $admin_title;
1090
  }
1091
 
1092
+ $custom_title = null;
1093
+
1094
  //Check if the we have a custom title for this page.
1095
  $default_title = isset($item['defaults']['page_title']) ? $item['defaults']['page_title'] : '';
1096
  if ( !empty($item['page_title']) && $item['page_title'] != $default_title ) {
1097
+ $custom_title = $item['page_title'];
1098
+ }
1099
+
1100
+ //Alternatively, use the custom menu title if the default page title is empty (as is usually
1101
+ //the case with core menus) or matches the default menu title (which is typical for plugins).
1102
+ //This saves the user a little bit of time, and, presumably, they'd want the titles to match.
1103
+ $default_menu_title = isset($item['defaults']['menu_title']) ? $item['defaults']['menu_title'] : '';
1104
+ if (
1105
+ !isset($custom_title)
1106
+ && !empty($item['menu_title'])
1107
+ && ($item['menu_title'] !== $default_menu_title)
1108
+ && (($default_menu_title === $default_title) || ($default_title === ''))
1109
+ ) {
1110
+ $custom_title = strip_tags($item['menu_title']);
1111
+ }
1112
+
1113
+ if ( isset($custom_title) ) {
1114
  if ( empty($title) ) {
1115
+ $admin_title = $custom_title . $admin_title;
1116
  } else {
1117
  //Replace the first occurrence of the default title with the custom one.
1118
  $title_pos = strpos($admin_title, $title);
1119
+ $admin_title = substr_replace($admin_title, $custom_title, $title_pos, strlen($title));
1120
  }
1121
  }
1122
 
1943
  $this->set_custom_menu($menu);
1944
 
1945
  //Redirect back to the editor and display the success message.
 
1946
  $query = array('message' => 1);
1947
+
1948
+ //Also, automatically select the last selected actor and menu (convenience feature).
1949
+ $pass_through_params = array(
1950
+ 'selected_actor', 'selected_menu_url', 'selected_submenu_url',
1951
+ 'expand_menu', 'expand_submenu',
1952
+ );
1953
+ foreach($pass_through_params as $param) {
1954
+ if ( isset($post[$param]) && !empty($post[$param]) ) {
1955
+ $query[$param] = rawurlencode(strval($post[$param]));
1956
+ }
1957
  }
1958
+
1959
  wp_redirect( add_query_arg($query, $url) );
1960
  die();
1961
  } else {
3427
  return '';
3428
  }
3429
 
3430
+ /**
3431
+ * @return array
3432
+ */
3433
+ private function load_cap_power() {
3434
+ $cap_power = array();
3435
+
3436
+ $power_filename = AME_ROOT_DIR . '/includes/capabilities/cap-power.csv';
3437
+ if ( is_file($power_filename) && is_readable($power_filename) ) {
3438
+ $csv = fopen($power_filename, 'r');
3439
+ $firstLineSkipped = false;
3440
+
3441
+ while ($csv && !feof($csv)) {
3442
+ $line = fgetcsv($csv, 1000, ';');
3443
+ if ( !$firstLineSkipped ) {
3444
+ $firstLineSkipped = true;
3445
+ continue;
3446
+ }
3447
+
3448
+ if ( count($line) >= 2 ) {
3449
+ $cap_power[strval($line[0])] = floatval(str_replace(',', '.', $line[1]));
3450
+ }
3451
+ }
3452
+ fclose($csv);
3453
+
3454
+ arsort($cap_power);
3455
+ }
3456
+
3457
+ return $cap_power;
3458
+ }
3459
+
3460
  } //class
3461
 
3462
 
3556
 
3557
  /**
3558
  * Sanitize a menu title for display.
3559
+ * Removes HTML tags and update notification bubbles. Truncates long titles.
3560
  *
3561
  * @param string $title
3562
  * @return string
3563
  */
3564
  private function sanitizeMenuTitle($title) {
3565
+ $title = strip_tags( preg_replace('@<span[^>]*>.*</span>@i', '', $title) );
3566
+
3567
+ //Compact whitespace.
3568
+ $title = rtrim(preg_replace('@[\s\t\r\n]+@', ' ', $title));
3569
+
3570
+ $maxLength = 50;
3571
+ if ( strlen($title) > $maxLength ) {
3572
+ $title = rtrim(substr($title, 0, $maxLength)) . '...';
3573
+ }
3574
+
3575
+ return $title;
3576
  }
3577
 
3578
  public function getRelativeTemplateOrder() {
js/actor-manager.js CHANGED
@@ -46,6 +46,9 @@ var AmeBaseActor = (function () {
46
  }
47
  return specificity;
48
  };
 
 
 
49
  return AmeBaseActor;
50
  }());
51
  var AmeRole = (function (_super) {
@@ -116,6 +119,7 @@ var AmeActorManager = (function () {
116
  this.users = {};
117
  this.grantedCapabilities = {};
118
  this.isMultisite = false;
 
119
  this.isMultisite = !!isMultisite;
120
  AmeActorManager._.forEach(roles, function (roleDetails, id) {
121
  var role = new AmeRole(id, roleDetails.name, roleDetails.capabilities);
@@ -341,10 +345,98 @@ var AmeActorManager = (function () {
341
  return delta;
342
  };
343
  ;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  AmeActorManager._ = wsAmeLodash;
345
  return AmeActorManager;
346
  }());
347
  if (typeof wsAmeActorData !== 'undefined') {
348
  AmeActors = new AmeActorManager(wsAmeActorData.roles, wsAmeActorData.users, wsAmeActorData.isMultisite);
 
 
 
349
  }
350
  //# sourceMappingURL=actor-manager.js.map
46
  }
47
  return specificity;
48
  };
49
+ AmeBaseActor.prototype.toString = function () {
50
+ return this.displayName + ' [' + this.id + ']';
51
+ };
52
  return AmeBaseActor;
53
  }());
54
  var AmeRole = (function (_super) {
119
  this.users = {};
120
  this.grantedCapabilities = {};
121
  this.isMultisite = false;
122
+ this.suggestedCapabilities = [];
123
  this.isMultisite = !!isMultisite;
124
  AmeActorManager._.forEach(roles, function (roleDetails, id) {
125
  var role = new AmeRole(id, roleDetails.name, roleDetails.capabilities);
345
  return delta;
346
  };
347
  ;
348
+ AmeActorManager.prototype.generateCapabilitySuggestions = function (capPower) {
349
+ var _ = AmeActorManager._;
350
+ var capsByPower = _.memoize(function (role) {
351
+ var sortedCaps = _.reduce(role.capabilities, function (result, hasCap, capability) {
352
+ if (hasCap) {
353
+ result.push({
354
+ capability: capability,
355
+ power: _.get(capPower, [capability], 0)
356
+ });
357
+ }
358
+ return result;
359
+ }, []);
360
+ sortedCaps = _.sortBy(sortedCaps, function (item) { return -item.power; });
361
+ return sortedCaps;
362
+ });
363
+ var rolesByPower = _.values(this.getRoles()).sort(function (a, b) {
364
+ var aCaps = capsByPower(a), bCaps = capsByPower(b);
365
+ //Prioritise roles with the highest number of the most powerful capabilities.
366
+ for (var i = 0, limit = Math.min(aCaps.length, bCaps.length); i < limit; i++) {
367
+ var delta_1 = bCaps[i].power - aCaps[i].power;
368
+ if (delta_1 !== 0) {
369
+ return delta_1;
370
+ }
371
+ }
372
+ //Give a tie to the role that has more capabilities.
373
+ var delta = bCaps.length - aCaps.length;
374
+ if (delta !== 0) {
375
+ return delta;
376
+ }
377
+ //Failing that, just sort alphabetically.
378
+ if (a.displayName > b.displayName) {
379
+ return 1;
380
+ }
381
+ else if (a.displayName < b.displayName) {
382
+ return -1;
383
+ }
384
+ return 0;
385
+ });
386
+ var preferredCaps = [
387
+ 'manage_network_options',
388
+ 'install_plugins', 'edit_plugins', 'delete_users',
389
+ 'manage_options', 'switch_themes',
390
+ 'edit_others_pages', 'edit_others_posts', 'edit_pages',
391
+ 'unfiltered_html',
392
+ 'publish_posts', 'edit_posts',
393
+ 'read'
394
+ ];
395
+ var deprecatedCaps = _(_.range(0, 10)).map(function (level) { return 'level_' + level; }).value();
396
+ deprecatedCaps.push('edit_files');
397
+ var findDiscriminant = function (caps, includeRoles, excludeRoles) {
398
+ var getEnabledCaps = function (role) {
399
+ return _.keys(_.pick(role.capabilities, _.identity));
400
+ };
401
+ //Find caps that all of the includeRoles have and excludeRoles don't.
402
+ var includeCaps = _.intersection.apply(_, _.map(includeRoles, getEnabledCaps)), excludeCaps = _.union.apply(_, _.map(excludeRoles, getEnabledCaps)), possibleCaps = _.without.apply(_, [includeCaps].concat(excludeCaps).concat(deprecatedCaps));
403
+ var bestCaps = _.intersection(preferredCaps, possibleCaps);
404
+ if (bestCaps.length > 0) {
405
+ return bestCaps[0];
406
+ }
407
+ else if (possibleCaps.length > 0) {
408
+ return possibleCaps[0];
409
+ }
410
+ return null;
411
+ };
412
+ var suggestedCapabilities = [];
413
+ for (var i = 0; i < rolesByPower.length; i++) {
414
+ var role = rolesByPower[i];
415
+ var cap = findDiscriminant(preferredCaps, _.slice(rolesByPower, 0, i + 1), _.slice(rolesByPower, i + 1, rolesByPower.length));
416
+ suggestedCapabilities.push({ role: role, capability: cap });
417
+ }
418
+ var previousSuggestion = null;
419
+ for (var i = suggestedCapabilities.length - 1; i >= 0; i--) {
420
+ if (suggestedCapabilities[i].capability === null) {
421
+ suggestedCapabilities[i].capability =
422
+ previousSuggestion ? previousSuggestion : 'exist';
423
+ }
424
+ else {
425
+ previousSuggestion = suggestedCapabilities[i].capability;
426
+ }
427
+ }
428
+ this.suggestedCapabilities = suggestedCapabilities;
429
+ };
430
+ AmeActorManager.prototype.getSuggestedCapabilities = function () {
431
+ return this.suggestedCapabilities;
432
+ };
433
  AmeActorManager._ = wsAmeLodash;
434
  return AmeActorManager;
435
  }());
436
  if (typeof wsAmeActorData !== 'undefined') {
437
  AmeActors = new AmeActorManager(wsAmeActorData.roles, wsAmeActorData.users, wsAmeActorData.isMultisite);
438
+ if (typeof wsAmeActorData['capPower'] !== 'undefined') {
439
+ AmeActors.generateCapabilitySuggestions(wsAmeActorData['capPower']);
440
+ }
441
  }
442
  //# sourceMappingURL=actor-manager.js.map
js/actor-manager.ts CHANGED
@@ -58,6 +58,10 @@ abstract class AmeBaseActor {
58
  }
59
  return specificity;
60
  }
 
 
 
 
61
  }
62
 
63
  class AmeRole extends AmeBaseActor {
@@ -162,6 +166,11 @@ interface AmeGrantedCapabilityMap {
162
  }
163
  }
164
 
 
 
 
 
 
165
  class AmeActorManager {
166
  private static _ = wsAmeLodash;
167
 
@@ -172,6 +181,8 @@ class AmeActorManager {
172
  private isMultisite: boolean = false;
173
  private superAdmin: AmeSuperAdmin;
174
 
 
 
175
  constructor(roles, users, isMultisite: boolean = false) {
176
  this.isMultisite = !!isMultisite;
177
 
@@ -450,6 +461,113 @@ class AmeActorManager {
450
  }
451
  return delta;
452
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  }
454
 
455
  if (typeof wsAmeActorData !== 'undefined') {
@@ -458,4 +576,8 @@ if (typeof wsAmeActorData !== 'undefined') {
458
  wsAmeActorData.users,
459
  wsAmeActorData.isMultisite
460
  );
 
 
 
 
461
  }
58
  }
59
  return specificity;
60
  }
61
+
62
+ toString(): string {
63
+ return this.displayName + ' [' + this.id + ']';
64
+ }
65
  }
66
 
67
  class AmeRole extends AmeBaseActor {
166
  }
167
  }
168
 
169
+ interface AmeCapabilitySuggestion {
170
+ role: AmeRole;
171
+ capability: string;
172
+ }
173
+
174
  class AmeActorManager {
175
  private static _ = wsAmeLodash;
176
 
181
  private isMultisite: boolean = false;
182
  private superAdmin: AmeSuperAdmin;
183
 
184
+ private suggestedCapabilities: AmeCapabilitySuggestion[] = [];
185
+
186
  constructor(roles, users, isMultisite: boolean = false) {
187
  this.isMultisite = !!isMultisite;
188
 
461
  }
462
  return delta;
463
  };
464
+
465
+ generateCapabilitySuggestions(capPower): void {
466
+ let _ = AmeActorManager._;
467
+
468
+ let capsByPower = _.memoize((role: AmeRole): {capability: string, power: number}[] => {
469
+ let sortedCaps = _.reduce(role.capabilities, (result, hasCap, capability) => {
470
+ if (hasCap) {
471
+ result.push({
472
+ capability: capability,
473
+ power: _.get(capPower, [capability], 0)
474
+ });
475
+ }
476
+ return result;
477
+ }, []);
478
+
479
+ sortedCaps = _.sortBy(sortedCaps, (item) => -item.power);
480
+ return sortedCaps;
481
+ });
482
+
483
+ let rolesByPower: AmeRole[] = _.values<AmeRole>(this.getRoles()).sort(function(a: AmeRole, b: AmeRole) {
484
+ let aCaps = capsByPower(a),
485
+ bCaps = capsByPower(b);
486
+
487
+ //Prioritise roles with the highest number of the most powerful capabilities.
488
+ for (var i = 0, limit = Math.min(aCaps.length, bCaps.length); i < limit; i++) {
489
+ let delta = bCaps[i].power - aCaps[i].power;
490
+ if (delta !== 0) {
491
+ return delta;
492
+ }
493
+ }
494
+
495
+ //Give a tie to the role that has more capabilities.
496
+ let delta = bCaps.length - aCaps.length;
497
+ if (delta !== 0) {
498
+ return delta;
499
+ }
500
+
501
+ //Failing that, just sort alphabetically.
502
+ if (a.displayName > b.displayName) {
503
+ return 1;
504
+ } else if (a.displayName < b.displayName) {
505
+ return -1;
506
+ }
507
+ return 0;
508
+ });
509
+
510
+ let preferredCaps = [
511
+ 'manage_network_options',
512
+ 'install_plugins', 'edit_plugins', 'delete_users',
513
+ 'manage_options', 'switch_themes',
514
+ 'edit_others_pages', 'edit_others_posts', 'edit_pages',
515
+ 'unfiltered_html',
516
+ 'publish_posts', 'edit_posts',
517
+ 'read'
518
+ ];
519
+
520
+ let deprecatedCaps = _(_.range(0, 10)).map((level) => 'level_' + level).value();
521
+ deprecatedCaps.push('edit_files');
522
+
523
+ let findDiscriminant = (caps: string[], includeRoles: AmeRole[], excludeRoles): string => {
524
+ let getEnabledCaps = (role: AmeRole): string[] => {
525
+ return _.keys(_.pick(role.capabilities, _.identity));
526
+ };
527
+
528
+ //Find caps that all of the includeRoles have and excludeRoles don't.
529
+ let includeCaps = _.intersection.apply(_, _.map(includeRoles, getEnabledCaps)),
530
+ excludeCaps = _.union.apply(_, _.map(excludeRoles, getEnabledCaps)),
531
+ possibleCaps = _.without.apply(_, [includeCaps].concat(excludeCaps).concat(deprecatedCaps));
532
+
533
+ let bestCaps = _.intersection(preferredCaps, possibleCaps);
534
+
535
+ if (bestCaps.length > 0) {
536
+ return bestCaps[0];
537
+ } else if (possibleCaps.length > 0) {
538
+ return possibleCaps[0];
539
+ }
540
+ return null;
541
+ };
542
+
543
+ let suggestedCapabilities = [];
544
+ for (let i = 0; i < rolesByPower.length; i++) {
545
+ let role = rolesByPower[i];
546
+
547
+ let cap = findDiscriminant(
548
+ preferredCaps,
549
+ _.slice(rolesByPower, 0, i + 1),
550
+ _.slice(rolesByPower, i + 1, rolesByPower.length)
551
+ );
552
+ suggestedCapabilities.push({role: role, capability: cap});
553
+ }
554
+
555
+ let previousSuggestion = null;
556
+ for (let i = suggestedCapabilities.length - 1; i >= 0; i--) {
557
+ if (suggestedCapabilities[i].capability === null) {
558
+ suggestedCapabilities[i].capability =
559
+ previousSuggestion ? previousSuggestion : 'exist';
560
+ } else {
561
+ previousSuggestion = suggestedCapabilities[i].capability;
562
+ }
563
+ }
564
+
565
+ this.suggestedCapabilities = suggestedCapabilities;
566
+ }
567
+
568
+ public getSuggestedCapabilities(): AmeCapabilitySuggestion[] {
569
+ return this.suggestedCapabilities;
570
+ }
571
  }
572
 
573
  if (typeof wsAmeActorData !== 'undefined') {
576
  wsAmeActorData.users,
577
  wsAmeActorData.isMultisite
578
  );
579
+
580
+ if (typeof wsAmeActorData['capPower'] !== 'undefined') {
581
+ AmeActors.generateCapabilitySuggestions(wsAmeActorData['capPower']);
582
+ }
583
  }
js/menu-editor.js CHANGED
@@ -43,6 +43,9 @@
43
  * @property {object} wsEditorData.postTypes
44
  * @property {object} wsEditorData.taxonomies
45
  *
 
 
 
46
  * @property {boolean} wsEditorData.isDemoMode
47
  * @property {boolean} wsEditorData.isMasterMode
48
  */
@@ -398,7 +401,7 @@ function buildMenuItem(itemData, isTopLevel) {
398
  itemData.separator ? '' : '<a class="ws_edit_link"> </a><div class="ws_flag_container"> </div>',
399
  '<input type="checkbox" class="ws_actor_access_checkbox">',
400
  '<span class="ws_item_title">',
401
- stripAllTags(menuTitle),
402
  '&nbsp;</span>',
403
 
404
  '</div>',
@@ -459,6 +462,37 @@ function stripAllTags(input) {
459
  return input.replace(commentsAndPhpTags, '').replace(tags, '');
460
  }
461
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  //Editor field spec template.
463
  var baseField = {
464
  caption : '[No caption]',
@@ -484,7 +518,7 @@ var knownMenuFields = {
484
  caption : 'Menu title',
485
  display: function(menuItem, displayValue, input, containerNode) {
486
  //Update the header as well.
487
- containerNode.find('.ws_item_title').html(stripAllTags(displayValue) + '&nbsp;');
488
  return displayValue;
489
  },
490
  write: function(menuItem, value, input, containerNode) {
@@ -736,66 +770,6 @@ var knownMenuFields = {
736
  }
737
  }),
738
 
739
- 'page_title' : $.extend({}, baseField, {
740
- caption: "Window title",
741
- standardCaption : true,
742
- advanced : true
743
- }),
744
-
745
- 'open_in' : $.extend({}, baseField, {
746
- caption: 'Open in',
747
- advanced : true,
748
- type : 'select',
749
- options : [
750
- ['Same window or tab', 'same_window'],
751
- ['New window', 'new_window'],
752
- ['Frame', 'iframe']
753
- ],
754
- defaultValue: 'same_window',
755
- visible: false
756
- }),
757
-
758
- 'iframe_height' : $.extend({}, baseField, {
759
- caption: 'Frame height (pixels)',
760
- advanced : true,
761
- visible: function(menuItem) {
762
- return wsEditorData.wsMenuEditorPro && (getFieldValue(menuItem, 'open_in') === 'iframe');
763
- },
764
-
765
- display: function(menuItem, displayValue, input) {
766
- input.prop('placeholder', 'Auto');
767
- if (displayValue === 0 || displayValue === '0') {
768
- displayValue = '';
769
- }
770
- return displayValue;
771
- },
772
-
773
- write: function(menuItem, value) {
774
- value = parseInt(value, 10);
775
- if (isNaN(value) || (value < 0)) {
776
- value = 0;
777
- }
778
- value = Math.round(value);
779
-
780
- if (value > 10000) {
781
- value = 10000;
782
- }
783
-
784
- if (value === 0) {
785
- menuItem.iframe_height = null;
786
- } else {
787
- menuItem.iframe_height = value;
788
- }
789
-
790
- }
791
- }),
792
-
793
- 'css_class' : $.extend({}, baseField, {
794
- caption: 'CSS classes',
795
- advanced : true,
796
- onlyForTopMenus: true
797
- }),
798
-
799
  'icon_url' : $.extend({}, baseField, {
800
  caption: 'Icon URL',
801
  type : 'icon_selector',
@@ -856,6 +830,60 @@ var knownMenuFields = {
856
  }
857
  }),
858
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
859
  'colors' : $.extend({}, baseField, {
860
  caption: 'Color scheme',
861
  defaultValue: 'Default',
@@ -894,6 +922,12 @@ var knownMenuFields = {
894
  }
895
  }),
896
 
 
 
 
 
 
 
897
  'page_heading' : $.extend({}, baseField, {
898
  caption: 'Page heading',
899
  advanced : true,
@@ -1329,6 +1363,22 @@ function getDefaultValue(entry, fieldName, defaultValue, containerNode) {
1329
  }
1330
  }
1331
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1332
  if (typeof defaultValue === 'undefined') {
1333
  defaultValue = null;
1334
  }
@@ -1400,6 +1450,45 @@ AmeEditorApi.forEachMenuItem = function(callback, skipSeparators) {
1400
  });
1401
  };
1402
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1403
  /***************************************************************************
1404
  Parsing & encoding menu inputs
1405
  ***************************************************************************/
@@ -1891,11 +1980,16 @@ function ameOnDomReady() {
1891
  submenuBox = $('#ws_submenu_box'),
1892
  submenuDropZone = submenuBox.closest('.ws_main_container').find('.ws_dropzone');
1893
 
1894
- //Highlight the clicked menu item and show it's submenu
1895
  var currentVisibleSubmenu = null;
1896
- menuEditorNode.on('click', '.ws_container', (function () {
1897
- var container = $(this);
 
 
 
 
 
1898
  if (container.hasClass('ws_active')) {
 
1899
  return;
1900
  }
1901
 
@@ -1920,6 +2014,12 @@ function ameOnDomReady() {
1920
  container.closest('.ws_main_container')
1921
  .find('.ws_toolbar .ws_delete_menu_button')
1922
  .toggleClass('ws_button_disabled', !canDeleteItem(container));
 
 
 
 
 
 
1923
  }));
1924
 
1925
  function updateSubmenuBoxHeight(selectedMenu) {
@@ -2277,7 +2377,9 @@ function ameOnDomReady() {
2277
 
2278
  var capSelectorDropdown = $('#ws_cap_selector');
2279
  var currentDropdownOwner = null; //The input element that the dropdown is currently associated with.
2280
- var isDropdownBeingHidden = false;
 
 
2281
 
2282
  //Show/hide the capability drop-down list when the trigger button is clicked
2283
  $('#ws_trigger_capability_dropdown').on('mousedown click', onDropdownTriggerClicked);
@@ -2288,9 +2390,13 @@ function ameOnDomReady() {
2288
  var inputBox = null;
2289
  var button = $(this);
2290
 
 
 
 
2291
  //Find the input associated with the button that was clicked.
2292
  if ( button.attr('id') === 'ws_trigger_capability_dropdown' ) {
2293
  inputBox = $('#ws_extra_capability');
 
2294
  } else {
2295
  inputBox = button.closest('.ws_edit_field').find('.ws_field_value').first();
2296
  }
@@ -2333,7 +2439,17 @@ function ameOnDomReady() {
2333
  width(inputBox.outerWidth());
2334
 
2335
  currentDropdownOwner = inputBox;
 
 
 
 
 
 
 
 
2336
  capSelectorDropdown.focus();
 
 
2337
  }
2338
 
2339
  //Also show it when the user presses the down arrow in the input field (doesn't work in Opera).
@@ -2343,26 +2459,34 @@ function ameOnDomReady() {
2343
  }
2344
  });
2345
 
 
 
 
 
 
 
2346
  //Event handlers for the drop-down lists themselves
2347
  var dropdownNodes = $('.ws_dropdown');
2348
 
2349
  // Hide capability drop-down when it loses focus.
2350
  dropdownNodes.blur(function(){
2351
- capSelectorDropdown.hide();
 
 
2352
  });
2353
 
2354
  dropdownNodes.keydown(function(event){
2355
 
2356
  //Hide it when the user presses Esc
2357
  if ( event.which === 27 ){
2358
- capSelectorDropdown.hide();
2359
  if (currentDropdownOwner) {
2360
  currentDropdownOwner.focus();
2361
  }
2362
 
2363
  //Select an item & hide the list when the user presses Enter or Tab
2364
  } else if ( (event.which === 13) || (event.which === 9) ){
2365
- capSelectorDropdown.hide();
2366
 
2367
  if (currentDropdownOwner) {
2368
  if ( capSelectorDropdown.val() ){
@@ -2386,7 +2510,7 @@ function ameOnDomReady() {
2386
  //Update the input & hide the list when an option is clicked
2387
  dropdownNodes.click(function(){
2388
  if (capSelectorDropdown.val()){
2389
- capSelectorDropdown.hide();
2390
  if (currentDropdownOwner) {
2391
  currentDropdownOwner.val(capSelectorDropdown.val()).change().focus();
2392
  }
@@ -2402,9 +2526,176 @@ function ameOnDomReady() {
2402
  var option = event.target;
2403
  if ( (typeof option.selected !== 'undefined') && !option.selected && option.value ){
2404
  option.selected = true;
 
 
 
2405
  }
2406
  });
2407
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2408
  /*************************************************************************
2409
  Icon selector
2410
  *************************************************************************/
@@ -2873,7 +3164,7 @@ function ameOnDomReady() {
2873
 
2874
  //Add menu title to the dialog caption.
2875
  var title = getFieldValue(menuItem, 'menu_title', null);
2876
- setUpColorDialog(title ? ('Colors: ' + title.substring(0, 30)) : 'Colors');
2877
 
2878
  //Show the [global] preset only if the user has set it up.
2879
  var globalPresetExists = colorPresets.hasOwnProperty('[global]');
@@ -3897,6 +4188,19 @@ function ameOnDomReady() {
3897
  $('#ws_data').val(data);
3898
  $('#ws_data_length').val(data.length);
3899
  $('#ws_selected_actor').val(actorSelectorWidget.selectedActor === null ? '' : actorSelectorWidget.selectedActor);
 
 
 
 
 
 
 
 
 
 
 
 
 
3900
  $('#ws_main_form').submit();
3901
  });
3902
 
@@ -4309,6 +4613,23 @@ function ameOnDomReady() {
4309
  //Finally, show the menu
4310
  loadMenuConfiguration(customMenu);
4311
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4312
  //... and make the UI visible now that it's fully rendered.
4313
  menuEditorNode.css('visibility', 'visible');
4314
  }
43
  * @property {object} wsEditorData.postTypes
44
  * @property {object} wsEditorData.taxonomies
45
  *
46
+ * @property {string|null} wsEditorData.selectedMenu
47
+ * @property {string|null} wsEditorData.selectedSubmenu
48
+ *
49
  * @property {boolean} wsEditorData.isDemoMode
50
  * @property {boolean} wsEditorData.isMasterMode
51
  */
401
  itemData.separator ? '' : '<a class="ws_edit_link"> </a><div class="ws_flag_container"> </div>',
402
  '<input type="checkbox" class="ws_actor_access_checkbox">',
403
  '<span class="ws_item_title">',
404
+ formatMenuTitle(menuTitle),
405
  '&nbsp;</span>',
406
 
407
  '</div>',
462
  return input.replace(commentsAndPhpTags, '').replace(tags, '');
463
  }
464
 
465
+ function truncateString(input, maxLength, padding) {
466
+ if (typeof padding === 'undefined') {
467
+ padding = '';
468
+ }
469
+
470
+ if (input.length > maxLength) {
471
+ input = input.substring(0, maxLength - 1) + padding;
472
+ }
473
+
474
+ return input;
475
+ }
476
+
477
+ /**
478
+ * Format menu title for display in HTML.
479
+ * Strips tags and truncates long titles.
480
+ *
481
+ * @param {String} title
482
+ * @returns {String}
483
+ */
484
+ function formatMenuTitle(title) {
485
+ title = stripAllTags(title);
486
+
487
+ //Compact whitespace.
488
+ title = title.replace(/[\s\t\r\n]+/g, ' ');
489
+ title = jsTrim(title);
490
+
491
+ //The max. length was chosen empirically.
492
+ title = truncateString(title, 34, '\u2026');
493
+ return title;
494
+ }
495
+
496
  //Editor field spec template.
497
  var baseField = {
498
  caption : '[No caption]',
518
  caption : 'Menu title',
519
  display: function(menuItem, displayValue, input, containerNode) {
520
  //Update the header as well.
521
+ containerNode.find('.ws_item_title').html(formatMenuTitle(displayValue) + '&nbsp;');
522
  return displayValue;
523
  },
524
  write: function(menuItem, value, input, containerNode) {
770
  }
771
  }),
772
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
773
  'icon_url' : $.extend({}, baseField, {
774
  caption: 'Icon URL',
775
  type : 'icon_selector',
830
  }
831
  }),
832
 
833
+ 'css_class' : $.extend({}, baseField, {
834
+ caption: 'CSS classes',
835
+ advanced : true,
836
+ onlyForTopMenus: true
837
+ }),
838
+
839
+ 'open_in' : $.extend({}, baseField, {
840
+ caption: 'Open in',
841
+ advanced : true,
842
+ type : 'select',
843
+ options : [
844
+ ['Same window or tab', 'same_window'],
845
+ ['New window', 'new_window'],
846
+ ['Frame', 'iframe']
847
+ ],
848
+ defaultValue: 'same_window',
849
+ visible: false
850
+ }),
851
+
852
+ 'iframe_height' : $.extend({}, baseField, {
853
+ caption: 'Frame height (pixels)',
854
+ advanced : true,
855
+ visible: function(menuItem) {
856
+ return wsEditorData.wsMenuEditorPro && (getFieldValue(menuItem, 'open_in') === 'iframe');
857
+ },
858
+
859
+ display: function(menuItem, displayValue, input) {
860
+ input.prop('placeholder', 'Auto');
861
+ if (displayValue === 0 || displayValue === '0') {
862
+ displayValue = '';
863
+ }
864
+ return displayValue;
865
+ },
866
+
867
+ write: function(menuItem, value) {
868
+ value = parseInt(value, 10);
869
+ if (isNaN(value) || (value < 0)) {
870
+ value = 0;
871
+ }
872
+ value = Math.round(value);
873
+
874
+ if (value > 10000) {
875
+ value = 10000;
876
+ }
877
+
878
+ if (value === 0) {
879
+ menuItem.iframe_height = null;
880
+ } else {
881
+ menuItem.iframe_height = value;
882
+ }
883
+
884
+ }
885
+ }),
886
+
887
  'colors' : $.extend({}, baseField, {
888
  caption: 'Color scheme',
889
  defaultValue: 'Default',
922
  }
923
  }),
924
 
925
+ 'page_title' : $.extend({}, baseField, {
926
+ caption: "Window title",
927
+ standardCaption : true,
928
+ advanced : true
929
+ }),
930
+
931
  'page_heading' : $.extend({}, baseField, {
932
  caption: 'Page heading',
933
  advanced : true,
1363
  }
1364
  }
1365
 
1366
+ //Use the custom menu title as the page title if the default page title matches the default menu title.
1367
+ //Note that if the page title is an empty string (''), WP automatically uses the menu title. So we do the same.
1368
+ if ((fieldName === 'page_title') && (entry.template_id !== '')) {
1369
+ var defaultPageTitle = itemTemplates.getDefaultValue(entry.template_id, 'page_title'),
1370
+ defaultMenuTitle = itemTemplates.getDefaultValue(entry.template_id, 'menu_title'),
1371
+ customMenuTitle = entry['menu_title'];
1372
+
1373
+ if (
1374
+ (customMenuTitle !== null)
1375
+ && (customMenuTitle !== '')
1376
+ && ((defaultPageTitle === '') || (defaultMenuTitle === defaultPageTitle))
1377
+ ) {
1378
+ return customMenuTitle;
1379
+ }
1380
+ }
1381
+
1382
  if (typeof defaultValue === 'undefined') {
1383
  defaultValue = null;
1384
  }
1450
  });
1451
  };
1452
 
1453
+ /**
1454
+ * Select the first menu item that has the specified URL.
1455
+ *
1456
+ * @param {string} boxSelector
1457
+ * @param {string} url
1458
+ * @param {boolean|null} [expandProperties]
1459
+ * @returns {JQuery}
1460
+ */
1461
+ AmeEditorApi.selectMenuItemByUrl = function(boxSelector, url, expandProperties) {
1462
+ if (typeof expandProperties === 'undefined') {
1463
+ expandProperties = null;
1464
+ }
1465
+
1466
+ var box = $(boxSelector);
1467
+ if (box.is('#ws_submenu_box')) {
1468
+ box = box.find('.ws_submenu:visible').first();
1469
+ }
1470
+
1471
+ var containerNode =
1472
+ box.find('.ws_container')
1473
+ .filter(function() {
1474
+ var itemUrl = AmeEditorApi.getItemDisplayUrl($(this).data('menu_item'));
1475
+ return (itemUrl === url);
1476
+ })
1477
+ .first();
1478
+
1479
+ if (containerNode.length > 0) {
1480
+ AmeEditorApi.selectItem(containerNode);
1481
+
1482
+ if (expandProperties !== null) {
1483
+ var expandLink = containerNode.find('.ws_edit_link').first();
1484
+ if (expandLink.hasClass('ws_edit_link_expanded') !== expandProperties) {
1485
+ expandLink.click();
1486
+ }
1487
+ }
1488
+ }
1489
+ return containerNode;
1490
+ };
1491
+
1492
  /***************************************************************************
1493
  Parsing & encoding menu inputs
1494
  ***************************************************************************/
1980
  submenuBox = $('#ws_submenu_box'),
1981
  submenuDropZone = submenuBox.closest('.ws_main_container').find('.ws_dropzone');
1982
 
 
1983
  var currentVisibleSubmenu = null;
1984
+
1985
+ /**
1986
+ * Select a menu item and show its submenu.
1987
+ *
1988
+ * @param {JQuery|HTMLElement} container Menu container node.
1989
+ */
1990
+ function selectItem(container) {
1991
  if (container.hasClass('ws_active')) {
1992
+ //The menu item is already selected.
1993
  return;
1994
  }
1995
 
2014
  container.closest('.ws_main_container')
2015
  .find('.ws_toolbar .ws_delete_menu_button')
2016
  .toggleClass('ws_button_disabled', !canDeleteItem(container));
2017
+ }
2018
+ AmeEditorApi.selectItem = selectItem;
2019
+
2020
+ //Select the clicked menu item and show its submenu
2021
+ menuEditorNode.on('click', '.ws_container', (function () {
2022
+ selectItem($(this));
2023
  }));
2024
 
2025
  function updateSubmenuBoxHeight(selectedMenu) {
2377
 
2378
  var capSelectorDropdown = $('#ws_cap_selector');
2379
  var currentDropdownOwner = null; //The input element that the dropdown is currently associated with.
2380
+ var currentDropdownOwnerMenu = null; //The menu item that the above input belongs to.
2381
+
2382
+ var isDropdownBeingHidden = false, isSuggestionClick = false;
2383
 
2384
  //Show/hide the capability drop-down list when the trigger button is clicked
2385
  $('#ws_trigger_capability_dropdown').on('mousedown click', onDropdownTriggerClicked);
2390
  var inputBox = null;
2391
  var button = $(this);
2392
 
2393
+ var isInAccessEditor = false;
2394
+ isSuggestionClick = false;
2395
+
2396
  //Find the input associated with the button that was clicked.
2397
  if ( button.attr('id') === 'ws_trigger_capability_dropdown' ) {
2398
  inputBox = $('#ws_extra_capability');
2399
+ isInAccessEditor = true;
2400
  } else {
2401
  inputBox = button.closest('.ws_edit_field').find('.ws_field_value').first();
2402
  }
2439
  width(inputBox.outerWidth());
2440
 
2441
  currentDropdownOwner = inputBox;
2442
+
2443
+ currentDropdownOwnerMenu = null;
2444
+ if (isInAccessEditor) {
2445
+ currentDropdownOwnerMenu = AmeItemAccessEditor.getCurrentMenuItem();
2446
+ } else {
2447
+ currentDropdownOwnerMenu = currentDropdownOwner.closest('.ws_container').data('menu_item');
2448
+ }
2449
+
2450
  capSelectorDropdown.focus();
2451
+
2452
+ capSuggestionFeature.show();
2453
  }
2454
 
2455
  //Also show it when the user presses the down arrow in the input field (doesn't work in Opera).
2459
  }
2460
  });
2461
 
2462
+ function hideCapSelector() {
2463
+ capSelectorDropdown.hide();
2464
+ capSuggestionFeature.hide();
2465
+ isSuggestionClick = false;
2466
+ }
2467
+
2468
  //Event handlers for the drop-down lists themselves
2469
  var dropdownNodes = $('.ws_dropdown');
2470
 
2471
  // Hide capability drop-down when it loses focus.
2472
  dropdownNodes.blur(function(){
2473
+ if (!isSuggestionClick) {
2474
+ hideCapSelector();
2475
+ }
2476
  });
2477
 
2478
  dropdownNodes.keydown(function(event){
2479
 
2480
  //Hide it when the user presses Esc
2481
  if ( event.which === 27 ){
2482
+ hideCapSelector();
2483
  if (currentDropdownOwner) {
2484
  currentDropdownOwner.focus();
2485
  }
2486
 
2487
  //Select an item & hide the list when the user presses Enter or Tab
2488
  } else if ( (event.which === 13) || (event.which === 9) ){
2489
+ hideCapSelector();
2490
 
2491
  if (currentDropdownOwner) {
2492
  if ( capSelectorDropdown.val() ){
2510
  //Update the input & hide the list when an option is clicked
2511
  dropdownNodes.click(function(){
2512
  if (capSelectorDropdown.val()){
2513
+ hideCapSelector();
2514
  if (currentDropdownOwner) {
2515
  currentDropdownOwner.val(capSelectorDropdown.val()).change().focus();
2516
  }
2526
  var option = event.target;
2527
  if ( (typeof option.selected !== 'undefined') && !option.selected && option.value ){
2528
  option.selected = true;
2529
+
2530
+ //Preview which roles have this capability and the required cap.
2531
+ capSuggestionFeature.previewAccessForItem(currentDropdownOwnerMenu, option.value);
2532
  }
2533
  });
2534
 
2535
+ /************************************************************************
2536
+ * Capability suggestions
2537
+ *************************************************************************/
2538
+
2539
+ var capSuggestionFeature = (function() {
2540
+ //This feature is not used in the Pro version because it has a different permission UI.
2541
+ if (wsEditorData.wsMenuEditorPro) {
2542
+ return {
2543
+ previewAccessForItem: function () {},
2544
+ show: function () {},
2545
+ hide: function () {}
2546
+ }
2547
+ }
2548
+
2549
+ var capabilitySuggestions = $('#ws_capability_suggestions'),
2550
+ suggestionBody = capabilitySuggestions.find('table tbody').first().empty(),
2551
+ suggestedCapabilities = AmeActors.getSuggestedCapabilities();
2552
+
2553
+ for (var i = 0; i < suggestedCapabilities.length; i++) {
2554
+ var role = suggestedCapabilities[i].role, capability = suggestedCapabilities[i].capability;
2555
+ $('<tr>')
2556
+ .data('role', role)
2557
+ .data('capability', capability)
2558
+ .append(
2559
+ $('<th>', {text: role.displayName, scope: 'row'}).addClass('ws_ame_role_name')
2560
+ )
2561
+ .append(
2562
+ $('<td>', {text: capability}).addClass('ws_ame_suggested_capability')
2563
+ )
2564
+ .appendTo(suggestionBody);
2565
+ }
2566
+
2567
+ var currentPreviewedCaps = null;
2568
+
2569
+ /**
2570
+ * Update the access preview.
2571
+ * @param {string|string[]|null} capabilities
2572
+ */
2573
+ function previewAccess(capabilities) {
2574
+ if (typeof capabilities === 'string') {
2575
+ capabilities = [capabilities];
2576
+ }
2577
+
2578
+ if (_.isEqual(capabilities, currentPreviewedCaps)) {
2579
+ return;
2580
+ }
2581
+ currentPreviewedCaps = capabilities;
2582
+ capabilitySuggestions.find('#ws_previewed_caps').text(currentPreviewedCaps.join(' + '));
2583
+
2584
+ //Short-circuit the no-caps case.
2585
+ if (capabilities === null || capabilities.length === 0) {
2586
+ suggestionBody.find('tr').removeClass('ws_preview_has_access');
2587
+ return;
2588
+ }
2589
+
2590
+ suggestionBody.find('tr').each(function() {
2591
+ var $row = $(this),
2592
+ role = $row.data('role');
2593
+
2594
+ var hasCaps = true;
2595
+ for (var i = 0; i < capabilities.length; i++) {
2596
+ hasCaps = hasCaps && AmeActors.hasCap(role.id, capabilities[i]);
2597
+ }
2598
+ $row.toggleClass('ws_preview_has_access', hasCaps);
2599
+ });
2600
+ }
2601
+
2602
+ function previewAccessForItem(menuItem, selectedExtraCap) {
2603
+ var requiredCap = '', extraCap = '';
2604
+
2605
+ if (menuItem) {
2606
+ requiredCap = getFieldValue(menuItem, 'access_level', '');
2607
+ extraCap = getFieldValue(menuItem, 'extra_capability', '');
2608
+ }
2609
+ if (typeof selectedExtraCap !== 'undefined') {
2610
+ extraCap = selectedExtraCap;
2611
+ }
2612
+
2613
+ var caps = [];
2614
+ if (menuItem && (menuItem.template_id !== '') || (extraCap === '')) {
2615
+ caps.push(requiredCap);
2616
+ }
2617
+ if (extraCap !== '') {
2618
+ caps.push(extraCap);
2619
+ }
2620
+
2621
+ previewAccess(caps);
2622
+ }
2623
+
2624
+ suggestionBody.on('mouseenter', 'td.ws_ame_suggested_capability', function() {
2625
+ var row = $(this).closest('tr');
2626
+ previewAccessForItem(currentDropdownOwnerMenu, row.data('capability'));
2627
+ });
2628
+
2629
+ capSelectorDropdown.on('keydown keyup', function() {
2630
+ previewAccessForItem(currentDropdownOwnerMenu, capSelectorDropdown.val());
2631
+ });
2632
+
2633
+ suggestionBody.on('mousedown', 'td.ws_ame_suggested_capability', function() {
2634
+ //Don't immediately hide the list when the user tries to click a suggestion.
2635
+ //It would prevent the click from registering.
2636
+ isSuggestionClick = true;
2637
+ });
2638
+
2639
+ suggestionBody.on('click', 'td.ws_ame_suggested_capability', function() {
2640
+ var capability = $(this).closest('tr').data('capability');
2641
+
2642
+ //Change the input to the selected capability.
2643
+ if (currentDropdownOwner) {
2644
+ currentDropdownOwner.val(capability).change();
2645
+ }
2646
+
2647
+ hideCapSelector();
2648
+ });
2649
+
2650
+ //Workaround for pressing LMB on a suggestion, then moving the mouse outside the suggestion box and releasing the button.
2651
+ $(document).on('click', function(event) {
2652
+ if (
2653
+ isSuggestionClick
2654
+ && capabilitySuggestions.is(':visible')
2655
+ && ( $(event.target).closest(capabilitySuggestions).length < 1 )
2656
+ ) {
2657
+ hideCapSelector();
2658
+ }
2659
+ });
2660
+
2661
+ return {
2662
+ previewAccessForItem: previewAccessForItem,
2663
+ show: function() {
2664
+ //Position the capability suggestion table next to the selector and match heights.
2665
+ capabilitySuggestions
2666
+ .css({
2667
+ position: 'absolute',
2668
+ zIndex: 1009
2669
+ })
2670
+ .show()
2671
+ .position({
2672
+ my: 'left top',
2673
+ at: 'right top',
2674
+ of: capSelectorDropdown,
2675
+ collision: 'none'
2676
+ });
2677
+
2678
+ var selectorHeight = capSelectorDropdown.height(),
2679
+ suggestionsHeight = capabilitySuggestions.height(),
2680
+ desiredHeight = Math.max(selectorHeight, suggestionsHeight);
2681
+ if (selectorHeight < desiredHeight) {
2682
+ capSelectorDropdown.height(desiredHeight);
2683
+ }
2684
+ if (suggestionsHeight < desiredHeight) {
2685
+ capabilitySuggestions.height(desiredHeight);
2686
+ }
2687
+
2688
+ if (currentDropdownOwnerMenu) {
2689
+ previewAccessForItem(currentDropdownOwnerMenu);
2690
+ }
2691
+ },
2692
+ hide: function() {
2693
+ capabilitySuggestions.hide();
2694
+ }
2695
+ };
2696
+ })();
2697
+
2698
+
2699
  /*************************************************************************
2700
  Icon selector
2701
  *************************************************************************/
3164
 
3165
  //Add menu title to the dialog caption.
3166
  var title = getFieldValue(menuItem, 'menu_title', null);
3167
+ setUpColorDialog(title ? ('Colors: ' + formatMenuTitle(title)) : 'Colors');
3168
 
3169
  //Show the [global] preset only if the user has set it up.
3170
  var globalPresetExists = colorPresets.hasOwnProperty('[global]');
4188
  $('#ws_data').val(data);
4189
  $('#ws_data_length').val(data.length);
4190
  $('#ws_selected_actor').val(actorSelectorWidget.selectedActor === null ? '' : actorSelectorWidget.selectedActor);
4191
+
4192
+ var selectedMenu = getSelectedMenu();
4193
+ if (selectedMenu.length > 0) {
4194
+ $('#ws_selected_menu_url').val(AmeEditorApi.getItemDisplayUrl(selectedMenu.data('menu_item')));
4195
+ $('#ws_expand_selected_menu').val(selectedMenu.find('.ws_editbox').is(':visible') ? '1' : '');
4196
+
4197
+ var selectedSubmenu = getSelectedSubmenuItem();
4198
+ if (selectedSubmenu.length > 0) {
4199
+ $('#ws_selected_submenu_url').val(AmeEditorApi.getItemDisplayUrl(selectedSubmenu.data('menu_item')));
4200
+ $('#ws_expand_selected_submenu').val(selectedSubmenu.find('.ws_editbox').is(':visible') ? '1' : '');
4201
+ }
4202
+ }
4203
+
4204
  $('#ws_main_form').submit();
4205
  });
4206
 
4613
  //Finally, show the menu
4614
  loadMenuConfiguration(customMenu);
4615
 
4616
+ //Select the previous selected menu, if any.
4617
+ if (wsEditorData.selectedMenu) {
4618
+ AmeEditorApi.selectMenuItemByUrl(
4619
+ '#ws_menu_box',
4620
+ wsEditorData.selectedMenu,
4621
+ _.get(wsEditorData, 'expandSelectedMenu') === '1'
4622
+ );
4623
+
4624
+ if (wsEditorData.selectedSubmenu) {
4625
+ AmeEditorApi.selectMenuItemByUrl(
4626
+ '#ws_submenu_box',
4627
+ wsEditorData.selectedSubmenu,
4628
+ _.get(wsEditorData, 'expandSelectedSubmenu') === '1'
4629
+ );
4630
+ }
4631
+ }
4632
+
4633
  //... and make the UI visible now that it's fully rendered.
4634
  menuEditorNode.css('visibility', 'visible');
4635
  }
menu-editor.php CHANGED
@@ -3,7 +3,7 @@
3
  Plugin Name: Admin Menu Editor
4
  Plugin URI: http://w-shadow.com/blog/2008/12/20/admin-menu-editor-for-wordpress/
5
  Description: Lets you directly edit the WordPress admin menu. You can re-order, hide or rename existing menus, add custom menus and more.
6
- Version: 1.7.1
7
  Author: Janis Elsts
8
  Author URI: http://w-shadow.com/blog/
9
  */
3
  Plugin Name: Admin Menu Editor
4
  Plugin URI: http://w-shadow.com/blog/2008/12/20/admin-menu-editor-for-wordpress/
5
  Description: Lets you directly edit the WordPress admin menu. You can re-order, hide or rename existing menus, add custom menus and more.
6
+ Version: 1.7.2
7
  Author: Janis Elsts
8
  Author URI: http://w-shadow.com/blog/
9
  */
modules/access-editor/access-editor.js CHANGED
@@ -488,6 +488,10 @@ window.AmeItemAccessEditor = (function ($) {
488
  $('#ws_role_access_container').toggleClass('ws_has_extended_permissions', hasExtendedPermissions);
489
  },
490
 
 
 
 
 
491
  detectExtPermissions: detectExtPermissions
492
  };
493
  })(jQuery);
488
  $('#ws_role_access_container').toggleClass('ws_has_extended_permissions', hasExtendedPermissions);
489
  },
490
 
491
+ getCurrentMenuItem: function() {
492
+ return menuItem;
493
+ },
494
+
495
  detectExtPermissions: detectExtPermissions
496
  };
497
  })(jQuery);
readme.txt CHANGED
@@ -4,7 +4,7 @@ Donate link: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_i
4
  Tags: admin, dashboard, menu, security, wpmu
5
  Requires at least: 4.1
6
  Tested up to: 4.6
7
- Stable tag: 1.7.1
8
 
9
  Lets you edit the WordPress admin menu. You can re-order, hide or rename menus, add custom menus and more.
10
 
@@ -63,6 +63,13 @@ Plugins installed in the `mu-plugins` directory are treated as "always on", so y
63
 
64
  == Changelog ==
65
 
 
 
 
 
 
 
 
66
  = 1.7.1 =
67
  * Split the "required capability" field into two parts - a read-only field that shows the actual required capability, and an editable "extra capability" that you can use to restrict access to the menu.
68
  * Added more detailed permission error messages. You can turn them off in the "Settings" tab by changing "Error verbosity level" to "Low".
4
  Tags: admin, dashboard, menu, security, wpmu
5
  Requires at least: 4.1
6
  Tested up to: 4.6
7
+ Stable tag: 1.7.2
8
 
9
  Lets you edit the WordPress admin menu. You can re-order, hide or rename menus, add custom menus and more.
10
 
63
 
64
  == Changelog ==
65
 
66
+ = 1.7.2 =
67
+ * Added capability suggestions and access preview to the "Extra capability" dropdown.
68
+ * The plugin now remembers the last selected menu item and re-selects it after you save changes.
69
+ * Fixed a layout issue where menus with very long titles would appear incorrectly in the menu editor.
70
+ * When you change the menu title, the window title will also be changed to match it. You can still edit the window title separately if necessary.
71
+ * Moved the "Icon URL" field up and moved "Window title" down.
72
+
73
  = 1.7.1 =
74
  * Split the "required capability" field into two parts - a read-only field that shows the actual required capability, and an editable "extra capability" that you can use to restrict access to the menu.
75
  * Added more detailed permission error messages. You can turn them off in the "Settings" tab by changing "Error verbosity level" to "Low".