Admin Menu Editor - Version 1.10

Version Description

  • Added a "Redirects" feature. You can create login redirects, logout redirects, and registration redirects. You can configure redirects for specific roles and users. You can also set up a default redirect that will apply to everyone who doesn't have a specific setting. Redirect URLs can contain shortcodes, but not all shortcodes will work in this context.
  • Added a few utility shortcodes: [ame-wp-admin], [ame-home-url], [ame-user-info field="..."]. These are mainly intended to be used to create dynamic redirects, but they will also work in posts and pages.
  • Slightly improved the appearance of settings page tabs on small screens and in narrow browser windows.
  • Fixed a minor conflict where several hidden menu items created by "WP Grid Builder" would unexpectedly show up when AME is active.
  • Fixed a conflict with "LoftLoader Pro", "WS Form", and probably a few other plugins that create new admin menu items that link to the theme customizer. Previously, it was impossible to hide or edit those menu items.
  • Fixed a few jQuery deprecation warnings.
  • Fixed an "Undefined array key" warning that could appear if another plugin created a user role that did not have a "capabilities" key.
  • Fixed a minor BuddyBoss Platform compatibility issue where the menu editor would show a "BuddyBoss -> BuddyBoss" menu item that was not present in the actual admin menu. The item is created by BuddyBoss Platform, but it is apparently intended to be hidden.
  • Refactored the menu editor and added limited support for editing three level menus. While the free version doesn't have the ability to actually render nested items in the admin menu, it should at least load a menu configuration that includes more than two levels without crashing. This will probably only matter if someone edits the settings in the database or copies a menu configuration from the Pro version.
Download this release

Release Info

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

Code changes from version 1.9.10 to 1.10

css/_main-tabs.scss ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /***************************************
2
+ Tabs on the settings page
3
+ ***************************************/
4
+
5
+ .wrap.ws-ame-too-many-tabs .ws-ame-nav-tab-list {
6
+ &.nav-tab-wrapper {
7
+ border-bottom-color: transparent;
8
+ }
9
+
10
+ .nav-tab {
11
+ border-bottom: 1px solid #c3c4c7;
12
+ margin-bottom: 10px;
13
+ margin-top: 0;
14
+ }
15
+ }
16
+
17
+ /* Spacing between the page heading and the tab list.
18
+
19
+ Normally, this is handled by .nav-tab styles, but WordPress changes the margins at smaller screen sizes
20
+ and the tabs end up without a left margin. Let's put that margin on the heading instead and remove it
21
+ from the first tab. */
22
+
23
+ #ws_ame_editor_heading {
24
+ margin-right: 0.305em;
25
+ }
26
+
27
+ .ws-ame-nav-tab-list {
28
+ a.nav-tab:first-of-type {
29
+ margin-left: 0;
30
+ }
31
+ }
32
+
33
+ /* When in "too many tabs" mode, there's too much space between the bottom of the tab list and the rest
34
+ of the page. I haven't found a good way to change the margins of just the last row, so here's a partial fix. */
35
+ .ws-ame-too-many-tabs #ws_actor_selector {
36
+ margin-top: 0;
37
+ }
css/admin.css CHANGED
@@ -118,25 +118,3 @@ hr.ws-submenu-separator {
118
  opacity: 1;
119
  filter: alpha(opacity=100);
120
  }
121
-
122
- /*
123
- * Third level menus.
124
- */
125
- #adminmenu .ame-deep-submenu {
126
-
127
- }
128
-
129
- #adminmenu li.menu-top.opensub .ame-deep-submenu {
130
- top: -1000em;
131
- }
132
-
133
- #adminmenu .wp-submenu li.opensub > ul.ame-deep-submenu {
134
- top: -1px;
135
- }
136
-
137
- .folded #adminmenu li.opensub > ul.ame-deep-submenu,
138
- .folded #adminmenu .wp-has-current-submenu.opensub > ul.ame-deep-submenu,
139
- .no-js.folded #adminmenu .ame-has-deep-submenu:hover > ul.ame-deep-submenu {
140
- top: 0;
141
- left: 160px;
142
- }
118
  opacity: 1;
119
  filter: alpha(opacity=100);
120
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
css/menu-editor.css CHANGED
@@ -1,1694 +1,2051 @@
1
- /* Admin Menu Editor CSS file */
2
- #ws_menu_editor {
3
- min-width: 780px; }
4
-
5
- .ame-is-free-version #ws_menu_editor {
6
- margin-top: 9px; }
7
-
8
- .ws_main_container {
9
- margin: 2px;
10
- width: 316px;
11
- float: left;
12
- display: block;
13
- border: 1px solid #ccd0d4;
14
- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
15
- background-color: #FFFFFF;
16
- border-radius: 0px;
17
- -moz-border-radius: 0px;
18
- -webkit-border-radius: 0px; }
19
-
20
- .ws_box {
21
- min-height: 30px;
22
- width: 100%;
23
- margin: 0; }
24
-
25
- .ws_basic_container {
26
- float: left;
27
- display: block; }
28
-
29
- .ws_dropzone {
30
- display: block;
31
- box-sizing: border-box;
32
- margin: 2px 6px;
33
- border: 3px none #b4b9be;
34
- height: 31px; }
35
-
36
- .ws_dropzone_active,
37
- .ws_dropzone_hover,
38
- .ws_top_to_submenu_drop_hover .ws_dropzone {
39
- border-style: dashed; }
40
-
41
- .ws_dropzone_hover,
42
- .ws_top_to_submenu_drop_hover .ws_dropzone {
43
- border-width: 1px; }
44
-
45
- /*************************************************
46
- Actor UI
47
- *************************************************/
48
- #ws_actor_selector li:after {
49
- content: '| '; }
50
-
51
- #ws_actor_selector li:last-child:after {
52
- content: ''; }
53
-
54
- #ws_actor_selector li a {
55
- display: inline-block;
56
- text-align: center; }
57
- #ws_actor_selector li a::before {
58
- display: block;
59
- content: attr(data-text);
60
- font-weight: bold;
61
- height: 1px;
62
- overflow: hidden;
63
- visibility: hidden;
64
- margin-bottom: -1px; }
65
-
66
- #ws_actor_selector {
67
- margin-top: 6px; }
68
-
69
- /**
70
- * The checkbox that lets the user show/hide a menu for the currently selected actor.
71
- */
72
- #ws_menu_editor .ws_actor_access_checkbox,
73
- #ws_menu_editor input[type="checkbox"].ws_actor_access_checkbox {
74
- margin-right: 2px;
75
- margin-left: 2px;
76
- margin-top: 1px;
77
- vertical-align: text-top; }
78
- #ws_menu_editor .ws_actor_access_checkbox:indeterminate:before,
79
- #ws_menu_editor input[type="checkbox"].ws_actor_access_checkbox:indeterminate:before {
80
- content: '\25a0';
81
- color: #1e8cbe;
82
- margin: -3px 0 0 -1px;
83
- font: 400 14px/1 dashicons;
84
- float: left;
85
- display: inline-block;
86
- vertical-align: middle;
87
- width: 16px;
88
- -webkit-font-smoothing: antialiased; }
89
-
90
- @media screen and (max-width: 782px) {
91
- #ws_menu_editor input[type="checkbox"].ws_actor_access_checkbox:indeterminate:before {
92
- margin: -6px 0 0 1px;
93
- font: 400 26px/1 dashicons; } }
94
- /* The checkbox is only visible when viewing the menu configuration for a specific actor. */
95
- #ws_menu_editor .ws_actor_access_checkbox {
96
- display: none; }
97
-
98
- #ws_menu_editor.ws_is_actor_view .ws_actor_access_checkbox {
99
- display: inline-block; }
100
-
101
- /* Gray-out items inaccessible to the currently selected actor */
102
- .ws_is_actor_view .ws_container.ws_is_hidden_for_actor {
103
- background-color: #F9F9F9; }
104
-
105
- .ws_is_actor_view .ws_is_hidden_for_actor .ws_item_title {
106
- color: #777; }
107
-
108
- /*
109
- * The sidebar
110
- */
111
- #ws_editor_sidebar {
112
- width: auto;
113
- padding: 2px; }
114
-
115
- #ws_menu_editor .ws_main_button {
116
- clear: both;
117
- display: block;
118
- margin: 4px;
119
- width: 130px; }
120
-
121
- #ws_menu_editor #ws_save_menu {
122
- margin-bottom: 20px; }
123
-
124
- #ws_menu_editor #ws_toggle_editor_layout {
125
- display: none; }
126
-
127
- #ws_menu_editor .ws_sidebar_button_separator {
128
- display: block;
129
- height: 4px;
130
- margin: 0;
131
- padding: 0; }
132
-
133
- /*
134
- * Page heading and tabs
135
- */
136
- #ws_ame_editor_heading {
137
- float: left; }
138
-
139
- /*
140
- * Menu components and widgets
141
- */
142
- .ws_container {
143
- display: block;
144
- width: 296px;
145
- padding: 3px;
146
- margin: 2px 0 2px 6px; }
147
- body.rtl .ws_container {
148
- margin-right: 6px;
149
- margin-left: 0; }
150
-
151
- .ws_submenu {
152
- min-height: 2em; }
153
-
154
- .ws_item_head {
155
- padding: 0; }
156
-
157
- .ws_item_title {
158
- display: inline-block;
159
- padding: 2px;
160
- cursor: default;
161
- font-size: 13px;
162
- line-height: 18px; }
163
-
164
- .ws_edit_link {
165
- float: right;
166
- margin-right: 0;
167
- cursor: pointer;
168
- display: block;
169
- width: 40px;
170
- height: 22px;
171
- border-radius: 3px;
172
- -moz-border-radius: 3px;
173
- -webkit-border-radius: 3px;
174
- text-decoration: none; }
175
-
176
- .ws_menu_drop_hover {
177
- background-color: #43b529 !important; }
178
-
179
- .ws_container.ui-sortable-helper * {
180
- cursor: move !important; }
181
-
182
- .ws_container.ws_sortable_placeholder {
183
- outline: 1px dashed #b4b9be;
184
- outline-offset: -1px;
185
- background: none;
186
- border-color: transparent; }
187
-
188
- /*
189
- If you ever want to apply a right-arrow style to the currently selected menu item,
190
- you can do it like this. Commented out for now since it doesn't look all that great,
191
- but might be useful in the future.
192
- */
193
- /*
194
- .ws_container {
195
- position: relative;
196
- }
197
-
198
- .ws_menu.ws_active::after {
199
- content: "";
200
- display: block;
201
- z-index: 1002;
202
-
203
- border-left: 14px solid #8EB0F1;
204
- border-top: 15px solid rgba(255, 255, 255, 0.1);
205
- border-bottom: 15px solid rgba(255, 255, 255, 0.1);
206
- background: transparent;
207
-
208
- position: absolute;
209
- right: -14px;
210
- top: -1px;
211
-
212
- width: 0;
213
- height: 0;
214
- }
215
- */
216
- /*
217
- * A left-arrow style alternative. This one is image-based and doesn't suffer from the finicky sizing issues
218
- * of CSS triangles.
219
- */
220
- .ws_container {
221
- position: relative; }
222
-
223
- .ws_menu.ws_active::after {
224
- content: "";
225
- display: block;
226
- position: absolute;
227
- right: -19px;
228
- top: -1px;
229
- width: 19px;
230
- height: 30px;
231
- background: transparent url("../images/submenu-tip.png") no-repeat center; }
232
-
233
- .ws_container.ws_menu_separator.ws_active::after,
234
- .ws_container.ui-sortable-helper::after {
235
- background-image: none; }
236
-
237
- /****************************************
238
- Per-menu settings fields & panels
239
- *****************************************/
240
- .ws_editbox {
241
- display: block;
242
- padding: 4px;
243
- border-radius: 2px;
244
- border-top-right-radius: 0;
245
- -moz-border-radius: 2px;
246
- -moz-border-radius-topright: 0;
247
- -webkit-border-radius: 2px;
248
- -webkit-border-top-right-radius: 0; }
249
-
250
- .ws_edit_panel {
251
- margin: 0;
252
- padding: 0;
253
- border: none; }
254
-
255
- .ws_edit_field {
256
- margin-bottom: 6px;
257
- min-height: 45px; }
258
- .ws_edit_field:after {
259
- visibility: hidden;
260
- display: block;
261
- height: 0;
262
- font-size: 0;
263
- content: " ";
264
- clear: both; }
265
-
266
- .ws_edit_field-custom {
267
- margin-top: 10px; }
268
-
269
- .ws_edit_field.ws_no_field_caption {
270
- margin-top: 10px;
271
- padding-left: 1px;
272
- height: 25px;
273
- min-height: 25px; }
274
-
275
- /*
276
- * Group headings
277
- */
278
- .ws_edit_field.ws_field_group_heading {
279
- height: 1px;
280
- min-height: 0;
281
- padding-top: 0;
282
- background: #ccc;
283
- margin: 8px -4px 5px; }
284
- .ws_edit_field.ws_field_group_heading span {
285
- display: none;
286
- font-weight: bold; }
287
-
288
- /* The reset-to-default button */
289
- .ws_reset_button {
290
- display: block;
291
- float: right;
292
- margin-left: 4px;
293
- margin-top: 2px;
294
- margin-right: 6px;
295
- cursor: pointer;
296
- width: 16px;
297
- height: 16px;
298
- vertical-align: top;
299
- background: url("../images/pencil_delete_gray.png") no-repeat center; }
300
- .ame-is-wp53-plus .ws_reset_button {
301
- margin-top: 5px; }
302
-
303
- .ws_reset_button:hover {
304
- background-image: url("../images/pencil_delete.png"); }
305
-
306
- .ws_input_default input,
307
- .ws_input_default select,
308
- .ws_input_default .ws_color_scheme_display {
309
- color: gray; }
310
-
311
- /* No reset button for fields set to the default value and fields without a default value */
312
- .ws_input_default .ws_reset_button,
313
- .ws_has_no_default .ws_reset_button {
314
- visibility: hidden; }
315
-
316
- /* The input box in each field editor */
317
- #ws_menu_editor .ws_editbox input[type="text"],
318
- #ws_menu_editor .ws_editbox select {
319
- display: block;
320
- float: left;
321
- width: 254px;
322
- height: 25px;
323
- font-size: 12px;
324
- line-height: 17px;
325
- padding-top: 3px;
326
- padding-bottom: 3px; }
327
- .ame-is-wp53-plus #ws_menu_editor .ws_editbox input[type="text"], .ame-is-wp53-plus
328
- #ws_menu_editor .ws_editbox select {
329
- height: 28px; }
330
-
331
- #ws_menu_editor .ws_edit_field label {
332
- display: block;
333
- float: left; }
334
-
335
- #ws_menu_editor .ws_edit_field-custom input[type="checkbox"] {
336
- margin-top: 0; }
337
-
338
- #ws_menu_editor input[type="text"].ws_field_value {
339
- min-height: 25px; }
340
- .ame-is-wp53-plus #ws_menu_editor input[type="text"].ws_field_value {
341
- min-height: 28px; }
342
-
343
- /* Dropdown button for combo-box fields */
344
- #ws_menu_editor .ws_dropdown_button,
345
- #ws_menu_access_editor .ws_dropdown_button {
346
- box-sizing: border-box;
347
- width: 25px;
348
- height: 25px;
349
- min-height: 25px;
350
- margin: 1px 1px 1px 0;
351
- padding: 0;
352
- text-align: center;
353
- font-size: 9px !important;
354
- line-height: 25px;
355
- border-color: #dfdfdf;
356
- box-shadow: none;
357
- border-top-right-radius: 3px;
358
- border-bottom-right-radius: 3px;
359
- border-top-left-radius: 0;
360
- border-bottom-left-radius: 0;
361
- -moz-border-radius-topright: 3px;
362
- -moz-border-radius-bottomright: 3px;
363
- -moz-border-radius-topleft: 0;
364
- -moz-border-radius-bottomleft: 0;
365
- -webkit-border-top-right-radius: 3px;
366
- -webkit-border-bottom-right-radius: 3px;
367
- -webkit-border-top-left-radius: 0;
368
- -webkit-border-bottom-left-radius: 0; }
369
-
370
- .ame-is-wp53-plus #ws_menu_editor .ws_dropdown_button,
371
- #ws_menu_access_editor.ame-is-wp53-plus .ws_dropdown_button {
372
- height: 28px;
373
- border-color: #7e8993;
374
- background-color: white;
375
- border-left-style: none;
376
- font-size: 10px !important;
377
- line-height: 24px;
378
- color: #555; }
379
- .ame-is-wp53-plus #ws_menu_editor .ws_dropdown_button:hover,
380
- #ws_menu_access_editor.ame-is-wp53-plus .ws_dropdown_button:hover {
381
- color: #23282d; }
382
-
383
- #ws_menu_access_editor .ws_dropdown_button {
384
- display: inline-block;
385
- height: 27px; }
386
-
387
- #ws_menu_access_editor.ame-is-wp53-plus .ws_dropdown_button {
388
- height: 30px; }
389
-
390
- #ws_menu_editor .ws_dropdown_button {
391
- display: block;
392
- float: left; }
393
-
394
- /*
395
- The appearance and size of combo-box fields need to be changed
396
- to accommodate the drop-down button.
397
- */
398
- #ws_menu_editor .ws_has_dropdown input.ws_field_value,
399
- #ws_menu_access_editor input.ws_has_dropdown {
400
- margin-right: 0;
401
- border-right: 0;
402
- border-top-right-radius: 0;
403
- border-bottom-right-radius: 0;
404
- -moz-border-radius-topright: 0;
405
- -moz-border-radius-bottomright: 0;
406
- -webkit-border-top-right-radius: 0;
407
- -webkit-border-bottom-right-radius: 0; }
408
-
409
- #ws_menu_access_editor input.ws_has_dropdown {
410
- width: 90%;
411
- box-sizing: border-box;
412
- height: 27px; }
413
-
414
- #ws_menu_access_editor.ame-is-wp53-plus input.ws_has_dropdown {
415
- height: 30px; }
416
-
417
- #ws_menu_editor .ws_has_dropdown input.ws_field_value {
418
- width: 229px; }
419
-
420
- /* Unlike others, this field is just a single checkbox, so it has a smaller height */
421
- #ws_menu_editor .ws_edit_field-custom {
422
- height: 16px; }
423
-
424
- /*
425
- * "Show/hide advanced fields"
426
- */
427
- .ws_toggle_container {
428
- text-align: right;
429
- margin-right: 27px; }
430
-
431
- .ws_toggle_advanced_fields {
432
- color: #6087CB;
433
- text-decoration: none;
434
- font-size: 0.85em; }
435
-
436
- .ws_toggle_advanced_fields:visited, .ws_toggle_advanced_fields:active {
437
- color: #6087CB; }
438
-
439
- .ws_toggle_advanced_fields:hover {
440
- color: #d54e21;
441
- text-decoration: underline; }
442
-
443
- /************************************
444
- Menu flags
445
- *************************************/
446
- .ws_flag_container {
447
- float: right;
448
- margin-right: 4px;
449
- padding-top: 2px; }
450
-
451
- .ws_flag {
452
- display: block;
453
- float: right;
454
- width: 16px;
455
- height: 16px;
456
- margin-left: 4px;
457
- background-repeat: no-repeat; }
458
-
459
- /* user-created items */
460
- .ws_custom_flag {
461
- background-image: url("../images/page-add.png"); }
462
-
463
- /* unused items - those that are in the default menu but not in the custom one */
464
- .ws_unused_flag {
465
- background-image: url("../images/new-menu-badge.png");
466
- width: 31px; }
467
-
468
- /* hidden items */
469
- .ws_hidden_flag {
470
- background-image: url("../images/page-invisible.png"); }
471
-
472
- /* items with custom permissions for the selected actor */
473
- .ws_custom_actor_permissions_flag {
474
- font: 16px/1 'dashicons'; }
475
-
476
- .ws_custom_actor_permissions_flag::before {
477
- /*content: "\f160";*/
478
- /* padlock */
479
- content: "\f110";
480
- /* human silhouette */
481
- color: black;
482
- filter: alpha(opacity=25);
483
- /*IE 5-7*/
484
- opacity: 0.25; }
485
-
486
- /* Hidden from everyone except the current user and Super Admin. */
487
- .ws_hidden_from_others_flag {
488
- background-image: url("../images/font-awesome/eye-slash.png"); }
489
-
490
- /* Item visibility can't be determined because it depends on a meta capability. */
491
- .ws_uncertain_meta_cap_flag::before {
492
- font: 16px/1 'dashicons';
493
- content: "\f348";
494
- color: black;
495
- filter: alpha(opacity=25);
496
- /*IE 5-7*/
497
- opacity: 0.25; }
498
-
499
- /* These classes could be used to apply different styles to items depending on their flags */
500
- /************************************
501
- Toolbars
502
- *************************************/
503
- .ws_toolbar {
504
- display: block;
505
- -webkit-box-sizing: border-box;
506
- -moz-box-sizing: border-box;
507
- box-sizing: border-box;
508
- width: 100%;
509
- padding: 6px 6px 0 6px; }
510
-
511
- .ws_button {
512
- display: block;
513
- margin-right: 3px;
514
- margin-bottom: 4px;
515
- padding: 4px;
516
- float: left;
517
- -webkit-box-sizing: content-box;
518
- -moz-box-sizing: content-box;
519
- box-sizing: content-box;
520
- width: 16px;
521
- height: 16px;
522
- border-radius: 3px;
523
- -moz-border-radius: 3px;
524
- -webkit-border-radius: 3px; }
525
- .ws_button img {
526
- vertical-align: top; }
527
-
528
- a.ws_button:hover {
529
- background-color: #d0e0ff;
530
- border-color: #9090c0; }
531
-
532
- .ws_button.ws_button_disabled {
533
- border-color: #ccc; }
534
-
535
- a.ws_button.ws_button_disabled:hover {
536
- background-color: white;
537
- border: 1px solid #ccc; }
538
-
539
- .ws_button_disabled img {
540
- filter: grayscale(1);
541
- -webkit-filter: grayscale(1);
542
- opacity: 0.65; }
543
-
544
- .ws_separator {
545
- float: left;
546
- width: 5px; }
547
-
548
- #ws_toggle_toolbar {
549
- margin-right: 0; }
550
-
551
- /************************************
552
- Capability selector
553
- *************************************/
554
- select.ws_dropdown {
555
- width: 252px;
556
- height: 20em;
557
- z-index: 1002;
558
- position: absolute;
559
- display: none;
560
- font-family: "Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif;
561
- font-size: 12px; }
562
-
563
- select.ws_dropdown option {
564
- font-family: "Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif;
565
- font-size: 12px;
566
- padding: 3px; }
567
-
568
- select.ws_dropdown optgroup option {
569
- padding-left: 10px; }
570
-
571
- /************************************
572
- Tabs (small)
573
- ************************************
574
- Tabbed navigation for dropdowns and small dialogs.
575
- */
576
- .ws_tool_tab_nav {
577
- list-style: outside none none;
578
- padding: 0;
579
- margin: 0 0 0 6px; }
580
- .ws_tool_tab_nav li {
581
- display: inline-block;
582
- border: 1px solid transparent;
583
- border-bottom-width: 0;
584
- padding: 3px 5px 5px;
585
- line-height: 1.35em;
586
- margin-bottom: 0; }
587
- .ws_tool_tab_nav li.ui-tabs-active {
588
- border-color: #dfdfdf;
589
- border-bottom-color: #FDFDFD;
590
- background: #FDFDFD none; }
591
- .ws_tool_tab_nav a {
592
- text-decoration: none; }
593
- .ws_tool_tab_nav li.ui-tabs-active a {
594
- color: #32373C; }
595
-
596
- .ws_tool_tab {
597
- border-top: 1px solid #DFDFDF;
598
- margin-top: -1px;
599
- background-color: #FDFDFD; }
600
-
601
- /************************************
602
- Icon selector
603
- *************************************/
604
- #ws_icon_selector {
605
- border: 1px solid silver;
606
- border-radius: 3px;
607
- background-color: white;
608
- width: 216px;
609
- padding: 4px 0 0 0;
610
- position: absolute; }
611
-
612
- #ws_icon_selector.ws_with_more_icons {
613
- width: 570px; }
614
-
615
- #ws_icon_selector .ws_icon_extra {
616
- display: none; }
617
-
618
- #ws_icon_selector.ws_with_more_icons .ws_icon_extra {
619
- display: inline-block; }
620
-
621
- #ws_icon_selector .ws_icon_option {
622
- float: left;
623
- height: 30px;
624
- margin: 2px;
625
- cursor: pointer;
626
- border: 1px solid #bbb;
627
- border-radius: 3px;
628
- /* Gradients and colours cribbed from WP 3.5.1 button styles */
629
- background: #f3f3f3;
630
- background-image: -webkit-gradient(linear, left top, left bottom, from(#fefefe), to(#f4f4f4));
631
- background-image: -webkit-linear-gradient(top, #fefefe, #f4f4f4);
632
- background-image: -moz-linear-gradient(top, #fefefe, #f4f4f4);
633
- background-image: -o-linear-gradient(top, #fefefe, #f4f4f4);
634
- background-image: linear-gradient(to bottom, #fefefe, #f4f4f4); }
635
-
636
- #ws_icon_selector .ws_icon_option:hover {
637
- /* Gradients and colours cribbed from WP 3.5.1 button styles */
638
- border-color: #999;
639
- background: #f3f3f3;
640
- background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f3f3f3));
641
- background-image: -webkit-linear-gradient(top, #fff, #f3f3f3);
642
- background-image: -moz-linear-gradient(top, #fff, #f3f3f3);
643
- background-image: -ms-linear-gradient(top, #fff, #f3f3f3);
644
- background-image: -o-linear-gradient(top, #fff, #f3f3f3);
645
- background-image: linear-gradient(to bottom, #fff, #f3f3f3); }
646
-
647
- #ws_icon_selector .ws_icon_option.ws_selected_icon {
648
- border-color: green;
649
- background-color: #deffca;
650
- background-image: none; }
651
-
652
- #ws_icon_selector .icon16 {
653
- float: none;
654
- margin: 0; }
655
-
656
- #ws_icon_selector .ws_icon_option .ws_icon_image.dashicons {
657
- width: 20px;
658
- height: 20px;
659
- padding: 5px; }
660
-
661
- #ws_icon_selector .ws_icon_option img {
662
- display: inline-block;
663
- margin: 0;
664
- padding: 7px;
665
- width: 16px;
666
- height: 16px; }
667
-
668
- #ws_menu_editor .ws_edit_field-icon_url input.ws_field_value {
669
- width: 220px;
670
- margin-right: 5px; }
671
-
672
- /* The icon button that displays the pop-up icon selector. */
673
- #ws_menu_editor .ws_select_icon {
674
- margin: 0;
675
- padding: 0;
676
- position: relative;
677
- box-sizing: border-box;
678
- height: 25px;
679
- min-height: 25px; }
680
- .ame-is-wp53-plus #ws_menu_editor .ws_select_icon {
681
- height: 28px;
682
- min-height: 28px;
683
- margin-top: 1px; }
684
-
685
- /* Current icon node (CSS class version, for the built-in WP icon sprites) */
686
- .ws_select_icon .icon16 {
687
- margin: 0;
688
- float: none;
689
- padding: 3px;
690
- /*
691
- The default .icon16 style has a 6px padding which would normally make it too large
692
- to fit in the button. We can't change the padding without making the background-position
693
- look wrong, so lets offset the icon so that it fits.
694
- */
695
- position: relative;
696
- top: -3px;
697
- left: -3px; }
698
- .ame-is-wp53-plus .ws_select_icon .icon16 {
699
- top: -1px; }
700
-
701
- /* Current icon node (image version) */
702
- .ws_select_icon img {
703
- margin: 0;
704
- padding: 4px;
705
- width: 16px;
706
- height: 16px; }
707
-
708
- #ws_icon_selector .ws_tool_tab_nav {
709
- display: inline-block;
710
- margin-top: 2px;
711
- position: relative; }
712
- #ws_icon_selector .ws_tool_tab_nav li {
713
- padding: 4px 10px 11px; }
714
- #ws_icon_selector .ws_tool_tab {
715
- padding: 4px 4px 2px;
716
- max-height: 324px;
717
- overflow-y: auto; }
718
-
719
- /* MP6 admin style compatibility */
720
- #ws_icon_selector .ws_icon_option .icon16::before {
721
- margin: 0;
722
- padding: 0; }
723
-
724
- .ws_select_icon .icon16::before {
725
- padding: 0;
726
- margin: 1px 0 0 2px; }
727
-
728
- #ws_choose_icon_from_media {
729
- margin: 2px; }
730
-
731
- /************************************
732
- Embedded page selector
733
- *************************************/
734
- #ws_embedded_page_selector {
735
- width: 254px;
736
- padding: 6px 0 0 0;
737
- border: 1px solid silver;
738
- border-radius: 3px;
739
- background-color: white;
740
- box-sizing: border-box;
741
- position: absolute; }
742
-
743
- .ws_page_selector_tab_nav {
744
- list-style: outside none none;
745
- padding: 0;
746
- margin: 0 0 0 6px; }
747
-
748
- .ws_page_selector_tab_nav li {
749
- display: inline-block;
750
- border: 1px solid transparent;
751
- border-bottom-width: 0;
752
- padding: 3px 5px 5px;
753
- line-height: 1.35em;
754
- margin-bottom: 0; }
755
-
756
- .ws_page_selector_tab_nav a {
757
- text-decoration: none; }
758
-
759
- .ws_page_selector_tab_nav li.ui-tabs-active {
760
- border-color: #dfdfdf;
761
- background-color: #FDFDFD;
762
- border-bottom-color: #FDFDFD; }
763
-
764
- .ws_page_selector_tab_nav li.ui-tabs-active a {
765
- color: #32373C; }
766
-
767
- .ws_page_selector_tab {
768
- border-top: 1px solid #DFDFDF;
769
- padding: 12px;
770
- /* The same padding as post editor boxes. */
771
- margin-top: -1px;
772
- background-color: #FDFDFD;
773
- border-bottom-left-radius: 3px;
774
- border-bottom-right-radius: 3px; }
775
-
776
- #ws_current_site_pages {
777
- width: 100%;
778
- min-height: 150px;
779
- max-height: 300px;
780
- margin-left: 0;
781
- margin-right: 0; }
782
-
783
- #ws_embedded_page_selector input {
784
- box-sizing: border-box;
785
- max-width: 100%; }
786
-
787
- #ws_custom_embedded_page_tab p:first-child {
788
- margin-top: 0; }
789
-
790
- /*
791
- Make the "Page" field look editable. It is read-only because the user can't change it directly (they have to use
792
- the dropdown), but we don't want it to be greyed-out.
793
- */
794
- #ws_menu_editor .ws_edit_field-embedded_page_id input.ws_field_value {
795
- background-color: white; }
796
-
797
- /************************************
798
- Menu color picker
799
- *************************************/
800
- #ws-ame-menu-color-settings {
801
- background: white;
802
- display: none; }
803
-
804
- #ame-menu-color-list {
805
- height: 500px;
806
- overflow-y: auto; }
807
-
808
- .ame-menu-color-column {
809
- min-width: 460px; }
810
-
811
- .ame-menu-color-name {
812
- display: inline-block;
813
- vertical-align: top;
814
- padding-top: 2px;
815
- line-height: 1.3;
816
- font-size: 14px;
817
- font-weight: 600;
818
- min-width: 180px; }
819
-
820
- .ame-color-option {
821
- padding: 10px 0; }
822
- .ame-color-option .wp-picker-container {
823
- display: inline-block; }
824
-
825
- .ame-advanced-menu-color {
826
- display: none; }
827
-
828
- #ws-ame-apply-colors-to-all {
829
- display: block;
830
- float: left;
831
- margin-left: 5px; }
832
-
833
- /* Color presets */
834
- #ame-color-preset-container {
835
- padding: 0 8px 8px 8px;
836
- margin-left: -8px;
837
- margin-right: -8px;
838
- margin-bottom: 4px;
839
- border-bottom: 1px solid #eee; }
840
-
841
- #ame-menu-color-presets {
842
- width: 290px;
843
- margin-right: 5px; }
844
-
845
- #ws-ame-save-color-preset {
846
- /*margin-right: 5px;*/ }
847
-
848
- a#ws-ame-delete-color-preset {
849
- color: #A00;
850
- text-decoration: none; }
851
-
852
- a#ws-ame-delete-color-preset:hover {
853
- color: #F00; }
854
-
855
- /* Color scheme display in the editor widget. */
856
- .ws_color_scheme_display {
857
- display: inline-block;
858
- box-sizing: border-box;
859
- height: 26px;
860
- width: 190px;
861
- margin-right: 5px;
862
- margin-left: 1px;
863
- padding: 2px 4px;
864
- font-size: 12px;
865
- border: 1px solid #ddd;
866
- background: white;
867
- cursor: pointer;
868
- line-height: 20px; }
869
- .ame-is-wp53-plus .ws_color_scheme_display {
870
- border-color: #7e8993;
871
- border-radius: 4px;
872
- margin-top: 1px;
873
- margin-bottom: 1px;
874
- padding: 3px 8px;
875
- height: 28px;
876
- line-height: 20px; }
877
-
878
- .ws_open_color_editor {
879
- width: 58px; }
880
-
881
- .ws_color_display_item {
882
- display: inline-block;
883
- width: 18px;
884
- height: 18px;
885
- margin-right: 4px;
886
- border: 1px solid #ccc;
887
- border-radius: 3px; }
888
-
889
- .ws_color_display_item:last-child {
890
- margin-right: 0; }
891
-
892
- /************************************
893
- Export and import
894
- *************************************/
895
- #export_dialog, #import_dialog {
896
- display: none; }
897
-
898
- .ui-widget-overlay {
899
- background-color: black;
900
- position: fixed;
901
- left: 0;
902
- top: 0;
903
- right: 0;
904
- bottom: 0;
905
- opacity: 0.70;
906
- -moz-opacity: 0.70;
907
- filter: alpha(opacity=70);
908
- width: 100%;
909
- height: 100%; }
910
-
911
- .ui-front {
912
- z-index: 10000; }
913
-
914
- .settings_page_menu_editor .ui-dialog {
915
- background: white;
916
- border: 1px solid #c0c0c0;
917
- padding: 0;
918
- -moz-border-radius: 5px;
919
- -webkit-border-radius: 5px;
920
- border-radius: 5px; }
921
- .settings_page_menu_editor .ui-dialog .ui-dialog-content {
922
- padding: 8px 8px 8px 8px;
923
- font-size: 1.1em; }
924
- .settings_page_menu_editor .ui-dialog .ame-scrollable-dialog-content {
925
- max-height: 500px;
926
- overflow-y: auto;
927
- padding-top: 0.5em; }
928
- .settings_page_menu_editor .ui-dialog-titlebar {
929
- display: block;
930
- height: 22px;
931
- margin: 0;
932
- padding: 4px 4px 4px 8px;
933
- background-color: #86A7E3;
934
- font-size: 1.0em;
935
- line-height: 22px;
936
- -webkit-border-top-left-radius: 4px;
937
- -webkit-border-top-right-radius: 4px;
938
- -moz-border-radius-topleft: 4px;
939
- -moz-border-radius-topright: 4px;
940
- border-top-left-radius: 4px;
941
- border-top-right-radius: 4px;
942
- border-bottom: 1px solid #809fd9; }
943
- .settings_page_menu_editor .ui-dialog-title {
944
- color: white;
945
- font-weight: bold; }
946
- .settings_page_menu_editor .ui-button.ui-dialog-titlebar-close {
947
- background: #86A7E3 url(../images/x.png) no-repeat center;
948
- width: 22px;
949
- height: 22px;
950
- display: block;
951
- float: right;
952
- color: white;
953
- border-radius: 3px;
954
- -moz-border-radius: 3px;
955
- -webkit-border-radius: 3px; }
956
- .settings_page_menu_editor .ui-dialog-titlebar-close:hover {
957
- /*background-image: url(../images/x-light.png);*/
958
- background-color: #a6c2f5; }
959
-
960
- #export_dialog .ws_dialog_panel {
961
- height: 50px; }
962
-
963
- #import_dialog .ws_dialog_panel {
964
- height: 64px; }
965
-
966
- .ws_dialog_panel .ame-fixed-label-text {
967
- display: inline-block;
968
- min-width: 6em; }
969
- .ws_dialog_panel .ame-inline-select-with-input {
970
- vertical-align: baseline; }
971
- .ws_dialog_panel .ame-box-side-sizes {
972
- display: flex;
973
- flex-wrap: wrap;
974
- max-width: 800px; }
975
- .ws_dialog_panel .ame-box-side-sizes .ame-fixed-label-text {
976
- min-width: 4em; }
977
- .ws_dialog_panel .ame-box-side-sizes label {
978
- margin-right: 2.5em; }
979
- .ws_dialog_panel .ame-box-side-sizes input {
980
- margin-bottom: 0.4em; }
981
- .ws_dialog_panel .ame-box-side-sizes input[type=number] {
982
- width: 6em; }
983
-
984
- .ame-flexbox-break {
985
- flex-basis: 100%;
986
- height: 0; }
987
-
988
- .ws_dialog_buttons {
989
- /*height: 30px;*/
990
- text-align: right;
991
- margin-top: 20px;
992
- margin-bottom: 1px;
993
- clear: both; }
994
-
995
- .ws_dialog_buttons .button-primary {
996
- display: block;
997
- float: left;
998
- margin-top: 0; }
999
-
1000
- .ws_dialog_buttons .button {
1001
- margin-top: 0; }
1002
-
1003
- .ws_dialog_buttons.ame-vertical-button-list {
1004
- text-align: left; }
1005
-
1006
- .ws_dialog_buttons.ame-vertical-button-list .button-primary {
1007
- float: none; }
1008
-
1009
- .ws_dialog_buttons.ame-vertical-button-list .button {
1010
- width: 100%;
1011
- text-align: left;
1012
- margin-bottom: 10px; }
1013
-
1014
- .ws_dialog_buttons.ame-vertical-button-list .button:last-child {
1015
- margin-bottom: 0; }
1016
-
1017
- #import_file_selector {
1018
- display: block;
1019
- width: 286px;
1020
- margin: 6px auto 12px; }
1021
-
1022
- #ws_start_import {
1023
- min-width: 100px; }
1024
-
1025
- #import_complete_notice {
1026
- text-align: center;
1027
- font-size: large;
1028
- padding-top: 25px; }
1029
-
1030
- #ws_import_error_response {
1031
- width: 100%; }
1032
-
1033
- .ws_dont_show_again {
1034
- display: inline-block;
1035
- margin-top: 1em; }
1036
-
1037
- /************************************
1038
- Menu access editor
1039
- *************************************/
1040
- /* The launch button */
1041
- #ws_menu_editor .ws_edit_field-access_level input.ws_field_value {
1042
- width: 190px;
1043
- margin-right: 5px; }
1044
-
1045
- .ws_launch_access_editor {
1046
- min-width: 40px;
1047
- width: 58px; }
1048
-
1049
- #ws_menu_access_editor {
1050
- width: 400px;
1051
- display: none; }
1052
-
1053
- .ws_dialog_subpanel {
1054
- margin-bottom: 1em; }
1055
- .ws_dialog_subpanel fieldset p {
1056
- margin-top: 0;
1057
- margin-bottom: 4px; }
1058
-
1059
- .ws-ame-dialog-subheading {
1060
- display: block;
1061
- font-weight: 600;
1062
- font-size: 1em;
1063
- margin: 0 0 0.2em 0; }
1064
-
1065
- #ws_menu_access_editor .ws_column_access,
1066
- #ws_menu_access_editor .ws_ext_action_check_column {
1067
- text-align: center;
1068
- width: 1em;
1069
- padding-right: 0; }
1070
-
1071
- #ws_menu_access_editor .ws_column_access input,
1072
- #ws_menu_access_editor .ws_ext_action_check_column input {
1073
- margin-right: 0; }
1074
-
1075
- #ws_menu_access_editor .ws_column_role {
1076
- white-space: nowrap; }
1077
-
1078
- #ws_role_table_body_container {
1079
- /*max-height: 400px;
1080
- overflow: auto;*/
1081
- overflow: hidden;
1082
- margin-right: -1px; }
1083
-
1084
- .ws_role_table_body {
1085
- margin-top: 2px;
1086
- max-width: 354px; }
1087
-
1088
- .ws_has_separate_header .ws_role_table_header {
1089
- border-bottom: none;
1090
- -moz-border-radius-bottomleft: 0;
1091
- -moz-border-radius-bottomright: 0;
1092
- -webkit-border-bottom-left-radius: 0;
1093
- -webkit-border-bottom-right-radius: 0;
1094
- border-bottom-left-radius: 0;
1095
- border-bottom-right-radius: 0; }
1096
-
1097
- .ws_has_separate_header .ws_role_table_body {
1098
- border-top: none;
1099
- margin-top: 0;
1100
- -moz-border-radius-topleft: 0;
1101
- -moz-border-radius-topright: 0;
1102
- -webkit-border-top-left-radius: 0;
1103
- -webkit-border-top-right-radius: 0;
1104
- border-top-left-radius: 0;
1105
- border-top-right-radius: 0; }
1106
-
1107
- .ws_role_id {
1108
- display: none; }
1109
-
1110
- #ws_extra_capability {
1111
- width: 100%; }
1112
-
1113
- #ws_role_access_container {
1114
- position: relative;
1115
- max-height: 430px;
1116
- overflow: auto; }
1117
-
1118
- #ws_role_access_overlay {
1119
- width: 100%;
1120
- height: 100%;
1121
- position: absolute;
1122
- line-height: 100%;
1123
- background: white;
1124
- filter: alpha(opacity=60);
1125
- opacity: 0.6;
1126
- -moz-opacity: 0.6; }
1127
-
1128
- #ws_role_access_overlay_content {
1129
- position: absolute;
1130
- width: 50%;
1131
- left: 22%;
1132
- top: 30%;
1133
- background: white;
1134
- padding: 8px;
1135
- border: 2px solid silver;
1136
- border-radius: 5px;
1137
- color: #555; }
1138
-
1139
- #ws_menu_access_editor div.error {
1140
- margin-left: 0;
1141
- margin-right: 0;
1142
- margin-bottom: 5px; }
1143
-
1144
- #ws_hardcoded_role_error {
1145
- display: none; }
1146
-
1147
- /*--------------------------------------------*
1148
- The CPT/taxonomy permissions panel
1149
- *--------------------------------------------*/
1150
- /*
1151
- * When there are CPT/taxonomy permissions available, the appearance of the role list changes a bit.
1152
- */
1153
- .ws_has_extended_permissions {
1154
- /* The role or actor whose CPT/taxonomy permissions are currently expanded. */ }
1155
- .ws_has_extended_permissions .ws_role_table_body .ws_column_role {
1156
- cursor: pointer; }
1157
- .ws_has_extended_permissions .ws_role_table_body .ws_column_selected_role_tip {
1158
- display: table-cell; }
1159
- .ws_has_extended_permissions .ws_role_table_body tr:hover {
1160
- background: #EAF2FA; }
1161
- .ws_has_extended_permissions .ws_role_table_body td {
1162
- border-top: 1px solid #f1f1f1; }
1163
- .ws_has_extended_permissions .ws_role_table_body tr:first-child td {
1164
- border-top-width: 0; }
1165
- .ws_has_extended_permissions .ws_role_table_body tr.ws_cpt_selected_role {
1166
- background-color: #dddddd; }
1167
- .ws_has_extended_permissions .ws_role_table_body tr.ws_cpt_selected_role .ws_column_role {
1168
- font-weight: bold; }
1169
- .ws_has_extended_permissions .ws_role_table_body tr.ws_cpt_selected_role .ws_cpt_selected_role_tip {
1170
- visibility: visible; }
1171
- .ws_has_extended_permissions .ws_role_table_body tr.ws_cpt_selected_role td {
1172
- color: #222; }
1173
-
1174
- #ws_ext_permissions_container {
1175
- float: left;
1176
- width: 352px;
1177
- padding: 0 9px 0 0; }
1178
-
1179
- #ws_ext_permissions_container_caption {
1180
- padding-left: 15px;
1181
- max-width: 352px;
1182
- position: relative;
1183
- white-space: nowrap; }
1184
-
1185
- #ws_ext_permissions_container .ws_ext_permissions_table {
1186
- margin-top: 2px; }
1187
- #ws_ext_permissions_container .ws_ext_permissions_table tr td:first-child {
1188
- padding-left: 15px; }
1189
- #ws_ext_permissions_container .ws_ext_permissions_table .ws_ext_group_title {
1190
- padding-bottom: 0;
1191
- font-weight: bold; }
1192
- #ws_ext_permissions_container .ws_ext_permissions_table .ws_ext_action_check_column,
1193
- #ws_ext_permissions_container .ws_ext_permissions_table .ws_ext_action_name_column {
1194
- padding-top: 3px;
1195
- padding-bottom: 3px; }
1196
- #ws_ext_permissions_container .ws_ext_permissions_table tr.ws_ext_padding_row td {
1197
- padding: 0 0 0 0;
1198
- height: 1px; }
1199
- #ws_ext_permissions_container .ws_ext_permissions_table .ws_same_as_required_cap {
1200
- text-decoration: underline; }
1201
- #ws_ext_permissions_container .ws_ext_permissions_table .ws_ext_has_custom_setting label.ws_ext_action_name::after {
1202
- content: " *"; }
1203
-
1204
- #ws_ext_permissions_container #ws_ext_toggle_capability_names {
1205
- cursor: pointer;
1206
- position: absolute;
1207
- right: 0;
1208
- color: #0073aa; }
1209
- #ws_ext_permissions_container.ws_ext_readable_names_enabled #ws_ext_toggle_capability_names {
1210
- color: #b4b9be; }
1211
- #ws_ext_permissions_container .ws_ext_readable_name {
1212
- display: none; }
1213
- #ws_ext_permissions_container .ws_ext_capability {
1214
- display: inline; }
1215
- #ws_ext_permissions_container.ws_ext_readable_names_enabled .ws_ext_readable_name {
1216
- display: inline; }
1217
- #ws_ext_permissions_container.ws_ext_readable_names_enabled .ws_ext_capability {
1218
- display: none; }
1219
-
1220
- #ws_ext_permissions_container #ws_taxonomy_permissions_table tr:first-child td {
1221
- padding-top: 8px; }
1222
-
1223
- /* The "selected role" indicator. */
1224
- .ws_cpt_selected_role_tip {
1225
- display: block;
1226
- visibility: hidden;
1227
- box-sizing: border-box;
1228
- width: 26px;
1229
- height: 26px;
1230
- position: absolute;
1231
- right: 0;
1232
- background: white;
1233
- transform: translate(1px, 0) rotate(-45deg);
1234
- transform-origin: top right; }
1235
-
1236
- .ws_role_table_body .ws_column_selected_role_tip {
1237
- display: none;
1238
- padding: 0;
1239
- width: 40px;
1240
- height: 100%;
1241
- text-align: right;
1242
- overflow: visible;
1243
- position: relative;
1244
- cursor: pointer; }
1245
-
1246
- .ws_ame_breadcrumb_separator {
1247
- color: #999; }
1248
-
1249
- #ws_menu_editor .ws_ext_permissions_indicator {
1250
- font-size: 16px;
1251
- height: 16px;
1252
- width: 16px;
1253
- visibility: hidden;
1254
- vertical-align: bottom;
1255
- cursor: pointer;
1256
- color: #4aa100; }
1257
-
1258
- #ws_menu_editor.ws_is_actor_view .ws_ext_permissions_indicator {
1259
- visibility: visible; }
1260
-
1261
- /************************************
1262
- Visible users dialog
1263
- *************************************/
1264
- #ws_visible_users_dialog {
1265
- background: white;
1266
- padding: 8px; }
1267
-
1268
- #ws_user_selection_panels {
1269
- min-width: 710px; }
1270
- #ws_user_selection_panels .ws_user_selection_panel {
1271
- display: block;
1272
- float: left;
1273
- position: relative;
1274
- -webkit-box-sizing: border-box;
1275
- -moz-box-sizing: border-box;
1276
- box-sizing: border-box;
1277
- width: 350px;
1278
- height: 400px;
1279
- border: 1px solid #e5e5e5;
1280
- margin-right: 10px;
1281
- padding: 10px; }
1282
- #ws_user_selection_panels #ws_user_selection_target_panel {
1283
- margin-right: 0; }
1284
- #ws_user_selection_panels #ws_available_user_query {
1285
- -webkit-box-sizing: border-box;
1286
- -moz-box-sizing: border-box;
1287
- box-sizing: border-box;
1288
- width: 100%;
1289
- max-height: 28px; }
1290
- #ws_user_selection_panels .ws_user_list_wrapper {
1291
- position: absolute;
1292
- top: 50px;
1293
- left: 10px;
1294
- right: 10px;
1295
- height: 338px;
1296
- overflow-x: auto;
1297
- overflow-y: auto; }
1298
- #ws_user_selection_panels .ws_user_selection_list {
1299
- min-height: 20px;
1300
- border-width: 0;
1301
- -webkit-box-shadow: none;
1302
- -moz-box-shadow: none;
1303
- box-shadow: none; }
1304
- #ws_user_selection_panels .ws_user_selection_list .ws_user_action_column {
1305
- width: 20px;
1306
- text-align: center;
1307
- padding-top: 9px;
1308
- padding-bottom: 0; }
1309
- #ws_user_selection_panels .ws_user_selection_list .ws_user_action_button {
1310
- cursor: pointer;
1311
- color: #b4b9be; }
1312
- #ws_user_selection_panels .ws_user_selection_list .ws_user_username_column {
1313
- padding-left: 0; }
1314
- #ws_user_selection_panels .ws_user_selection_list .ws_user_display_name_column {
1315
- white-space: nowrap; }
1316
- #ws_user_selection_panels #ws_available_users tr {
1317
- cursor: pointer; }
1318
- #ws_user_selection_panels #ws_available_users tr:hover, #ws_user_selection_panels #ws_available_users tr.ws_user_best_match {
1319
- background-color: #eaf2fa; }
1320
- #ws_user_selection_panels #ws_available_users tr:hover .ws_user_action_button {
1321
- color: #7ad03a; }
1322
- #ws_user_selection_panels #ws_selected_users .ws_user_action_button::before {
1323
- content: "\f158"; }
1324
- #ws_user_selection_panels #ws_selected_users .ws_user_action_button:hover {
1325
- color: #dd3d36; }
1326
- #ws_user_selection_panels #ws_selected_users .ws_user_action_column {
1327
- padding-left: 6px; }
1328
- #ws_user_selection_panels #ws_selected_users .ws_user_display_name_column {
1329
- display: none; }
1330
- #ws_user_selection_panels #ws_selected_users tr.ws_user_must_be_selected .ws_user_action_button {
1331
- display: none; }
1332
- #ws_user_selection_panels #ws_selected_users_caption {
1333
- font-size: 14px;
1334
- line-height: 1.4em;
1335
- padding: 7px 10px;
1336
- color: #555;
1337
- font-weight: 600; }
1338
- #ws_user_selection_panels::after {
1339
- display: block;
1340
- height: 1px;
1341
- visibility: hidden;
1342
- content: ' ';
1343
- clear: both; }
1344
-
1345
- #ws_loading_users_indicator {
1346
- position: absolute;
1347
- right: 10px;
1348
- bottom: 10px;
1349
- margin-right: 0;
1350
- margin-bottom: 0; }
1351
-
1352
- /************************************
1353
- Menu deletion error
1354
- *************************************/
1355
- #ws-ame-menu-deletion-error {
1356
- max-width: 400px; }
1357
-
1358
- /************************************
1359
- Tooltips and hints
1360
- *************************************/
1361
- .ws_tooltip_trigger, .ws_field_tooltip_trigger {
1362
- cursor: pointer; }
1363
-
1364
- .ws_tooltip_content_list {
1365
- list-style: disc;
1366
- margin-left: 1em;
1367
- margin-bottom: 0; }
1368
-
1369
- .ws_tooltip_node {
1370
- font-size: 13px;
1371
- line-height: 1.3;
1372
- border-radius: 3px;
1373
- max-width: 300px; }
1374
-
1375
- .ws_field_tooltip_trigger .dashicons {
1376
- font-size: 16px;
1377
- height: 16px;
1378
- vertical-align: bottom; }
1379
-
1380
- .ws_field_tooltip_trigger {
1381
- color: #a1a1a1; }
1382
-
1383
- #ws_plugin_settings_form .ws_tooltip_trigger .dashicons {
1384
- font-size: 18px; }
1385
-
1386
- .ws_ame_custom_postbox .ws_tooltip_trigger .dashicons {
1387
- font-size: 18px;
1388
- height: 18px;
1389
- vertical-align: bottom; }
1390
-
1391
- .ws_wide_tooltip {
1392
- max-width: 450px; }
1393
-
1394
- .ws_hint {
1395
- background: #FFFFE0;
1396
- border: 1px solid #E6DB55;
1397
- margin-bottom: 0.5em;
1398
- border-radius: 3px;
1399
- position: relative;
1400
- padding-right: 20px; }
1401
-
1402
- .ws_hint_close {
1403
- border: 1px solid #E6DB55;
1404
- border-right: none;
1405
- border-top: none;
1406
- color: #dcc500;
1407
- font-weight: bold;
1408
- cursor: pointer;
1409
- width: 18px;
1410
- text-align: center;
1411
- border-radius: 3px;
1412
- position: absolute;
1413
- right: 0;
1414
- top: 0; }
1415
-
1416
- .ws_hint_close:hover {
1417
- background-color: #ffef4c;
1418
- border-color: #e0b900;
1419
- color: black; }
1420
-
1421
- .ws_hint_content {
1422
- padding: 0.4em 0 0.4em 0.4em; }
1423
-
1424
- .ws_hint_content ul {
1425
- list-style: disc;
1426
- list-style-position: inside;
1427
- margin-left: 0.5em; }
1428
-
1429
- .ws_ame_doc_box .hndle, .ws_ame_custom_postbox .hndle {
1430
- cursor: default !important;
1431
- border-bottom: 1px solid #ccd0d4; }
1432
- .ws_ame_doc_box .handlediv, .ws_ame_custom_postbox .handlediv {
1433
- display: block;
1434
- float: right; }
1435
- .ws_ame_doc_box .inside, .ws_ame_custom_postbox .inside {
1436
- margin-bottom: 0; }
1437
- .ws_ame_doc_box ul, .ws_ame_custom_postbox ul {
1438
- list-style: disc outside;
1439
- margin-left: 1em; }
1440
- .ws_ame_doc_box li > ul, .ws_ame_custom_postbox li > ul {
1441
- margin-top: 6px; }
1442
- .ws_ame_doc_box .button-link .toggle-indicator::before, .ws_ame_custom_postbox .button-link .toggle-indicator::before {
1443
- margin-top: 4px;
1444
- width: 20px;
1445
- -webkit-border-radius: 50%;
1446
- border-radius: 50%;
1447
- text-indent: -1px;
1448
- content: "\f142";
1449
- display: inline-block;
1450
- font: normal 20px/1 dashicons;
1451
- -webkit-font-smoothing: antialiased;
1452
- -moz-osx-font-smoothing: grayscale;
1453
- text-decoration: none !important; }
1454
- .ws_ame_doc_box.closed .button-link .toggle-indicator::before, .ws_ame_custom_postbox.closed .button-link .toggle-indicator::before {
1455
- content: "\f140"; }
1456
-
1457
- .ws_basic_container .ws_ame_custom_postbox {
1458
- margin-left: 2px;
1459
- margin-right: 2px; }
1460
-
1461
- .ws_ame_custom_postbox .ame-tutorial-list {
1462
- margin: 0; }
1463
- .ws_ame_custom_postbox .ame-tutorial-list a {
1464
- text-decoration: none;
1465
- display: block;
1466
- padding: 4px; }
1467
- .ws_ame_custom_postbox .ame-tutorial-list ul {
1468
- margin-left: 1em; }
1469
- .ws_ame_custom_postbox .ame-tutorial-list li {
1470
- display: block;
1471
- margin: 0;
1472
- list-style: none; }
1473
-
1474
- /************************************
1475
- Copy Permissions dialog
1476
- *************************************/
1477
- #ws-ame-copy-permissions-dialog select {
1478
- min-width: 280px; }
1479
-
1480
- /*********************************************
1481
- Capability suggestions and preview
1482
- **********************************************/
1483
- #ws_capability_suggestions {
1484
- padding: 4px;
1485
- width: 350px;
1486
- border: 1px solid #cdd5d5;
1487
- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
1488
- background: #fff;
1489
- border-top-right-radius: 3px;
1490
- border-bottom-right-radius: 3px; }
1491
- #ws_capability_suggestions #ws_previewed_caps {
1492
- margin-top: 0;
1493
- margin-bottom: 6px; }
1494
- #ws_capability_suggestions td, #ws_capability_suggestions th {
1495
- padding-top: 3px;
1496
- padding-bottom: 3px; }
1497
- #ws_capability_suggestions tr.ws_preview_has_access .ws_ame_role_name {
1498
- background-color: lightgreen; }
1499
- #ws_capability_suggestions .ws_ame_suggested_capability {
1500
- cursor: pointer; }
1501
- #ws_capability_suggestions .ws_ame_suggested_capability:hover {
1502
- background-color: #d0f2d0; }
1503
-
1504
- /*********************************************
1505
- Settings page stuff
1506
- **********************************************/
1507
- #ws_plugin_settings_form figure {
1508
- margin-left: 0;
1509
- margin-top: 0;
1510
- margin-bottom: 1em; }
1511
-
1512
- .ame-available-add-ons tr:first-of-type td {
1513
- margin-top: 0;
1514
- padding-top: 0; }
1515
- .ame-available-add-ons td {
1516
- padding-top: 10px;
1517
- padding-bottom: 10px; }
1518
- .ame-available-add-ons .ame-add-on-heading {
1519
- padding-left: 0; }
1520
-
1521
- .ame-add-on-name {
1522
- font-weight: 600; }
1523
-
1524
- .ame-add-on-details-link::after {
1525
- /*content: " \f504";
1526
- font-family: dashicons, sans-serif;*/ }
1527
-
1528
- /*********************************************
1529
- WordPress 5.3+ consistent styles
1530
- **********************************************/
1531
- .ame-is-wp53-plus .ws_edit_field input[type="button"] {
1532
- margin-top: 1px; }
1533
-
1534
- /*********************************************
1535
- CSS border style selector
1536
- **********************************************/
1537
- .ame-css-border-styles .ame-fixed-label-text {
1538
- min-width: 5em; }
1539
- .ame-css-border-styles .ame-border-sample-container {
1540
- display: inline-block;
1541
- vertical-align: top;
1542
- min-height: 28px; }
1543
- .ame-css-border-styles .ame-border-sample {
1544
- display: inline-block;
1545
- width: 14em;
1546
- border-top: 0.3em solid #444; }
1547
-
1548
- /*********************************************
1549
- Miscellaneous
1550
- **********************************************/
1551
- #ws_sidebar_pro_ad {
1552
- min-width: 225px;
1553
- margin-top: 5px;
1554
- margin-left: 3px;
1555
- position: fixed;
1556
- right: 20px;
1557
- bottom: 40px;
1558
- z-index: 100; }
1559
-
1560
- .ws-ame-icon-radio-button-group > label {
1561
- display: inline-block;
1562
- padding: 8px;
1563
- border: 1px solid #ccd0d4;
1564
- border-radius: 2px;
1565
- margin-right: 0.5em; }
1566
-
1567
- span.description {
1568
- color: #666;
1569
- font-style: italic; }
1570
-
1571
- .test-wrap {
1572
- background-color: #444444;
1573
- padding: 30px; }
1574
-
1575
- .test-container {
1576
- width: 400px;
1577
- height: 200px;
1578
- background-color: white;
1579
- border: 1px solid black;
1580
- border-radius: 10px;
1581
- overflow: hidden; }
1582
-
1583
- .test-header {
1584
- background-color: #67d6ff;
1585
- padding: 6px;
1586
- border-top-left-radius: 8px;
1587
- border-top-right-radius: 8px; }
1588
-
1589
- .test-content {
1590
- padding: 8px; }
1591
-
1592
- /*********************************************
1593
- "Test access" dialog
1594
- **********************************************/
1595
- #ws_ame_test_access_screen {
1596
- display: none;
1597
- background: #fcfcfc; }
1598
-
1599
- #ws_ame_test_inputs {
1600
- padding-bottom: 16px; }
1601
-
1602
- .ws_ame_test_input {
1603
- display: block;
1604
- float: left;
1605
- width: 100%;
1606
- margin: 2px 0;
1607
- box-sizing: content-box; }
1608
-
1609
- .ws_ame_test_input_name {
1610
- display: block;
1611
- float: left;
1612
- width: 35%;
1613
- margin-right: 4%;
1614
- text-align: right;
1615
- padding-top: 6px;
1616
- line-height: 16px; }
1617
-
1618
- .ws_ame_test_input_value {
1619
- display: block;
1620
- float: right;
1621
- width: 60%;
1622
- -webkit-box-sizing: border-box;
1623
- -moz-box-sizing: border-box;
1624
- box-sizing: border-box; }
1625
-
1626
- #ws_ame_test_actions {
1627
- float: left;
1628
- width: 100%;
1629
- margin-top: 1em; }
1630
-
1631
- #ws_ame_test_button_container {
1632
- width: 35%;
1633
- margin-right: 4%;
1634
- float: left;
1635
- text-align: right; }
1636
-
1637
- #ws_ame_test_progress {
1638
- display: none;
1639
- width: 60%;
1640
- float: right; }
1641
- #ws_ame_test_progress .spinner {
1642
- float: none;
1643
- vertical-align: bottom;
1644
- margin-left: 0;
1645
- margin-right: 4px; }
1646
-
1647
- #ws_ame_test_access_body {
1648
- width: 100%;
1649
- position: relative;
1650
- border: 1px solid #ddd;
1651
- -webkit-border-radius: 3px;
1652
- -moz-border-radius: 3px;
1653
- border-radius: 3px; }
1654
-
1655
- #ws_ame_test_frame_container {
1656
- margin-right: 250px;
1657
- background: white;
1658
- min-height: 500px;
1659
- position: relative; }
1660
-
1661
- #ws_ame_test_access_frame {
1662
- -webkit-box-sizing: border-box;
1663
- -moz-box-sizing: border-box;
1664
- box-sizing: border-box;
1665
- width: 100%;
1666
- height: 100%;
1667
- min-height: 500px;
1668
- border: none;
1669
- margin: 0;
1670
- padding: 0; }
1671
-
1672
- #ws_ame_test_access_sidebar {
1673
- -webkit-box-sizing: border-box;
1674
- -moz-box-sizing: border-box;
1675
- box-sizing: border-box;
1676
- position: absolute;
1677
- top: 0;
1678
- right: 0;
1679
- bottom: 0;
1680
- width: 250px;
1681
- padding: 16px 24px;
1682
- background-color: #f3f3f3;
1683
- border-left: 1px solid #ddd; }
1684
- #ws_ame_test_access_sidebar h4:first-of-type {
1685
- margin-top: 0; }
1686
-
1687
- #ws_ame_test_frame_placeholder {
1688
- display: block;
1689
- padding: 16px 24px; }
1690
-
1691
- #ws_ame_test_output {
1692
- display: none; }
1693
-
1694
- /*# sourceMappingURL=menu-editor.css.map */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @charset "UTF-8";
2
+ /* Admin Menu Editor CSS file */
3
+ #ws_menu_editor {
4
+ min-width: 780px;
5
+ }
6
+
7
+ .ame-is-free-version #ws_menu_editor {
8
+ margin-top: 9px;
9
+ }
10
+
11
+ .ws_main_container {
12
+ margin: 2px;
13
+ width: 316px;
14
+ float: left;
15
+ display: block;
16
+ border: 1px solid #ccd0d4;
17
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
18
+ background-color: #FFFFFF;
19
+ border-radius: 0px;
20
+ -moz-border-radius: 0px;
21
+ -webkit-border-radius: 0px;
22
+ }
23
+
24
+ .ws_box {
25
+ min-height: 30px;
26
+ width: 100%;
27
+ margin: 0;
28
+ }
29
+
30
+ .ws_basic_container {
31
+ float: left;
32
+ display: block;
33
+ }
34
+
35
+ .ws_dropzone {
36
+ display: block;
37
+ box-sizing: border-box;
38
+ margin: 2px 6px;
39
+ border: 3px none #b4b9be;
40
+ height: 31px;
41
+ }
42
+
43
+ .ws_dropzone_active,
44
+ .ws_dropzone_hover,
45
+ .ws_top_to_submenu_drop_hover .ws_dropzone {
46
+ border-style: dashed;
47
+ }
48
+
49
+ .ws_dropzone_hover,
50
+ .ws_top_to_submenu_drop_hover .ws_dropzone {
51
+ border-width: 1px;
52
+ }
53
+
54
+ /*************************************************
55
+ Actor UI
56
+ *************************************************/
57
+ #ws_actor_selector li:after {
58
+ content: "| ";
59
+ }
60
+
61
+ #ws_actor_selector li:last-child:after {
62
+ content: "";
63
+ }
64
+
65
+ #ws_actor_selector li a {
66
+ display: inline-block;
67
+ text-align: center;
68
+ }
69
+ #ws_actor_selector li a::before {
70
+ display: block;
71
+ content: attr(data-text);
72
+ font-weight: bold;
73
+ height: 1px;
74
+ overflow: hidden;
75
+ visibility: hidden;
76
+ margin-bottom: -1px;
77
+ }
78
+
79
+ #ws_actor_selector {
80
+ margin-top: 6px;
81
+ }
82
+
83
+ /**
84
+ * The checkbox that lets the user show/hide a menu for the currently selected actor.
85
+ */
86
+ #ws_menu_editor .ws_actor_access_checkbox,
87
+ #ws_menu_editor input[type=checkbox].ws_actor_access_checkbox {
88
+ margin-right: 2px;
89
+ margin-left: 2px;
90
+ margin-top: 1px;
91
+ vertical-align: text-top;
92
+ }
93
+ #ws_menu_editor .ws_actor_access_checkbox:indeterminate:before,
94
+ #ws_menu_editor input[type=checkbox].ws_actor_access_checkbox:indeterminate:before {
95
+ content: "■";
96
+ color: #1e8cbe;
97
+ margin: -3px 0 0 -1px;
98
+ font: 400 14px/1 dashicons;
99
+ float: left;
100
+ display: inline-block;
101
+ vertical-align: middle;
102
+ width: 16px;
103
+ -webkit-font-smoothing: antialiased;
104
+ }
105
+
106
+ @media screen and (max-width: 782px) {
107
+ #ws_menu_editor input[type=checkbox].ws_actor_access_checkbox:indeterminate:before {
108
+ margin: -6px 0 0 1px;
109
+ font: 400 26px/1 dashicons;
110
+ }
111
+ }
112
+ /* The checkbox is only visible when viewing the menu configuration for a specific actor. */
113
+ #ws_menu_editor .ws_actor_access_checkbox {
114
+ display: none;
115
+ }
116
+
117
+ #ws_menu_editor.ws_is_actor_view .ws_actor_access_checkbox {
118
+ display: inline-block;
119
+ }
120
+
121
+ /* Gray-out items inaccessible to the currently selected actor */
122
+ .ws_is_actor_view .ws_container.ws_is_hidden_for_actor {
123
+ background-color: #F9F9F9;
124
+ }
125
+
126
+ .ws_is_actor_view .ws_is_hidden_for_actor .ws_item_title {
127
+ color: #777;
128
+ }
129
+
130
+ /*
131
+ * The sidebar
132
+ */
133
+ #ws_editor_sidebar {
134
+ width: auto;
135
+ padding: 2px;
136
+ }
137
+
138
+ #ws_menu_editor .ws_main_button {
139
+ clear: both;
140
+ display: block;
141
+ margin: 4px;
142
+ width: 130px;
143
+ }
144
+
145
+ #ws_menu_editor #ws_save_menu {
146
+ margin-bottom: 20px;
147
+ }
148
+
149
+ #ws_menu_editor #ws_toggle_editor_layout {
150
+ display: none;
151
+ }
152
+
153
+ #ws_menu_editor .ws_sidebar_button_separator {
154
+ display: block;
155
+ height: 4px;
156
+ margin: 0;
157
+ padding: 0;
158
+ }
159
+
160
+ /*
161
+ * Page heading and tabs
162
+ */
163
+ #ws_ame_editor_heading {
164
+ float: left;
165
+ }
166
+
167
+ /*
168
+ * Menu components and widgets
169
+ */
170
+ .ws_container {
171
+ display: block;
172
+ width: 296px;
173
+ padding: 3px;
174
+ margin: 2px 0 2px 6px;
175
+ }
176
+ body.rtl .ws_container {
177
+ margin-right: 6px;
178
+ margin-left: 0;
179
+ }
180
+
181
+ .ws_submenu {
182
+ min-height: 2em;
183
+ }
184
+
185
+ .ws_item_head {
186
+ padding: 0;
187
+ }
188
+
189
+ .ws_item_title {
190
+ display: inline-block;
191
+ padding: 2px;
192
+ cursor: default;
193
+ font-size: 13px;
194
+ line-height: 18px;
195
+ }
196
+
197
+ .ws_edit_link {
198
+ float: right;
199
+ margin-right: 0;
200
+ cursor: pointer;
201
+ display: block;
202
+ width: 40px;
203
+ height: 22px;
204
+ border-radius: 3px;
205
+ -moz-border-radius: 3px;
206
+ -webkit-border-radius: 3px;
207
+ text-decoration: none;
208
+ }
209
+
210
+ .ws_menu_drop_hover {
211
+ background-color: #43b529 !important;
212
+ }
213
+
214
+ .ws_container.ui-sortable-helper * {
215
+ cursor: move !important;
216
+ }
217
+
218
+ .ws_container.ws_sortable_placeholder {
219
+ outline: 1px dashed #b4b9be;
220
+ outline-offset: -1px;
221
+ background: none;
222
+ border-color: transparent;
223
+ }
224
+
225
+ /*
226
+ If you ever want to apply a right-arrow style to the currently selected menu item,
227
+ you can do it like this. Commented out for now since it doesn't look all that great,
228
+ but might be useful in the future.
229
+ */
230
+ /*
231
+ .ws_container {
232
+ position: relative;
233
+ }
234
+
235
+ .ws_menu.ws_active::after {
236
+ content: "";
237
+ display: block;
238
+ z-index: 1002;
239
+
240
+ border-left: 14px solid #8EB0F1;
241
+ border-top: 15px solid rgba(255, 255, 255, 0.1);
242
+ border-bottom: 15px solid rgba(255, 255, 255, 0.1);
243
+ background: transparent;
244
+
245
+ position: absolute;
246
+ right: -14px;
247
+ top: -1px;
248
+
249
+ width: 0;
250
+ height: 0;
251
+ }
252
+ */
253
+ /*
254
+ * A left-arrow style alternative. This one is image-based and doesn't suffer from the finicky sizing issues
255
+ * of CSS triangles.
256
+ */
257
+ .ws_container {
258
+ position: relative;
259
+ }
260
+
261
+ .ws_menu.ws_active::after {
262
+ content: "";
263
+ display: block;
264
+ position: absolute;
265
+ right: -19px;
266
+ top: -1px;
267
+ width: 19px;
268
+ height: 30px;
269
+ background: transparent url("../images/submenu-tip.png") no-repeat center;
270
+ }
271
+
272
+ .ws_container.ws_menu_separator.ws_active::after,
273
+ .ws_container.ui-sortable-helper::after {
274
+ background-image: none;
275
+ }
276
+
277
+ /****************************************
278
+ Per-menu settings fields & panels
279
+ *****************************************/
280
+ .ws_editbox {
281
+ display: block;
282
+ padding: 4px;
283
+ border-radius: 2px;
284
+ border-top-right-radius: 0;
285
+ -moz-border-radius: 2px;
286
+ -moz-border-radius-topright: 0;
287
+ -webkit-border-radius: 2px;
288
+ -webkit-border-top-right-radius: 0;
289
+ }
290
+
291
+ .ws_edit_panel {
292
+ margin: 0;
293
+ padding: 0;
294
+ border: none;
295
+ }
296
+
297
+ .ws_edit_field {
298
+ margin-bottom: 6px;
299
+ min-height: 45px;
300
+ }
301
+ .ws_edit_field:after {
302
+ visibility: hidden;
303
+ display: block;
304
+ height: 0;
305
+ font-size: 0;
306
+ content: " ";
307
+ clear: both;
308
+ }
309
+
310
+ .ws_edit_field-custom {
311
+ margin-top: 10px;
312
+ }
313
+
314
+ .ws_edit_field.ws_no_field_caption {
315
+ margin-top: 10px;
316
+ padding-left: 1px;
317
+ height: 25px;
318
+ min-height: 25px;
319
+ }
320
+
321
+ /*
322
+ * Group headings
323
+ */
324
+ .ws_edit_field.ws_field_group_heading {
325
+ height: 1px;
326
+ min-height: 0;
327
+ padding-top: 0;
328
+ background: #ccc;
329
+ margin: 8px -4px 5px;
330
+ }
331
+ .ws_edit_field.ws_field_group_heading span {
332
+ display: none;
333
+ font-weight: bold;
334
+ }
335
+
336
+ /* The reset-to-default button */
337
+ .ws_reset_button {
338
+ display: block;
339
+ float: right;
340
+ margin-left: 4px;
341
+ margin-top: 2px;
342
+ margin-right: 6px;
343
+ cursor: pointer;
344
+ width: 16px;
345
+ height: 16px;
346
+ vertical-align: top;
347
+ background: url("../images/pencil_delete_gray.png") no-repeat center;
348
+ }
349
+ .ame-is-wp53-plus .ws_reset_button {
350
+ margin-top: 5px;
351
+ }
352
+
353
+ .ws_reset_button:hover {
354
+ background-image: url("../images/pencil_delete.png");
355
+ }
356
+
357
+ .ws_input_default input,
358
+ .ws_input_default select,
359
+ .ws_input_default .ws_color_scheme_display {
360
+ color: gray;
361
+ }
362
+
363
+ /* No reset button for fields set to the default value and fields without a default value */
364
+ .ws_input_default .ws_reset_button,
365
+ .ws_has_no_default .ws_reset_button {
366
+ visibility: hidden;
367
+ }
368
+
369
+ /* The input box in each field editor */
370
+ #ws_menu_editor .ws_editbox input[type=text],
371
+ #ws_menu_editor .ws_editbox select {
372
+ display: block;
373
+ float: left;
374
+ width: 254px;
375
+ height: 25px;
376
+ font-size: 12px;
377
+ line-height: 17px;
378
+ padding-top: 3px;
379
+ padding-bottom: 3px;
380
+ }
381
+ .ame-is-wp53-plus #ws_menu_editor .ws_editbox input[type=text],
382
+ .ame-is-wp53-plus #ws_menu_editor .ws_editbox select {
383
+ height: 28px;
384
+ }
385
+
386
+ #ws_menu_editor .ws_edit_field label {
387
+ display: block;
388
+ float: left;
389
+ }
390
+
391
+ #ws_menu_editor .ws_edit_field-custom input[type=checkbox] {
392
+ margin-top: 0;
393
+ }
394
+
395
+ #ws_menu_editor input[type=text].ws_field_value {
396
+ min-height: 25px;
397
+ }
398
+ .ame-is-wp53-plus #ws_menu_editor input[type=text].ws_field_value {
399
+ min-height: 28px;
400
+ }
401
+
402
+ /* Dropdown button for combo-box fields */
403
+ #ws_menu_editor .ws_dropdown_button,
404
+ #ws_menu_access_editor .ws_dropdown_button {
405
+ box-sizing: border-box;
406
+ width: 25px;
407
+ height: 25px;
408
+ min-height: 25px;
409
+ margin: 1px 1px 1px 0;
410
+ padding: 0;
411
+ text-align: center;
412
+ font-size: 9px !important;
413
+ line-height: 25px;
414
+ border-color: #dfdfdf;
415
+ box-shadow: none;
416
+ border-top-right-radius: 3px;
417
+ border-bottom-right-radius: 3px;
418
+ border-top-left-radius: 0;
419
+ border-bottom-left-radius: 0;
420
+ -moz-border-radius-topright: 3px;
421
+ -moz-border-radius-bottomright: 3px;
422
+ -moz-border-radius-topleft: 0;
423
+ -moz-border-radius-bottomleft: 0;
424
+ -webkit-border-top-right-radius: 3px;
425
+ -webkit-border-bottom-right-radius: 3px;
426
+ -webkit-border-top-left-radius: 0;
427
+ -webkit-border-bottom-left-radius: 0;
428
+ }
429
+
430
+ .ame-is-wp53-plus #ws_menu_editor .ws_dropdown_button,
431
+ #ws_menu_access_editor.ame-is-wp53-plus .ws_dropdown_button {
432
+ height: 28px;
433
+ border-color: #7e8993;
434
+ background-color: white;
435
+ border-left-style: none;
436
+ font-size: 10px !important;
437
+ line-height: 24px;
438
+ color: #555;
439
+ }
440
+ .ame-is-wp53-plus #ws_menu_editor .ws_dropdown_button:hover,
441
+ #ws_menu_access_editor.ame-is-wp53-plus .ws_dropdown_button:hover {
442
+ color: #23282d;
443
+ }
444
+
445
+ #ws_menu_access_editor .ws_dropdown_button {
446
+ display: inline-block;
447
+ height: 27px;
448
+ }
449
+
450
+ #ws_menu_access_editor.ame-is-wp53-plus .ws_dropdown_button {
451
+ height: 30px;
452
+ }
453
+
454
+ #ws_menu_editor .ws_dropdown_button {
455
+ display: block;
456
+ float: left;
457
+ }
458
+
459
+ /*
460
+ The appearance and size of combo-box fields need to be changed
461
+ to accommodate the drop-down button.
462
+ */
463
+ #ws_menu_editor .ws_has_dropdown input.ws_field_value,
464
+ #ws_menu_access_editor input.ws_has_dropdown {
465
+ margin-right: 0;
466
+ border-right: 0;
467
+ border-top-right-radius: 0;
468
+ border-bottom-right-radius: 0;
469
+ -moz-border-radius-topright: 0;
470
+ -moz-border-radius-bottomright: 0;
471
+ -webkit-border-top-right-radius: 0;
472
+ -webkit-border-bottom-right-radius: 0;
473
+ }
474
+
475
+ #ws_menu_access_editor input.ws_has_dropdown {
476
+ width: 90%;
477
+ box-sizing: border-box;
478
+ height: 27px;
479
+ }
480
+
481
+ #ws_menu_access_editor.ame-is-wp53-plus input.ws_has_dropdown {
482
+ height: 30px;
483
+ }
484
+
485
+ #ws_menu_editor .ws_has_dropdown input.ws_field_value {
486
+ width: 229px;
487
+ }
488
+
489
+ /* Unlike others, this field is just a single checkbox, so it has a smaller height */
490
+ #ws_menu_editor .ws_edit_field-custom {
491
+ height: 16px;
492
+ }
493
+
494
+ /*
495
+ * "Show/hide advanced fields"
496
+ */
497
+ .ws_toggle_container {
498
+ text-align: right;
499
+ margin-right: 27px;
500
+ }
501
+
502
+ .ws_toggle_advanced_fields {
503
+ color: #6087CB;
504
+ text-decoration: none;
505
+ font-size: 0.85em;
506
+ }
507
+
508
+ .ws_toggle_advanced_fields:visited, .ws_toggle_advanced_fields:active {
509
+ color: #6087CB;
510
+ }
511
+
512
+ .ws_toggle_advanced_fields:hover {
513
+ color: #d54e21;
514
+ text-decoration: underline;
515
+ }
516
+
517
+ /************************************
518
+ Menu flags
519
+ *************************************/
520
+ .ws_flag_container {
521
+ float: right;
522
+ margin-right: 4px;
523
+ padding-top: 2px;
524
+ }
525
+
526
+ .ws_flag {
527
+ display: block;
528
+ float: right;
529
+ width: 16px;
530
+ height: 16px;
531
+ margin-left: 4px;
532
+ background-repeat: no-repeat;
533
+ }
534
+
535
+ /* user-created items */
536
+ .ws_custom_flag {
537
+ background-image: url("../images/page-add.png");
538
+ }
539
+
540
+ /* unused items - those that are in the default menu but not in the custom one */
541
+ .ws_unused_flag {
542
+ background-image: url("../images/new-menu-badge.png");
543
+ width: 31px;
544
+ }
545
+
546
+ /* hidden items */
547
+ .ws_hidden_flag {
548
+ background-image: url("../images/page-invisible.png");
549
+ }
550
+
551
+ /* items with custom permissions for the selected actor */
552
+ .ws_custom_actor_permissions_flag {
553
+ font: 16px/1 "dashicons";
554
+ }
555
+
556
+ .ws_custom_actor_permissions_flag::before {
557
+ /*content: "\f160";*/
558
+ /* padlock */
559
+ content: "";
560
+ /* human silhouette */
561
+ color: black;
562
+ filter: alpha(opacity=25);
563
+ /*IE 5-7*/
564
+ opacity: 0.25;
565
+ }
566
+
567
+ /* Hidden from everyone except the current user and Super Admin. */
568
+ .ws_hidden_from_others_flag {
569
+ background-image: url("../images/font-awesome/eye-slash.png");
570
+ }
571
+
572
+ /* Item visibility can't be determined because it depends on a meta capability. */
573
+ .ws_uncertain_meta_cap_flag::before {
574
+ font: 16px/1 "dashicons";
575
+ content: "";
576
+ color: black;
577
+ filter: alpha(opacity=25);
578
+ /*IE 5-7*/
579
+ opacity: 0.25;
580
+ }
581
+
582
+ /* These classes could be used to apply different styles to items depending on their flags */
583
+ /************************************
584
+ Toolbars
585
+ *************************************/
586
+ .ws_toolbar {
587
+ display: block;
588
+ -webkit-box-sizing: border-box;
589
+ -moz-box-sizing: border-box;
590
+ box-sizing: border-box;
591
+ width: 100%;
592
+ padding: 6px 6px 0 6px;
593
+ }
594
+
595
+ .ws_button {
596
+ display: block;
597
+ margin-right: 3px;
598
+ margin-bottom: 4px;
599
+ padding: 4px;
600
+ float: left;
601
+ -webkit-box-sizing: content-box;
602
+ -moz-box-sizing: content-box;
603
+ box-sizing: content-box;
604
+ width: 16px;
605
+ height: 16px;
606
+ border-radius: 3px;
607
+ -moz-border-radius: 3px;
608
+ -webkit-border-radius: 3px;
609
+ }
610
+ .ws_button img {
611
+ vertical-align: top;
612
+ }
613
+
614
+ a.ws_button:hover {
615
+ background-color: #d0e0ff;
616
+ border-color: #9090c0;
617
+ }
618
+
619
+ .ws_button.ws_button_disabled {
620
+ border-color: #ccc;
621
+ }
622
+
623
+ a.ws_button.ws_button_disabled:hover {
624
+ background-color: white;
625
+ border: 1px solid #ccc;
626
+ }
627
+
628
+ .ws_button_disabled img {
629
+ filter: grayscale(1);
630
+ -webkit-filter: grayscale(1);
631
+ opacity: 0.65;
632
+ }
633
+
634
+ .ws_separator {
635
+ float: left;
636
+ width: 5px;
637
+ }
638
+
639
+ #ws_toggle_toolbar, .ws_toggle_toolbar_button {
640
+ margin-right: 0;
641
+ }
642
+
643
+ /************************************
644
+ Capability selector
645
+ *************************************/
646
+ select.ws_dropdown {
647
+ width: 252px;
648
+ height: 20em;
649
+ z-index: 1002;
650
+ position: absolute;
651
+ display: none;
652
+ font-family: "Lucida Grande", Verdana, Arial, "Bitstream Vera Sans", sans-serif;
653
+ font-size: 12px;
654
+ }
655
+
656
+ select.ws_dropdown option {
657
+ font-family: "Lucida Grande", Verdana, Arial, "Bitstream Vera Sans", sans-serif;
658
+ font-size: 12px;
659
+ padding: 3px;
660
+ }
661
+
662
+ select.ws_dropdown optgroup option {
663
+ padding-left: 10px;
664
+ }
665
+
666
+ /************************************
667
+ Tabs (small)
668
+ ************************************
669
+ Tabbed navigation for dropdowns and small dialogs.
670
+ */
671
+ .ws_tool_tab_nav {
672
+ list-style: outside none none;
673
+ padding: 0;
674
+ margin: 0 0 0 6px;
675
+ }
676
+ .ws_tool_tab_nav li {
677
+ display: inline-block;
678
+ border: 1px solid transparent;
679
+ border-bottom-width: 0;
680
+ padding: 3px 5px 5px;
681
+ line-height: 1.35em;
682
+ margin-bottom: 0;
683
+ }
684
+ .ws_tool_tab_nav li.ui-tabs-active {
685
+ border-color: #dfdfdf;
686
+ border-bottom-color: #FDFDFD;
687
+ background: #FDFDFD none;
688
+ }
689
+ .ws_tool_tab_nav a {
690
+ text-decoration: none;
691
+ }
692
+ .ws_tool_tab_nav li.ui-tabs-active a {
693
+ color: #32373C;
694
+ }
695
+
696
+ .ws_tool_tab {
697
+ border-top: 1px solid #DFDFDF;
698
+ margin-top: -1px;
699
+ background-color: #FDFDFD;
700
+ }
701
+
702
+ /************************************
703
+ Icon selector
704
+ *************************************/
705
+ #ws_icon_selector {
706
+ border: 1px solid silver;
707
+ border-radius: 3px;
708
+ background-color: white;
709
+ width: 216px;
710
+ padding: 4px 0 0 0;
711
+ position: absolute;
712
+ }
713
+
714
+ #ws_icon_selector.ws_with_more_icons {
715
+ width: 570px;
716
+ }
717
+
718
+ #ws_icon_selector .ws_icon_extra {
719
+ display: none;
720
+ }
721
+
722
+ #ws_icon_selector.ws_with_more_icons .ws_icon_extra {
723
+ display: inline-block;
724
+ }
725
+
726
+ #ws_icon_selector .ws_icon_option {
727
+ float: left;
728
+ height: 30px;
729
+ margin: 2px;
730
+ cursor: pointer;
731
+ border: 1px solid #bbb;
732
+ border-radius: 3px;
733
+ /* Gradients and colours cribbed from WP 3.5.1 button styles */
734
+ background: #f3f3f3;
735
+ background-image: -webkit-gradient(linear, left top, left bottom, from(#fefefe), to(#f4f4f4));
736
+ background-image: -webkit-linear-gradient(top, #fefefe, #f4f4f4);
737
+ background-image: -moz-linear-gradient(top, #fefefe, #f4f4f4);
738
+ background-image: -o-linear-gradient(top, #fefefe, #f4f4f4);
739
+ background-image: linear-gradient(to bottom, #fefefe, #f4f4f4);
740
+ }
741
+
742
+ #ws_icon_selector .ws_icon_option:hover {
743
+ /* Gradients and colours cribbed from WP 3.5.1 button styles */
744
+ border-color: #999;
745
+ background: #f3f3f3;
746
+ background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f3f3f3));
747
+ background-image: -webkit-linear-gradient(top, #fff, #f3f3f3);
748
+ background-image: -moz-linear-gradient(top, #fff, #f3f3f3);
749
+ background-image: -ms-linear-gradient(top, #fff, #f3f3f3);
750
+ background-image: -o-linear-gradient(top, #fff, #f3f3f3);
751
+ background-image: linear-gradient(to bottom, #fff, #f3f3f3);
752
+ }
753
+
754
+ #ws_icon_selector .ws_icon_option.ws_selected_icon {
755
+ border-color: green;
756
+ background-color: #deffca;
757
+ background-image: none;
758
+ }
759
+
760
+ #ws_icon_selector .icon16 {
761
+ float: none;
762
+ margin: 0;
763
+ }
764
+
765
+ #ws_icon_selector .ws_icon_option .ws_icon_image.dashicons {
766
+ width: 20px;
767
+ height: 20px;
768
+ padding: 5px;
769
+ }
770
+
771
+ #ws_icon_selector .ws_icon_option img {
772
+ display: inline-block;
773
+ margin: 0;
774
+ padding: 7px;
775
+ width: 16px;
776
+ height: 16px;
777
+ }
778
+
779
+ #ws_menu_editor .ws_edit_field-icon_url input.ws_field_value {
780
+ width: 220px;
781
+ margin-right: 5px;
782
+ }
783
+
784
+ /* The icon button that displays the pop-up icon selector. */
785
+ #ws_menu_editor .ws_select_icon {
786
+ margin: 0;
787
+ padding: 0;
788
+ position: relative;
789
+ box-sizing: border-box;
790
+ height: 25px;
791
+ min-height: 25px;
792
+ }
793
+ .ame-is-wp53-plus #ws_menu_editor .ws_select_icon {
794
+ height: 28px;
795
+ min-height: 28px;
796
+ margin-top: 1px;
797
+ }
798
+
799
+ /* Current icon node (CSS class version, for the built-in WP icon sprites) */
800
+ .ws_select_icon .icon16 {
801
+ margin: 0;
802
+ float: none;
803
+ padding: 3px;
804
+ /*
805
+ The default .icon16 style has a 6px padding which would normally make it too large
806
+ to fit in the button. We can't change the padding without making the background-position
807
+ look wrong, so lets offset the icon so that it fits.
808
+ */
809
+ position: relative;
810
+ top: -3px;
811
+ left: -3px;
812
+ }
813
+ .ame-is-wp53-plus .ws_select_icon .icon16 {
814
+ top: -1px;
815
+ }
816
+
817
+ /* Current icon node (image version) */
818
+ .ws_select_icon img {
819
+ margin: 0;
820
+ padding: 4px;
821
+ width: 16px;
822
+ height: 16px;
823
+ }
824
+
825
+ #ws_icon_selector .ws_tool_tab_nav {
826
+ display: inline-block;
827
+ margin-top: 2px;
828
+ position: relative;
829
+ }
830
+ #ws_icon_selector .ws_tool_tab_nav li {
831
+ padding: 4px 10px 11px;
832
+ }
833
+ #ws_icon_selector .ws_tool_tab {
834
+ padding: 4px 4px 2px;
835
+ max-height: 324px;
836
+ overflow-y: auto;
837
+ }
838
+
839
+ /* MP6 admin style compatibility */
840
+ #ws_icon_selector .ws_icon_option .icon16::before {
841
+ margin: 0;
842
+ padding: 0;
843
+ }
844
+
845
+ .ws_select_icon .icon16::before {
846
+ padding: 0;
847
+ margin: 1px 0 0 2px;
848
+ }
849
+
850
+ #ws_choose_icon_from_media {
851
+ margin: 2px;
852
+ }
853
+
854
+ /************************************
855
+ Embedded page selector
856
+ *************************************/
857
+ #ws_embedded_page_selector {
858
+ width: 254px;
859
+ padding: 6px 0 0 0;
860
+ border: 1px solid silver;
861
+ border-radius: 3px;
862
+ background-color: white;
863
+ box-sizing: border-box;
864
+ position: absolute;
865
+ }
866
+
867
+ .ws_page_selector_tab_nav {
868
+ list-style: outside none none;
869
+ padding: 0;
870
+ margin: 0 0 0 6px;
871
+ }
872
+
873
+ .ws_page_selector_tab_nav li {
874
+ display: inline-block;
875
+ border: 1px solid transparent;
876
+ border-bottom-width: 0;
877
+ padding: 3px 5px 5px;
878
+ line-height: 1.35em;
879
+ margin-bottom: 0;
880
+ }
881
+
882
+ .ws_page_selector_tab_nav a {
883
+ text-decoration: none;
884
+ }
885
+
886
+ .ws_page_selector_tab_nav li.ui-tabs-active {
887
+ border-color: #dfdfdf;
888
+ background-color: #FDFDFD;
889
+ border-bottom-color: #FDFDFD;
890
+ }
891
+
892
+ .ws_page_selector_tab_nav li.ui-tabs-active a {
893
+ color: #32373C;
894
+ }
895
+
896
+ .ws_page_selector_tab {
897
+ border-top: 1px solid #DFDFDF;
898
+ padding: 12px;
899
+ /* The same padding as post editor boxes. */
900
+ margin-top: -1px;
901
+ background-color: #FDFDFD;
902
+ border-bottom-left-radius: 3px;
903
+ border-bottom-right-radius: 3px;
904
+ }
905
+
906
+ #ws_current_site_pages {
907
+ width: 100%;
908
+ min-height: 150px;
909
+ max-height: 300px;
910
+ margin-left: 0;
911
+ margin-right: 0;
912
+ }
913
+
914
+ #ws_embedded_page_selector input {
915
+ box-sizing: border-box;
916
+ max-width: 100%;
917
+ }
918
+
919
+ #ws_custom_embedded_page_tab p:first-child {
920
+ margin-top: 0;
921
+ }
922
+
923
+ /*
924
+ Make the "Page" field look editable. It is read-only because the user can't change it directly (they have to use
925
+ the dropdown), but we don't want it to be greyed-out.
926
+ */
927
+ #ws_menu_editor .ws_edit_field-embedded_page_id input.ws_field_value {
928
+ background-color: white;
929
+ }
930
+
931
+ /************************************
932
+ Menu color picker
933
+ *************************************/
934
+ #ws-ame-menu-color-settings {
935
+ background: white;
936
+ display: none;
937
+ }
938
+
939
+ #ame-menu-color-list {
940
+ height: 500px;
941
+ overflow-y: auto;
942
+ }
943
+
944
+ .ame-menu-color-column {
945
+ min-width: 460px;
946
+ }
947
+
948
+ .ame-menu-color-name {
949
+ display: inline-block;
950
+ vertical-align: top;
951
+ padding-top: 2px;
952
+ line-height: 1.3;
953
+ font-size: 14px;
954
+ font-weight: 600;
955
+ min-width: 180px;
956
+ }
957
+
958
+ .ame-color-option {
959
+ padding: 10px 0;
960
+ }
961
+ .ame-color-option .wp-picker-container {
962
+ display: inline-block;
963
+ }
964
+
965
+ .ame-advanced-menu-color {
966
+ display: none;
967
+ }
968
+
969
+ #ws-ame-apply-colors-to-all {
970
+ display: block;
971
+ float: left;
972
+ margin-left: 5px;
973
+ }
974
+
975
+ /* Color presets */
976
+ #ame-color-preset-container {
977
+ padding: 0 8px 8px 8px;
978
+ margin-left: -8px;
979
+ margin-right: -8px;
980
+ margin-bottom: 4px;
981
+ border-bottom: 1px solid #eee;
982
+ }
983
+
984
+ #ame-menu-color-presets {
985
+ width: 290px;
986
+ margin-right: 5px;
987
+ }
988
+
989
+ #ws-ame-save-color-preset {
990
+ /*margin-right: 5px;*/
991
+ }
992
+
993
+ a#ws-ame-delete-color-preset {
994
+ color: #A00;
995
+ text-decoration: none;
996
+ }
997
+
998
+ a#ws-ame-delete-color-preset:hover {
999
+ color: #F00;
1000
+ }
1001
+
1002
+ /* Color scheme display in the editor widget. */
1003
+ .ws_color_scheme_display {
1004
+ display: inline-block;
1005
+ box-sizing: border-box;
1006
+ height: 26px;
1007
+ width: 190px;
1008
+ margin-right: 5px;
1009
+ margin-left: 1px;
1010
+ padding: 2px 4px;
1011
+ font-size: 12px;
1012
+ border: 1px solid #ddd;
1013
+ background: white;
1014
+ cursor: pointer;
1015
+ line-height: 20px;
1016
+ }
1017
+ .ame-is-wp53-plus .ws_color_scheme_display {
1018
+ border-color: #7e8993;
1019
+ border-radius: 4px;
1020
+ margin-top: 1px;
1021
+ margin-bottom: 1px;
1022
+ padding: 3px 8px;
1023
+ height: 28px;
1024
+ line-height: 20px;
1025
+ }
1026
+
1027
+ .ws_open_color_editor {
1028
+ width: 58px;
1029
+ }
1030
+
1031
+ .ws_color_display_item {
1032
+ display: inline-block;
1033
+ width: 18px;
1034
+ height: 18px;
1035
+ margin-right: 4px;
1036
+ border: 1px solid #ccc;
1037
+ border-radius: 3px;
1038
+ }
1039
+
1040
+ .ws_color_display_item:last-child {
1041
+ margin-right: 0;
1042
+ }
1043
+
1044
+ /************************************
1045
+ Export and import
1046
+ *************************************/
1047
+ #export_dialog, #import_dialog {
1048
+ display: none;
1049
+ }
1050
+
1051
+ .ui-widget-overlay {
1052
+ background-color: black;
1053
+ position: fixed;
1054
+ left: 0;
1055
+ top: 0;
1056
+ right: 0;
1057
+ bottom: 0;
1058
+ opacity: 0.7;
1059
+ -moz-opacity: 0.7;
1060
+ filter: alpha(opacity=70);
1061
+ width: 100%;
1062
+ height: 100%;
1063
+ }
1064
+
1065
+ .ui-front {
1066
+ z-index: 10000;
1067
+ }
1068
+
1069
+ .settings_page_menu_editor .ui-dialog {
1070
+ background: white;
1071
+ border: 1px solid #c0c0c0;
1072
+ padding: 0;
1073
+ -moz-border-radius: 5px;
1074
+ -webkit-border-radius: 5px;
1075
+ border-radius: 5px;
1076
+ }
1077
+ .settings_page_menu_editor .ui-dialog .ui-dialog-content {
1078
+ padding: 8px 8px 8px 8px;
1079
+ font-size: 1.1em;
1080
+ }
1081
+ .settings_page_menu_editor .ui-dialog .ame-scrollable-dialog-content {
1082
+ max-height: 500px;
1083
+ overflow-y: auto;
1084
+ padding-top: 0.5em;
1085
+ }
1086
+ .settings_page_menu_editor .ui-dialog-titlebar {
1087
+ display: block;
1088
+ height: 22px;
1089
+ margin: 0;
1090
+ padding: 4px 4px 4px 8px;
1091
+ background-color: #86A7E3;
1092
+ font-size: 1em;
1093
+ line-height: 22px;
1094
+ -webkit-border-top-left-radius: 4px;
1095
+ -webkit-border-top-right-radius: 4px;
1096
+ -moz-border-radius-topleft: 4px;
1097
+ -moz-border-radius-topright: 4px;
1098
+ border-top-left-radius: 4px;
1099
+ border-top-right-radius: 4px;
1100
+ border-bottom: 1px solid #809fd9;
1101
+ }
1102
+ .settings_page_menu_editor .ui-dialog-title {
1103
+ color: white;
1104
+ font-weight: bold;
1105
+ }
1106
+ .settings_page_menu_editor .ui-button.ui-dialog-titlebar-close {
1107
+ background: #86A7E3 url(../images/x.png) no-repeat center;
1108
+ width: 22px;
1109
+ height: 22px;
1110
+ display: block;
1111
+ float: right;
1112
+ color: white;
1113
+ border-radius: 3px;
1114
+ -moz-border-radius: 3px;
1115
+ -webkit-border-radius: 3px;
1116
+ }
1117
+ .settings_page_menu_editor .ui-dialog-titlebar-close:hover {
1118
+ /*background-image: url(../images/x-light.png);*/
1119
+ background-color: #a6c2f5;
1120
+ }
1121
+ #export_dialog .ws_dialog_panel {
1122
+ height: 50px;
1123
+ }
1124
+
1125
+ #import_dialog .ws_dialog_panel {
1126
+ height: 64px;
1127
+ }
1128
+
1129
+ .ws_dialog_panel .ame-fixed-label-text {
1130
+ display: inline-block;
1131
+ min-width: 6em;
1132
+ }
1133
+ .ws_dialog_panel .ame-inline-select-with-input {
1134
+ vertical-align: baseline;
1135
+ }
1136
+ .ws_dialog_panel .ame-box-side-sizes {
1137
+ display: flex;
1138
+ flex-wrap: wrap;
1139
+ max-width: 800px;
1140
+ }
1141
+ .ws_dialog_panel .ame-box-side-sizes .ame-fixed-label-text {
1142
+ min-width: 4em;
1143
+ }
1144
+ .ws_dialog_panel .ame-box-side-sizes label {
1145
+ margin-right: 2.5em;
1146
+ }
1147
+ .ws_dialog_panel .ame-box-side-sizes input {
1148
+ margin-bottom: 0.4em;
1149
+ }
1150
+ .ws_dialog_panel .ame-box-side-sizes input[type=number] {
1151
+ width: 6em;
1152
+ }
1153
+
1154
+ .ame-flexbox-break {
1155
+ flex-basis: 100%;
1156
+ height: 0;
1157
+ }
1158
+
1159
+ .ws_dialog_buttons {
1160
+ /*height: 30px;*/
1161
+ text-align: right;
1162
+ margin-top: 20px;
1163
+ margin-bottom: 1px;
1164
+ clear: both;
1165
+ }
1166
+
1167
+ .ws_dialog_buttons .button-primary {
1168
+ display: block;
1169
+ float: left;
1170
+ margin-top: 0;
1171
+ }
1172
+
1173
+ .ws_dialog_buttons .button {
1174
+ margin-top: 0;
1175
+ }
1176
+
1177
+ .ws_dialog_buttons.ame-vertical-button-list {
1178
+ text-align: left;
1179
+ }
1180
+
1181
+ .ws_dialog_buttons.ame-vertical-button-list .button-primary {
1182
+ float: none;
1183
+ }
1184
+
1185
+ .ws_dialog_buttons.ame-vertical-button-list .button {
1186
+ width: 100%;
1187
+ text-align: left;
1188
+ margin-bottom: 10px;
1189
+ }
1190
+
1191
+ .ws_dialog_buttons.ame-vertical-button-list .button:last-child {
1192
+ margin-bottom: 0;
1193
+ }
1194
+
1195
+ #import_file_selector {
1196
+ display: block;
1197
+ width: 286px;
1198
+ margin: 6px auto 12px;
1199
+ }
1200
+
1201
+ #ws_start_import {
1202
+ min-width: 100px;
1203
+ }
1204
+
1205
+ #import_complete_notice {
1206
+ text-align: center;
1207
+ font-size: large;
1208
+ padding-top: 25px;
1209
+ }
1210
+
1211
+ #ws_import_error_response {
1212
+ width: 100%;
1213
+ }
1214
+
1215
+ .ws_dont_show_again {
1216
+ display: inline-block;
1217
+ margin-top: 1em;
1218
+ }
1219
+
1220
+ /************************************
1221
+ Menu access editor
1222
+ *************************************/
1223
+ /* The launch button */
1224
+ #ws_menu_editor .ws_edit_field-access_level input.ws_field_value {
1225
+ width: 190px;
1226
+ margin-right: 5px;
1227
+ }
1228
+
1229
+ .ws_launch_access_editor {
1230
+ min-width: 40px;
1231
+ width: 58px;
1232
+ }
1233
+
1234
+ #ws_menu_access_editor {
1235
+ width: 400px;
1236
+ display: none;
1237
+ }
1238
+
1239
+ .ws_dialog_subpanel {
1240
+ margin-bottom: 1em;
1241
+ }
1242
+ .ws_dialog_subpanel fieldset p {
1243
+ margin-top: 0;
1244
+ margin-bottom: 4px;
1245
+ }
1246
+
1247
+ .ws-ame-dialog-subheading {
1248
+ display: block;
1249
+ font-weight: 600;
1250
+ font-size: 1em;
1251
+ margin: 0 0 0.2em 0;
1252
+ }
1253
+
1254
+ #ws_menu_access_editor .ws_column_access,
1255
+ #ws_menu_access_editor .ws_ext_action_check_column {
1256
+ text-align: center;
1257
+ width: 1em;
1258
+ padding-right: 0;
1259
+ }
1260
+
1261
+ #ws_menu_access_editor .ws_column_access input,
1262
+ #ws_menu_access_editor .ws_ext_action_check_column input {
1263
+ margin-right: 0;
1264
+ }
1265
+
1266
+ #ws_menu_access_editor .ws_column_role {
1267
+ white-space: nowrap;
1268
+ }
1269
+
1270
+ #ws_role_table_body_container {
1271
+ /*max-height: 400px;
1272
+ overflow: auto;*/
1273
+ overflow: hidden;
1274
+ margin-right: -1px;
1275
+ }
1276
+
1277
+ .ws_role_table_body {
1278
+ margin-top: 2px;
1279
+ max-width: 354px;
1280
+ }
1281
+
1282
+ .ws_has_separate_header .ws_role_table_header {
1283
+ border-bottom: none;
1284
+ -moz-border-radius-bottomleft: 0;
1285
+ -moz-border-radius-bottomright: 0;
1286
+ -webkit-border-bottom-left-radius: 0;
1287
+ -webkit-border-bottom-right-radius: 0;
1288
+ border-bottom-left-radius: 0;
1289
+ border-bottom-right-radius: 0;
1290
+ }
1291
+
1292
+ .ws_has_separate_header .ws_role_table_body {
1293
+ border-top: none;
1294
+ margin-top: 0;
1295
+ -moz-border-radius-topleft: 0;
1296
+ -moz-border-radius-topright: 0;
1297
+ -webkit-border-top-left-radius: 0;
1298
+ -webkit-border-top-right-radius: 0;
1299
+ border-top-left-radius: 0;
1300
+ border-top-right-radius: 0;
1301
+ }
1302
+
1303
+ .ws_role_id {
1304
+ display: none;
1305
+ }
1306
+
1307
+ #ws_extra_capability {
1308
+ width: 100%;
1309
+ }
1310
+
1311
+ #ws_role_access_container {
1312
+ position: relative;
1313
+ max-height: 430px;
1314
+ overflow: auto;
1315
+ }
1316
+
1317
+ #ws_role_access_overlay {
1318
+ width: 100%;
1319
+ height: 100%;
1320
+ position: absolute;
1321
+ line-height: 100%;
1322
+ background: white;
1323
+ filter: alpha(opacity=60);
1324
+ opacity: 0.6;
1325
+ -moz-opacity: 0.6;
1326
+ }
1327
+
1328
+ #ws_role_access_overlay_content {
1329
+ position: absolute;
1330
+ width: 50%;
1331
+ left: 22%;
1332
+ top: 30%;
1333
+ background: white;
1334
+ padding: 8px;
1335
+ border: 2px solid silver;
1336
+ border-radius: 5px;
1337
+ color: #555;
1338
+ }
1339
+
1340
+ #ws_menu_access_editor div.error {
1341
+ margin-left: 0;
1342
+ margin-right: 0;
1343
+ margin-bottom: 5px;
1344
+ }
1345
+
1346
+ #ws_hardcoded_role_error {
1347
+ display: none;
1348
+ }
1349
+
1350
+ /*--------------------------------------------*
1351
+ The CPT/taxonomy permissions panel
1352
+ *--------------------------------------------*/
1353
+ /*
1354
+ * When there are CPT/taxonomy permissions available, the appearance of the role list changes a bit.
1355
+ */
1356
+ .ws_has_extended_permissions {
1357
+ /* The role or actor whose CPT/taxonomy permissions are currently expanded. */
1358
+ }
1359
+ .ws_has_extended_permissions .ws_role_table_body .ws_column_role {
1360
+ cursor: pointer;
1361
+ }
1362
+ .ws_has_extended_permissions .ws_role_table_body .ws_column_selected_role_tip {
1363
+ display: table-cell;
1364
+ }
1365
+ .ws_has_extended_permissions .ws_role_table_body tr:hover {
1366
+ background: #EAF2FA;
1367
+ }
1368
+ .ws_has_extended_permissions .ws_role_table_body td {
1369
+ border-top: 1px solid #f1f1f1;
1370
+ }
1371
+ .ws_has_extended_permissions .ws_role_table_body tr:first-child td {
1372
+ border-top-width: 0;
1373
+ }
1374
+ .ws_has_extended_permissions .ws_role_table_body tr.ws_cpt_selected_role {
1375
+ background-color: #dddddd;
1376
+ }
1377
+ .ws_has_extended_permissions .ws_role_table_body tr.ws_cpt_selected_role .ws_column_role {
1378
+ font-weight: bold;
1379
+ }
1380
+ .ws_has_extended_permissions .ws_role_table_body tr.ws_cpt_selected_role .ws_cpt_selected_role_tip {
1381
+ visibility: visible;
1382
+ }
1383
+ .ws_has_extended_permissions .ws_role_table_body tr.ws_cpt_selected_role td {
1384
+ color: #222;
1385
+ }
1386
+
1387
+ #ws_ext_permissions_container {
1388
+ float: left;
1389
+ width: 352px;
1390
+ padding: 0 9px 0 0;
1391
+ }
1392
+
1393
+ #ws_ext_permissions_container_caption {
1394
+ padding-left: 15px;
1395
+ max-width: 352px;
1396
+ position: relative;
1397
+ white-space: nowrap;
1398
+ }
1399
+
1400
+ #ws_ext_permissions_container .ws_ext_permissions_table {
1401
+ margin-top: 2px;
1402
+ }
1403
+ #ws_ext_permissions_container .ws_ext_permissions_table tr td:first-child {
1404
+ padding-left: 15px;
1405
+ }
1406
+ #ws_ext_permissions_container .ws_ext_permissions_table .ws_ext_group_title {
1407
+ padding-bottom: 0;
1408
+ font-weight: bold;
1409
+ }
1410
+ #ws_ext_permissions_container .ws_ext_permissions_table .ws_ext_action_check_column,
1411
+ #ws_ext_permissions_container .ws_ext_permissions_table .ws_ext_action_name_column {
1412
+ padding-top: 3px;
1413
+ padding-bottom: 3px;
1414
+ }
1415
+ #ws_ext_permissions_container .ws_ext_permissions_table tr.ws_ext_padding_row td {
1416
+ padding: 0 0 0 0;
1417
+ height: 1px;
1418
+ }
1419
+ #ws_ext_permissions_container .ws_ext_permissions_table .ws_same_as_required_cap {
1420
+ text-decoration: underline;
1421
+ }
1422
+ #ws_ext_permissions_container .ws_ext_permissions_table .ws_ext_has_custom_setting label.ws_ext_action_name::after {
1423
+ content: " *";
1424
+ }
1425
+
1426
+ #ws_ext_permissions_container #ws_ext_toggle_capability_names {
1427
+ cursor: pointer;
1428
+ position: absolute;
1429
+ right: 0;
1430
+ color: #0073aa;
1431
+ }
1432
+ #ws_ext_permissions_container.ws_ext_readable_names_enabled #ws_ext_toggle_capability_names {
1433
+ color: #b4b9be;
1434
+ }
1435
+ #ws_ext_permissions_container .ws_ext_readable_name {
1436
+ display: none;
1437
+ }
1438
+ #ws_ext_permissions_container .ws_ext_capability {
1439
+ display: inline;
1440
+ }
1441
+ #ws_ext_permissions_container.ws_ext_readable_names_enabled .ws_ext_readable_name {
1442
+ display: inline;
1443
+ }
1444
+ #ws_ext_permissions_container.ws_ext_readable_names_enabled .ws_ext_capability {
1445
+ display: none;
1446
+ }
1447
+
1448
+ #ws_ext_permissions_container #ws_taxonomy_permissions_table tr:first-child td {
1449
+ padding-top: 8px;
1450
+ }
1451
+
1452
+ /* The "selected role" indicator. */
1453
+ .ws_cpt_selected_role_tip {
1454
+ display: block;
1455
+ visibility: hidden;
1456
+ box-sizing: border-box;
1457
+ width: 26px;
1458
+ height: 26px;
1459
+ position: absolute;
1460
+ right: 0;
1461
+ background: white;
1462
+ transform: translate(1px, 0) rotate(-45deg);
1463
+ transform-origin: top right;
1464
+ }
1465
+
1466
+ .ws_role_table_body .ws_column_selected_role_tip {
1467
+ display: none;
1468
+ padding: 0;
1469
+ width: 40px;
1470
+ height: 100%;
1471
+ text-align: right;
1472
+ overflow: visible;
1473
+ position: relative;
1474
+ cursor: pointer;
1475
+ }
1476
+
1477
+ .ws_ame_breadcrumb_separator {
1478
+ color: #999;
1479
+ }
1480
+
1481
+ #ws_menu_editor .ws_ext_permissions_indicator {
1482
+ font-size: 16px;
1483
+ height: 16px;
1484
+ width: 16px;
1485
+ visibility: hidden;
1486
+ vertical-align: bottom;
1487
+ cursor: pointer;
1488
+ color: #4aa100;
1489
+ }
1490
+
1491
+ #ws_menu_editor.ws_is_actor_view .ws_ext_permissions_indicator {
1492
+ visibility: visible;
1493
+ }
1494
+
1495
+ /************************************
1496
+ Visible users dialog
1497
+ *************************************/
1498
+ #ws_visible_users_dialog {
1499
+ background: white;
1500
+ padding: 8px;
1501
+ }
1502
+
1503
+ #ws_user_selection_panels {
1504
+ min-width: 710px;
1505
+ }
1506
+ #ws_user_selection_panels .ws_user_selection_panel {
1507
+ display: block;
1508
+ float: left;
1509
+ position: relative;
1510
+ -webkit-box-sizing: border-box;
1511
+ -moz-box-sizing: border-box;
1512
+ box-sizing: border-box;
1513
+ width: 350px;
1514
+ height: 400px;
1515
+ border: 1px solid #e5e5e5;
1516
+ margin-right: 10px;
1517
+ padding: 10px;
1518
+ }
1519
+ #ws_user_selection_panels #ws_user_selection_target_panel {
1520
+ margin-right: 0;
1521
+ }
1522
+ #ws_user_selection_panels #ws_available_user_query {
1523
+ -webkit-box-sizing: border-box;
1524
+ -moz-box-sizing: border-box;
1525
+ box-sizing: border-box;
1526
+ width: 100%;
1527
+ max-height: 28px;
1528
+ }
1529
+ #ws_user_selection_panels .ws_user_list_wrapper {
1530
+ position: absolute;
1531
+ top: 50px;
1532
+ left: 10px;
1533
+ right: 10px;
1534
+ height: 338px;
1535
+ overflow-x: auto;
1536
+ overflow-y: auto;
1537
+ }
1538
+ #ws_user_selection_panels .ws_user_selection_list {
1539
+ min-height: 20px;
1540
+ border-width: 0;
1541
+ -webkit-box-shadow: none;
1542
+ -moz-box-shadow: none;
1543
+ box-shadow: none;
1544
+ }
1545
+ #ws_user_selection_panels .ws_user_selection_list .ws_user_action_column {
1546
+ width: 20px;
1547
+ text-align: center;
1548
+ padding-top: 9px;
1549
+ padding-bottom: 0;
1550
+ }
1551
+ #ws_user_selection_panels .ws_user_selection_list .ws_user_action_button {
1552
+ cursor: pointer;
1553
+ color: #b4b9be;
1554
+ }
1555
+ #ws_user_selection_panels .ws_user_selection_list .ws_user_username_column {
1556
+ padding-left: 0;
1557
+ }
1558
+ #ws_user_selection_panels .ws_user_selection_list .ws_user_display_name_column {
1559
+ white-space: nowrap;
1560
+ }
1561
+ #ws_user_selection_panels #ws_available_users tr {
1562
+ cursor: pointer;
1563
+ }
1564
+ #ws_user_selection_panels #ws_available_users tr:hover, #ws_user_selection_panels #ws_available_users tr.ws_user_best_match {
1565
+ background-color: #eaf2fa;
1566
+ }
1567
+ #ws_user_selection_panels #ws_available_users tr:hover .ws_user_action_button {
1568
+ color: #7ad03a;
1569
+ }
1570
+ #ws_user_selection_panels #ws_selected_users .ws_user_action_button::before {
1571
+ content: "";
1572
+ }
1573
+ #ws_user_selection_panels #ws_selected_users .ws_user_action_button:hover {
1574
+ color: #dd3d36;
1575
+ }
1576
+ #ws_user_selection_panels #ws_selected_users .ws_user_action_column {
1577
+ padding-left: 6px;
1578
+ }
1579
+ #ws_user_selection_panels #ws_selected_users .ws_user_display_name_column {
1580
+ display: none;
1581
+ }
1582
+ #ws_user_selection_panels #ws_selected_users tr.ws_user_must_be_selected .ws_user_action_button {
1583
+ display: none;
1584
+ }
1585
+ #ws_user_selection_panels #ws_selected_users_caption {
1586
+ font-size: 14px;
1587
+ line-height: 1.4em;
1588
+ padding: 7px 10px;
1589
+ color: #555;
1590
+ font-weight: 600;
1591
+ }
1592
+ #ws_user_selection_panels::after {
1593
+ display: block;
1594
+ height: 1px;
1595
+ visibility: hidden;
1596
+ content: " ";
1597
+ clear: both;
1598
+ }
1599
+
1600
+ #ws_loading_users_indicator {
1601
+ position: absolute;
1602
+ right: 10px;
1603
+ bottom: 10px;
1604
+ margin-right: 0;
1605
+ margin-bottom: 0;
1606
+ }
1607
+
1608
+ /************************************
1609
+ Menu deletion error
1610
+ *************************************/
1611
+ #ws-ame-menu-deletion-error {
1612
+ max-width: 400px;
1613
+ }
1614
+
1615
+ /************************************
1616
+ Tooltips and hints
1617
+ *************************************/
1618
+ .ws_tooltip_trigger, .ws_field_tooltip_trigger {
1619
+ cursor: pointer;
1620
+ }
1621
+
1622
+ .ws_tooltip_content_list {
1623
+ list-style: disc;
1624
+ margin-left: 1em;
1625
+ margin-bottom: 0;
1626
+ }
1627
+
1628
+ .ws_tooltip_node {
1629
+ font-size: 13px;
1630
+ line-height: 1.3;
1631
+ border-radius: 3px;
1632
+ max-width: 300px;
1633
+ }
1634
+
1635
+ .ws_field_tooltip_trigger .dashicons {
1636
+ font-size: 16px;
1637
+ height: 16px;
1638
+ vertical-align: bottom;
1639
+ }
1640
+
1641
+ .ws_field_tooltip_trigger {
1642
+ color: #a1a1a1;
1643
+ }
1644
+
1645
+ #ws_plugin_settings_form .ws_tooltip_trigger .dashicons {
1646
+ font-size: 18px;
1647
+ }
1648
+
1649
+ .ws_ame_custom_postbox .ws_tooltip_trigger .dashicons {
1650
+ font-size: 18px;
1651
+ height: 18px;
1652
+ vertical-align: bottom;
1653
+ }
1654
+
1655
+ .ws_tooltip_trigger.ame-warning-tooltip {
1656
+ color: orange;
1657
+ }
1658
+
1659
+ .ws_wide_tooltip {
1660
+ max-width: 450px;
1661
+ }
1662
+
1663
+ .ws_hint {
1664
+ background: #FFFFE0;
1665
+ border: 1px solid #E6DB55;
1666
+ margin-bottom: 0.5em;
1667
+ border-radius: 3px;
1668
+ position: relative;
1669
+ padding-right: 20px;
1670
+ }
1671
+
1672
+ .ws_hint_close {
1673
+ border: 1px solid #E6DB55;
1674
+ border-right: none;
1675
+ border-top: none;
1676
+ color: #dcc500;
1677
+ font-weight: bold;
1678
+ cursor: pointer;
1679
+ width: 18px;
1680
+ text-align: center;
1681
+ border-radius: 3px;
1682
+ position: absolute;
1683
+ right: 0;
1684
+ top: 0;
1685
+ }
1686
+
1687
+ .ws_hint_close:hover {
1688
+ background-color: #ffef4c;
1689
+ border-color: #e0b900;
1690
+ color: black;
1691
+ }
1692
+
1693
+ .ws_hint_content {
1694
+ padding: 0.4em 0 0.4em 0.4em;
1695
+ }
1696
+
1697
+ .ws_hint_content ul {
1698
+ list-style: disc;
1699
+ list-style-position: inside;
1700
+ margin-left: 0.5em;
1701
+ }
1702
+
1703
+ .ws_ame_doc_box .hndle, .ws_ame_custom_postbox .hndle {
1704
+ cursor: default !important;
1705
+ border-bottom: 1px solid #ccd0d4;
1706
+ }
1707
+ .ws_ame_doc_box .handlediv, .ws_ame_custom_postbox .handlediv {
1708
+ display: block;
1709
+ float: right;
1710
+ }
1711
+ .ws_ame_doc_box .inside, .ws_ame_custom_postbox .inside {
1712
+ margin-bottom: 0;
1713
+ }
1714
+ .ws_ame_doc_box ul, .ws_ame_custom_postbox ul {
1715
+ list-style: disc outside;
1716
+ margin-left: 1em;
1717
+ }
1718
+ .ws_ame_doc_box li > ul, .ws_ame_custom_postbox li > ul {
1719
+ margin-top: 6px;
1720
+ }
1721
+ .ws_ame_doc_box .button-link .toggle-indicator::before, .ws_ame_custom_postbox .button-link .toggle-indicator::before {
1722
+ margin-top: 4px;
1723
+ width: 20px;
1724
+ -webkit-border-radius: 50%;
1725
+ border-radius: 50%;
1726
+ text-indent: -1px;
1727
+ content: "";
1728
+ display: inline-block;
1729
+ font: normal 20px/1 dashicons;
1730
+ -webkit-font-smoothing: antialiased;
1731
+ -moz-osx-font-smoothing: grayscale;
1732
+ text-decoration: none !important;
1733
+ }
1734
+ .ws_ame_doc_box.closed .button-link .toggle-indicator::before, .ws_ame_custom_postbox.closed .button-link .toggle-indicator::before {
1735
+ content: "";
1736
+ }
1737
+
1738
+ .ws_basic_container .ws_ame_custom_postbox {
1739
+ margin-left: 2px;
1740
+ margin-right: 2px;
1741
+ }
1742
+
1743
+ .ws_ame_custom_postbox .ame-tutorial-list {
1744
+ margin: 0;
1745
+ }
1746
+ .ws_ame_custom_postbox .ame-tutorial-list a {
1747
+ text-decoration: none;
1748
+ display: block;
1749
+ padding: 4px;
1750
+ }
1751
+ .ws_ame_custom_postbox .ame-tutorial-list ul {
1752
+ margin-left: 1em;
1753
+ }
1754
+ .ws_ame_custom_postbox .ame-tutorial-list li {
1755
+ display: block;
1756
+ margin: 0;
1757
+ list-style: none;
1758
+ }
1759
+
1760
+ /************************************
1761
+ Copy Permissions dialog
1762
+ *************************************/
1763
+ #ws-ame-copy-permissions-dialog select {
1764
+ min-width: 280px;
1765
+ }
1766
+
1767
+ /*********************************************
1768
+ Capability suggestions and preview
1769
+ **********************************************/
1770
+ #ws_capability_suggestions {
1771
+ padding: 4px;
1772
+ width: 350px;
1773
+ border: 1px solid #cdd5d5;
1774
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
1775
+ background: #fff;
1776
+ border-top-right-radius: 3px;
1777
+ border-bottom-right-radius: 3px;
1778
+ }
1779
+ #ws_capability_suggestions #ws_previewed_caps {
1780
+ margin-top: 0;
1781
+ margin-bottom: 6px;
1782
+ }
1783
+ #ws_capability_suggestions td, #ws_capability_suggestions th {
1784
+ padding-top: 3px;
1785
+ padding-bottom: 3px;
1786
+ }
1787
+ #ws_capability_suggestions tr.ws_preview_has_access .ws_ame_role_name {
1788
+ background-color: lightgreen;
1789
+ }
1790
+ #ws_capability_suggestions .ws_ame_suggested_capability {
1791
+ cursor: pointer;
1792
+ }
1793
+ #ws_capability_suggestions .ws_ame_suggested_capability:hover {
1794
+ background-color: #d0f2d0;
1795
+ }
1796
+
1797
+ /*********************************************
1798
+ Settings page stuff
1799
+ **********************************************/
1800
+ #ws_plugin_settings_form figure {
1801
+ margin-left: 0;
1802
+ margin-top: 0;
1803
+ margin-bottom: 1em;
1804
+ }
1805
+
1806
+ .ame-available-add-ons tr:first-of-type td {
1807
+ margin-top: 0;
1808
+ padding-top: 0;
1809
+ }
1810
+ .ame-available-add-ons td {
1811
+ padding-top: 10px;
1812
+ padding-bottom: 10px;
1813
+ }
1814
+ .ame-available-add-ons .ame-add-on-heading {
1815
+ padding-left: 0;
1816
+ }
1817
+
1818
+ .ame-add-on-name {
1819
+ font-weight: 600;
1820
+ }
1821
+
1822
+ .ame-add-on-details-link::after {
1823
+ /*content: " \f504";
1824
+ font-family: dashicons, sans-serif;*/
1825
+ }
1826
+
1827
+ /*********************************************
1828
+ WordPress 5.3+ consistent styles
1829
+ **********************************************/
1830
+ .ame-is-wp53-plus .ws_edit_field input[type=button] {
1831
+ margin-top: 1px;
1832
+ }
1833
+
1834
+ /*********************************************
1835
+ CSS border style selector
1836
+ **********************************************/
1837
+ .ame-css-border-styles .ame-fixed-label-text {
1838
+ min-width: 5em;
1839
+ }
1840
+ .ame-css-border-styles .ame-border-sample-container {
1841
+ display: inline-block;
1842
+ vertical-align: top;
1843
+ min-height: 28px;
1844
+ }
1845
+ .ame-css-border-styles .ame-border-sample {
1846
+ display: inline-block;
1847
+ width: 14em;
1848
+ border-top: 0.3em solid #444;
1849
+ }
1850
+
1851
+ /*********************************************
1852
+ Miscellaneous
1853
+ **********************************************/
1854
+ #ws_sidebar_pro_ad {
1855
+ min-width: 225px;
1856
+ margin-top: 5px;
1857
+ margin-left: 3px;
1858
+ position: fixed;
1859
+ right: 20px;
1860
+ bottom: 40px;
1861
+ z-index: 100;
1862
+ }
1863
+
1864
+ .ws-ame-icon-radio-button-group > label {
1865
+ display: inline-block;
1866
+ padding: 8px;
1867
+ border: 1px solid #ccd0d4;
1868
+ border-radius: 2px;
1869
+ margin-right: 0.5em;
1870
+ }
1871
+
1872
+ span.description {
1873
+ color: #666;
1874
+ font-style: italic;
1875
+ }
1876
+
1877
+ .test-wrap {
1878
+ background-color: #444444;
1879
+ padding: 30px;
1880
+ }
1881
+
1882
+ .test-container {
1883
+ width: 400px;
1884
+ height: 200px;
1885
+ background-color: white;
1886
+ border: 1px solid black;
1887
+ border-radius: 10px;
1888
+ overflow: hidden;
1889
+ }
1890
+
1891
+ .test-header {
1892
+ background-color: #67d6ff;
1893
+ padding: 6px;
1894
+ border-top-left-radius: 8px;
1895
+ border-top-right-radius: 8px;
1896
+ }
1897
+
1898
+ .test-content {
1899
+ padding: 8px;
1900
+ }
1901
+
1902
+ /*********************************************
1903
+ "Test access" dialog
1904
+ **********************************************/
1905
+ #ws_ame_test_access_screen {
1906
+ display: none;
1907
+ background: #fcfcfc;
1908
+ }
1909
+
1910
+ #ws_ame_test_inputs {
1911
+ padding-bottom: 16px;
1912
+ }
1913
+
1914
+ .ws_ame_test_input {
1915
+ display: block;
1916
+ float: left;
1917
+ width: 100%;
1918
+ margin: 2px 0;
1919
+ box-sizing: content-box;
1920
+ }
1921
+
1922
+ .ws_ame_test_input_name {
1923
+ display: block;
1924
+ float: left;
1925
+ width: 35%;
1926
+ margin-right: 4%;
1927
+ text-align: right;
1928
+ padding-top: 6px;
1929
+ line-height: 16px;
1930
+ }
1931
+
1932
+ .ws_ame_test_input_value {
1933
+ display: block;
1934
+ float: right;
1935
+ width: 60%;
1936
+ -webkit-box-sizing: border-box;
1937
+ -moz-box-sizing: border-box;
1938
+ box-sizing: border-box;
1939
+ }
1940
+
1941
+ #ws_ame_test_actions {
1942
+ float: left;
1943
+ width: 100%;
1944
+ margin-top: 1em;
1945
+ }
1946
+
1947
+ #ws_ame_test_button_container {
1948
+ width: 35%;
1949
+ margin-right: 4%;
1950
+ float: left;
1951
+ text-align: right;
1952
+ }
1953
+
1954
+ #ws_ame_test_progress {
1955
+ display: none;
1956
+ width: 60%;
1957
+ float: right;
1958
+ }
1959
+ #ws_ame_test_progress .spinner {
1960
+ float: none;
1961
+ vertical-align: bottom;
1962
+ margin-left: 0;
1963
+ margin-right: 4px;
1964
+ }
1965
+
1966
+ #ws_ame_test_access_body {
1967
+ width: 100%;
1968
+ position: relative;
1969
+ border: 1px solid #ddd;
1970
+ -webkit-border-radius: 3px;
1971
+ -moz-border-radius: 3px;
1972
+ border-radius: 3px;
1973
+ }
1974
+
1975
+ #ws_ame_test_frame_container {
1976
+ margin-right: 250px;
1977
+ background: white;
1978
+ min-height: 500px;
1979
+ position: relative;
1980
+ }
1981
+
1982
+ #ws_ame_test_access_frame {
1983
+ -webkit-box-sizing: border-box;
1984
+ -moz-box-sizing: border-box;
1985
+ box-sizing: border-box;
1986
+ width: 100%;
1987
+ height: 100%;
1988
+ min-height: 500px;
1989
+ border: none;
1990
+ margin: 0;
1991
+ padding: 0;
1992
+ }
1993
+
1994
+ #ws_ame_test_access_sidebar {
1995
+ -webkit-box-sizing: border-box;
1996
+ -moz-box-sizing: border-box;
1997
+ box-sizing: border-box;
1998
+ position: absolute;
1999
+ top: 0;
2000
+ right: 0;
2001
+ bottom: 0;
2002
+ width: 250px;
2003
+ padding: 16px 24px;
2004
+ background-color: #f3f3f3;
2005
+ border-left: 1px solid #ddd;
2006
+ }
2007
+ #ws_ame_test_access_sidebar h4:first-of-type {
2008
+ margin-top: 0;
2009
+ }
2010
+
2011
+ #ws_ame_test_frame_placeholder {
2012
+ display: block;
2013
+ padding: 16px 24px;
2014
+ }
2015
+
2016
+ #ws_ame_test_output {
2017
+ display: none;
2018
+ }
2019
+
2020
+ /***************************************
2021
+ Tabs on the settings page
2022
+ ***************************************/
2023
+ .wrap.ws-ame-too-many-tabs .ws-ame-nav-tab-list.nav-tab-wrapper {
2024
+ border-bottom-color: transparent;
2025
+ }
2026
+ .wrap.ws-ame-too-many-tabs .ws-ame-nav-tab-list .nav-tab {
2027
+ border-bottom: 1px solid #c3c4c7;
2028
+ margin-bottom: 10px;
2029
+ margin-top: 0;
2030
+ }
2031
+
2032
+ /* Spacing between the page heading and the tab list.
2033
+
2034
+ Normally, this is handled by .nav-tab styles, but WordPress changes the margins at smaller screen sizes
2035
+ and the tabs end up without a left margin. Let's put that margin on the heading instead and remove it
2036
+ from the first tab. */
2037
+ #ws_ame_editor_heading {
2038
+ margin-right: 0.305em;
2039
+ }
2040
+
2041
+ .ws-ame-nav-tab-list a.nav-tab:first-of-type {
2042
+ margin-left: 0;
2043
+ }
2044
+
2045
+ /* When in "too many tabs" mode, there's too much space between the bottom of the tab list and the rest
2046
+ of the page. I haven't found a good way to change the margins of just the last row, so here's a partial fix. */
2047
+ .ws-ame-too-many-tabs #ws_actor_selector {
2048
+ margin-top: 0;
2049
+ }
2050
+
2051
+ /*# sourceMappingURL=menu-editor.css.map */
css/menu-editor.scss CHANGED
@@ -740,7 +740,7 @@ a.ws_button.ws_button_disabled:hover {
740
  width: 5px;
741
  }
742
 
743
- #ws_toggle_toolbar {
744
  margin-right: 0;
745
  }
746
 
@@ -1968,6 +1968,10 @@ $userSelectionPanelPadding: 10px;
1968
  vertical-align: bottom;
1969
  }
1970
 
 
 
 
 
1971
  .ws_wide_tooltip {
1972
  max-width: 450px;
1973
  }
@@ -2256,4 +2260,6 @@ span.description {
2256
  padding: 8px;
2257
  }
2258
 
2259
- @import "test-access-screen";
 
 
740
  width: 5px;
741
  }
742
 
743
+ #ws_toggle_toolbar, .ws_toggle_toolbar_button {
744
  margin-right: 0;
745
  }
746
 
1968
  vertical-align: bottom;
1969
  }
1970
 
1971
+ .ws_tooltip_trigger.ame-warning-tooltip {
1972
+ color: orange;
1973
+ }
1974
+
1975
  .ws_wide_tooltip {
1976
  max-width: 450px;
1977
  }
2260
  padding: 8px;
2261
  }
2262
 
2263
+ @import "test-access-screen";
2264
+
2265
+ @import "main-tabs";
includes/ame-utils.php CHANGED
@@ -183,4 +183,214 @@ class ameFileLock {
183
  public function __destruct() {
184
  $this->release();
185
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  }
183
  public function __destruct() {
184
  $this->release();
185
  }
186
+ }
187
+
188
+ class ameOrderedMap implements Iterator, Countable {
189
+ /**
190
+ * @var ameLinkedListNode[]
191
+ */
192
+ private $nodesByKey = array();
193
+
194
+ /**
195
+ * @var ameLinkedListNode|null
196
+ */
197
+ private $head = null;
198
+ /**
199
+ * @var ameLinkedListNode|null
200
+ */
201
+ private $tail = null;
202
+ /**
203
+ * @var ameLinkedListNode|null
204
+ */
205
+ private $currentNode = null;
206
+
207
+ /**
208
+ * @param array $items
209
+ * @return $this
210
+ */
211
+ public function addAll($items) {
212
+ foreach ($items as $key => $item) {
213
+ $this->set($key, $item);
214
+ }
215
+ return $this;
216
+ }
217
+
218
+ /**
219
+ * @param string $previousKey
220
+ * @param array $items
221
+ * @return $this
222
+ */
223
+ public function insertAllAfter($previousKey, $items) {
224
+ if ( !isset($this->nodesByKey[$previousKey]) ) {
225
+ return $this->addAll($items);
226
+ }
227
+
228
+ $previousNode = $this->nodesByKey[$previousKey];
229
+ foreach ($items as $key => $value) {
230
+ if ( isset($this->nodesByKey[$key]) ) {
231
+ $node = $this->nodesByKey[$key];
232
+ } else {
233
+ $node = new ameLinkedListNode($value, $key);
234
+ $this->nodesByKey[$key] = $node;
235
+ }
236
+
237
+ $this->insertNodeAfter($previousNode, $node);
238
+ $previousNode = $node;
239
+ }
240
+
241
+ return $this;
242
+ }
243
+
244
+ /**
245
+ * @param string $previousKey
246
+ * @param string $key
247
+ * @param mixed $item
248
+ * @return $this
249
+ */
250
+ public function insertAfter($previousKey, $key, $item) {
251
+ return $this->insertAllAfter($previousKey, array($key => $item));
252
+ }
253
+
254
+ private function insertNodeAfter($previousNode, $newNode) {
255
+ $newNode->previous = $previousNode;
256
+ $newNode->next = $previousNode->next;
257
+ if ( $newNode->next !== null ) {
258
+ $newNode->next->previous = $newNode;
259
+ }
260
+
261
+ $previousNode->next = $newNode;
262
+
263
+ if ( $this->tail === $previousNode ) {
264
+ $this->tail = $newNode;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * @param string $nextKey
270
+ * @param string $key
271
+ * @param $item
272
+ * @return $this
273
+ */
274
+ public function insertBefore($nextKey, $key, $item) {
275
+ if ( !isset($this->nodesByKey[$nextKey]) ) {
276
+ return $this->set($key, $item);
277
+ }
278
+
279
+ $nextNode = $this->nodesByKey[$nextKey];
280
+ $previousNode = $nextNode->previous;
281
+
282
+ if ( isset($this->nodesByKey[$key]) ) {
283
+ $node = $this->nodesByKey[$key];
284
+ } else {
285
+ $node = new ameLinkedListNode($item, $key);
286
+ $this->nodesByKey[$key] = $node;
287
+ }
288
+
289
+ $node->next = $nextNode;
290
+ $node->previous = $previousNode;
291
+
292
+ $nextNode->previous = $node;
293
+ if ( $previousNode !== null ) {
294
+ $previousNode->next = $node;
295
+ }
296
+
297
+ if ( $this->head === $nextNode ) {
298
+ $this->head = $node;
299
+ }
300
+
301
+ return $this;
302
+ }
303
+
304
+ public function set($key, $item) {
305
+ if ( isset($this->nodesByKey[$key]) ) {
306
+ $this->nodesByKey[$key]->value = $item;
307
+ } else {
308
+ $this->append($key, $item);
309
+ }
310
+ return $this;
311
+ }
312
+
313
+ private function append($key, $item) {
314
+ $node = new ameLinkedListNode($item, $key);
315
+ $this->nodesByKey[$key] = $node;
316
+
317
+ if ( $this->tail === null ) {
318
+ $this->head = $node;
319
+ $this->tail = $node;
320
+ } else {
321
+ $this->insertNodeAfter($this->tail, $node);
322
+ $this->tail = $node;
323
+ }
324
+
325
+ return $this;
326
+ }
327
+
328
+ public function current() {
329
+ return $this->currentNode->value;
330
+ }
331
+
332
+ public function next() {
333
+ if ( $this->currentNode !== null ) {
334
+ $this->currentNode = $this->currentNode->next;
335
+ }
336
+ }
337
+
338
+ public function key() {
339
+ return $this->currentNode->key;
340
+ }
341
+
342
+ public function valid() {
343
+ return ($this->currentNode !== null);
344
+ }
345
+
346
+ public function rewind() {
347
+ $this->currentNode = $this->head;
348
+ }
349
+
350
+ public function count() {
351
+ return count($this->nodesByKey);
352
+ }
353
+
354
+ /**
355
+ * Filter the map using a callback function.
356
+ * Returns a new map that contains only the items for which the callback function returns a truthy value.
357
+ *
358
+ * @param callable $predicate
359
+ * @return ameOrderedMap
360
+ */
361
+ public function filter($predicate) {
362
+ $result = new self();
363
+ foreach($this as $key => $value) {
364
+ if ( call_user_func($predicate, $value, $key) ) {
365
+ $result->append($key, $value);
366
+ }
367
+ }
368
+ return $result;
369
+ }
370
+ }
371
+
372
+ class ameLinkedListNode {
373
+ /**
374
+ * @var string
375
+ */
376
+ public $key;
377
+
378
+ /**
379
+ * @var mixed
380
+ */
381
+ public $value;
382
+
383
+ /**
384
+ * @var self|null
385
+ */
386
+ public $next = null;
387
+ /**
388
+ * @var self|null
389
+ */
390
+ public $previous = null;
391
+
392
+ public function __construct($value, $key = '') {
393
+ $this->value = $value;
394
+ $this->key = $key;
395
+ }
396
  }
includes/basic-dependencies.php CHANGED
@@ -13,6 +13,7 @@ require_once $thisDirectory . '/auto-versioning.php';
13
  require_once $thisDirectory . '/../ajax-wrapper/AjaxWrapper.php';
14
  require_once $thisDirectory . '/module.php';
15
  require_once $thisDirectory . '/persistent-module.php';
 
16
 
17
  if ( !class_exists('WPMenuEditor', false) ) {
18
  require_once $thisDirectory . '/menu-editor-core.php';
13
  require_once $thisDirectory . '/../ajax-wrapper/AjaxWrapper.php';
14
  require_once $thisDirectory . '/module.php';
15
  require_once $thisDirectory . '/persistent-module.php';
16
+ require_once $thisDirectory . '/shortcodes.php';
17
 
18
  if ( !class_exists('WPMenuEditor', false) ) {
19
  require_once $thisDirectory . '/menu-editor-core.php';
includes/editor-page.php CHANGED
@@ -29,8 +29,129 @@ foreach($icons as $name => $url) {
29
  }
30
  $icons = apply_filters('admin_menu_editor-toolbar_icons', $icons, $images_url);
31
 
32
- $hide_button_extra_tooltip = 'When "All" is selected, this will hide the menu from everyone except the current user'
33
- . ($is_multisite ? ' and Super Admin' : '') . '.';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  //Output the "Upgrade to Pro" message
36
  if ( !apply_filters('admin_menu_editor_is_pro', false) ){
@@ -66,15 +187,26 @@ if ( $is_pro_version ) {
66
  include $extrasDirectory . '/copy-permissions-dialog.php';
67
  }
68
 
69
- function ame_output_sort_buttons($icons) {
70
- ?>
71
- <a class='ws_button ws_sort_menus_button' data-sort-direction="asc" href='javascript:void(0)' title='Sort ascending'>
72
- <img src='<?php echo $icons['sort-ascending']; ?>' alt="Sort ascending" />
73
- </a>
74
- <a class='ws_button ws_sort_menus_button' data-sort-direction="desc" href='javascript:void(0)' title='Sort descending'>
75
- <img src='<?php echo $icons['sort-descending']; ?>' alt="Sort descending" />
76
- </a>
77
- <?php
 
 
 
 
 
 
 
 
 
 
 
78
  }
79
 
80
  ?>
@@ -93,64 +225,10 @@ function ame_output_sort_buttons($icons) {
93
 
94
  <div class='ws_main_container'>
95
  <div class='ws_toolbar'>
96
- <div class="ws_button_container">
97
- <a id='ws_cut_menu' class='ws_button' href='javascript:void(0)' title='Cut'><img src='<?php echo $icons['cut']; ?>' alt="Cut" /></a>
98
- <a id='ws_copy_menu' class='ws_button' href='javascript:void(0)' title='Copy'><img src='<?php echo $icons['copy']; ?>' alt="Copy" /></a>
99
- <a id='ws_paste_menu' class='ws_button' href='javascript:void(0)' title='Paste'><img src='<?php echo $icons['paste']; ?>' alt="Paste" /></a>
100
-
101
- <div class="ws_separator">&nbsp;</div>
102
-
103
- <a id='ws_new_menu' class='ws_button' href='javascript:void(0)' title='New menu'><img src='<?php echo $icons['new']; ?>' alt="New menu" /></a>
104
- <a id='ws_new_separator' class='ws_button' href='javascript:void(0)' title='New separator'><img src='<?php echo $icons['new-separator']; ?>' alt="New separator" /></a>
105
- <?php if ( $is_pro_version ): ?>
106
- <a id='ws_new_heading' class='ws_button' href='javascript:void(0)' title='New heading'><img src='<?php echo $icons['new-heading']; ?>' alt="New heading" /></a>
107
- <?php endif; ?>
108
-
109
- <?php if ( $is_pro_version ): ?>
110
- <div class="ws_separator">&nbsp;</div>
111
-
112
- <a id='ws_hide_and_deny_menu' class='ws_button ws_hide_and_deny_button' href='javascript:void(0)'
113
- title='Hide and prevent access. &#10;<?php echo esc_attr($hide_button_extra_tooltip); ?>'>
114
- <img src='<?php echo $icons['hide-and-deny']; ?>' alt="Hide" />
115
- </a>
116
- <?php endif; ?>
117
-
118
- <?php if ( $editor_data['show_deprecated_hide_button'] ): ?>
119
- <a id='ws_hide_menu' class='ws_button' href='javascript:void(0)' title='Hide without preventing access (cosmetic)'><img src='<?php echo $icons['hide']; ?>' alt="Hide (cosmetic)" /></a>
120
- <?php endif; ?>
121
-
122
- <a id='ws_delete_menu' class='ws_button ws_delete_menu_button' href='javascript:void(0)' title='Delete menu'><img src='<?php echo $icons['delete']; ?>' alt="Delete menu" /></a>
123
-
124
- <div class="ws_separator">&nbsp;</div>
125
-
126
- <?php
127
- if ( !$is_pro_version ) {
128
- ame_output_sort_buttons($icons);
129
- }
130
- ?>
131
-
132
- <?php if ( $is_pro_version ): ?>
133
- <a id='ws_toggle_toolbar' class='ws_button' href='javascript:void(0)' title='Toggle second toolbar'>
134
- <img src='<?php echo $icons['toggle-toolbar']; ?>' alt="Toolbar toggle" />
135
- </a>
136
- <?php endif; ?>
137
-
138
- <div class="clear"></div>
139
- </div>
140
-
141
- <div class="ws_button_container ws_second_toolbar_row <?php
142
- if (!$is_second_toolbar_visible) { echo ' hidden'; }
143
- ?>">
144
-
145
- <?php
146
- if ( $is_pro_version ) {
147
- ame_output_sort_buttons($icons);
148
- }
149
- ?>
150
-
151
- <?php do_action('admin_menu_editor-toolbar_row_2', $icons); ?>
152
- <div class="clear"></div>
153
- </div>
154
  </div>
155
 
156
  <div id='ws_menu_box' class="ws_box">
@@ -159,58 +237,24 @@ function ame_output_sort_buttons($icons) {
159
  <?php do_action('admin_menu_editor-container', 'menu'); ?>
160
  </div>
161
 
162
- <div class='ws_main_container'>
163
  <div class='ws_toolbar'>
164
- <div class="ws_button_container">
165
- <a id='ws_cut_item' class='ws_button' href='javascript:void(0)' title='Cut'><img src='<?php echo $icons['cut']; ?>' alt="Cut" /></a>
166
- <a id='ws_copy_item' class='ws_button' href='javascript:void(0)' title='Copy'><img src='<?php echo $icons['copy']; ?>' alt="Copy" /></a>
167
- <a id='ws_paste_item' class='ws_button' href='javascript:void(0)' title='Paste'><img src='<?php echo $icons['paste']; ?>' alt="Paste" /></a>
168
-
169
- <div class="ws_separator">&nbsp;</div>
170
-
171
- <a id='ws_new_item' class='ws_button' href='javascript:void(0)' title='New menu item'><img src='<?php echo $icons['new']; ?>' alt="New menu item" /></a>
172
- <?php if ( $is_pro_version ): ?>
173
- <a id='ws_new_submenu_separator' class='ws_button' href='javascript:void(0)' title='New separator'><img src='<?php echo $icons['new-separator']; ?>' alt="New separator" /></a>
174
- <?php endif; ?>
175
-
176
- <?php if ( $is_pro_version ): ?>
177
- <div class="ws_separator">&nbsp;</div>
178
-
179
- <a id='ws_hide_and_deny_item' class='ws_button ws_hide_and_deny_button' href='javascript:void(0)'
180
- title='Hide and prevent access. &#10;<?php echo esc_attr($hide_button_extra_tooltip); ?>'>
181
- <img src='<?php echo $icons['hide-and-deny']; ?>' alt="Hide" />
182
- </a>
183
- <?php endif; ?>
184
-
185
- <?php if ( $editor_data['show_deprecated_hide_button'] ): ?>
186
- <a id='ws_hide_item' class='ws_button' href='javascript:void(0)' title='Hide without preventing access (cosmetic)'><img src='<?php echo $icons['hide']; ?>' alt="Show/Hide" /></a>
187
- <?php endif; ?>
188
-
189
- <a id='ws_delete_item' class='ws_button ws_delete_menu_button' href='javascript:void(0)' title='Delete menu item'><img src='<?php echo $icons['delete']; ?>' alt="Delete menu item" /></a>
190
-
191
- <div class="ws_separator">&nbsp;</div>
192
-
193
- <?php
194
- if ( !$is_pro_version ) {
195
- ame_output_sort_buttons($icons);
196
- }
197
- ?>
198
-
199
- <div class="clear"></div>
200
- </div>
201
-
202
- <div class="ws_button_container ws_second_toolbar_row <?php
203
- if (!$is_second_toolbar_visible) { echo ' hidden'; }
204
- ?>">
205
 
206
- <?php
207
- if ( $is_pro_version ) {
208
- ame_output_sort_buttons($icons);
209
- }
210
- ?>
211
 
212
- <div class="clear"></div>
213
- </div>
 
 
 
 
214
  </div>
215
 
216
  <div id='ws_submenu_box' class="ws_box">
@@ -238,6 +282,8 @@ function ame_output_sort_buttons($icons) {
238
  <input type="hidden" name="expand_menu" id="ws_expand_selected_menu" value="">
239
  <input type="hidden" name="expand_submenu" id="ws_expand_selected_submenu" value="">
240
 
 
 
241
  <input type="button" id='ws_save_menu' class="button-primary ws_main_button" value="Save Changes" />
242
  </form>
243
 
29
  }
30
  $icons = apply_filters('admin_menu_editor-toolbar_icons', $icons, $images_url);
31
 
32
+ $toolbarButtons = new ameOrderedMap();
33
+ $toolbarButtons->addAll(array(
34
+ 'cut' => array(
35
+ 'title' => 'Cut',
36
+ ),
37
+ 'copy' => array(
38
+ 'title' => 'Copy',
39
+ ),
40
+ 'paste' => array(
41
+ 'title' => 'Paste',
42
+ ),
43
+ 'separator-1' => null,
44
+ 'new-menu' => array(
45
+ 'title' => 'New menu',
46
+ 'iconName' => 'new',
47
+ ),
48
+ 'new-separator' => array(
49
+ 'title' => 'New separator',
50
+ 'topLevelOnly' => !$is_pro_version,
51
+ ),
52
+ 'delete' => array(
53
+ 'title' => 'Delete menu',
54
+ 'class' => array('ws_delete_menu_button'),
55
+ ),
56
+ 'separator-2' => null,
57
+ ));
58
+
59
+ if ( !$is_pro_version ) {
60
+ ame_register_sort_buttons($toolbarButtons);
61
+ }
62
+
63
+ if ( $editor_data['show_deprecated_hide_button'] ) {
64
+ $toolbarButtons->insertBefore(
65
+ 'delete',
66
+ 'hide',
67
+ array(
68
+ 'title' => 'Hide without preventing access (cosmetic)',
69
+ 'alt' => 'Hide (cosmetic)',
70
+ )
71
+ );
72
+ }
73
+
74
+ $secondToolbarRow = new ameOrderedMap();
75
+ if ( $is_pro_version ) {
76
+ //In the Pro version, the sort buttons are on the second row.
77
+ ame_register_sort_buttons($secondToolbarRow);
78
+ }
79
+
80
+ $secondToolbarRowClasses = array('ws_second_toolbar_row');
81
+ if ( !$is_second_toolbar_visible ) {
82
+ $secondToolbarRowClasses[] = 'hidden';
83
+ }
84
+
85
+ do_action('admin_menu_editor-register_toolbar_buttons', $toolbarButtons, $secondToolbarRow, $icons);
86
+
87
+ if ( count($secondToolbarRow) > 0 ) {
88
+ $toolbarButtons->set(
89
+ 'toggle-toolbar',
90
+ array(
91
+ 'title' => 'Toggle second toolbar',
92
+ 'alt' => 'Toolbar toggle',
93
+ 'class' => array('ws_toggle_toolbar_button'),
94
+ 'topLevelOnly' => true,
95
+ )
96
+ );
97
+ }
98
+
99
+ /**
100
+ * @param ameOrderedMap $buttons
101
+ * @param array $icons
102
+ * @param array $classes CSS classes to add to the toolbar row.
103
+ */
104
+ function ame_output_toolbar_row($buttons, $icons, $classes = array()) {
105
+ $classes = array_merge(array('ws_button_container'), $classes);
106
+ printf('<div class="%s">', esc_attr(implode(' ', $classes)));
107
+
108
+ foreach ($buttons as $key => $settings) {
109
+ if ( $settings === null ) {
110
+ echo '<div class="ws_separator">&nbsp;</div>';
111
+ continue;
112
+ }
113
+
114
+ if ( !isset($settings['title']) ) {
115
+ $settings['title'] = $key;
116
+ }
117
+ $action = isset($settings['action']) ? $settings['action'] : $key;
118
+
119
+ $buttonClasses = array('ws_button');
120
+ if ( !empty($settings['class']) ) {
121
+ $buttonClasses = array_merge($buttonClasses, $settings['class']);
122
+ }
123
+
124
+ $attributes = array(
125
+ 'data-ame-button-action' => $action,
126
+ 'class' => implode(' ', $buttonClasses),
127
+ 'href' => '#',
128
+ 'title' => $settings['title'],
129
+ );
130
+ if ( isset($settings['attributes']) ) {
131
+ $attributes = array_merge($attributes, $settings['attributes']);
132
+ }
133
+
134
+ $iconName = isset($settings['iconName']) ? $settings['iconName'] : $key;
135
+ $icon = '';
136
+ if ( isset($icons[$iconName]) ) {
137
+ $icon = sprintf(
138
+ '<img src="%s" alt="%s">',
139
+ esc_attr($icons[$iconName]),
140
+ esc_attr(isset($settings['alt']) ? $settings['alt'] : $settings['title'])
141
+ );
142
+ }
143
+
144
+ $pairs = array();
145
+ foreach ($attributes as $name => $value) {
146
+ $pairs[] = $name . '="' . esc_attr($value) . '"';
147
+ }
148
+
149
+ printf('<a %s>%s</a>' . "\n", implode(' ', $pairs), $icon);
150
+ }
151
+
152
+ echo '<div class="clear"></div>' . "\n";
153
+ echo '</div>';
154
+ }
155
 
156
  //Output the "Upgrade to Pro" message
157
  if ( !apply_filters('admin_menu_editor_is_pro', false) ){
187
  include $extrasDirectory . '/copy-permissions-dialog.php';
188
  }
189
 
190
+ /**
191
+ * @param ameOrderedMap $toolbar
192
+ */
193
+ function ame_register_sort_buttons($toolbar) {
194
+ $toolbar->addAll(array(
195
+ 'sort-ascending' => array(
196
+ 'title' => 'Sort ascending',
197
+ 'action' => 'sort',
198
+ 'attributes' => array(
199
+ 'data-sort-direction' => 'asc',
200
+ ),
201
+ ),
202
+ 'sort-descending' => array(
203
+ 'title' => 'Sort descending',
204
+ 'action' => 'sort',
205
+ 'attributes' => array(
206
+ 'data-sort-direction' => 'desc',
207
+ ),
208
+ ),
209
+ ));
210
  }
211
 
212
  ?>
225
 
226
  <div class='ws_main_container'>
227
  <div class='ws_toolbar'>
228
+ <?php
229
+ ame_output_toolbar_row($toolbarButtons, $icons);
230
+ ame_output_toolbar_row($secondToolbarRow, $icons, $secondToolbarRowClasses);
231
+ ?>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  </div>
233
 
234
  <div id='ws_menu_box' class="ws_box">
237
  <?php do_action('admin_menu_editor-container', 'menu'); ?>
238
  </div>
239
 
240
+ <div class='ws_main_container' id="ame-submenu-column-template" style="display: none;">
241
  <div class='ws_toolbar'>
242
+ <?php
243
+ function ame_button_can_be_in_submenu_toolbar($settings) {
244
+ return empty($settings['topLevelOnly']);
245
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
+ ame_output_toolbar_row(
248
+ $toolbarButtons->filter('ame_button_can_be_in_submenu_toolbar'),
249
+ $icons
250
+ );
 
251
 
252
+ ame_output_toolbar_row(
253
+ $secondToolbarRow->filter('ame_button_can_be_in_submenu_toolbar'),
254
+ $icons,
255
+ $secondToolbarRowClasses
256
+ );
257
+ ?>
258
  </div>
259
 
260
  <div id='ws_submenu_box' class="ws_box">
282
  <input type="hidden" name="expand_menu" id="ws_expand_selected_menu" value="">
283
  <input type="hidden" name="expand_submenu" id="ws_expand_selected_submenu" value="">
284
 
285
+ <input type="hidden" name="deep_nesting_enabled" id="ws_is_deep_nesting_enabled" value="">
286
+
287
  <input type="button" id='ws_save_menu' class="button-primary ws_main_button" value="Save Changes" />
288
  </form>
289
 
includes/menu-editor-core.php CHANGED
@@ -113,6 +113,11 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
113
  */
114
  private $caps_used_in_menu = array();
115
 
 
 
 
 
 
116
  public $is_access_test = false;
117
  private $test_menu = null;
118
  /**
@@ -206,6 +211,10 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
206
  //with any role editing plugin. Disabled by default due to risk of conflicts and the performance impact.
207
  'bbpress_override_enabled' => false,
208
 
 
 
 
 
209
  //Which modules are active or inactive. Format: ['module-id' => true/false].
210
  'is_active_module' => array(
211
  'highlight-new-menus' => false,
@@ -232,7 +241,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
232
  *
233
  * We can't automatically detect menus like that. Here's why:
234
  * 1) Most plugins remove them too late, e.g. in admin_head. By that point, output has already started.
235
- * We need the finalize the list of menu items and their permissions before that.
236
  * 2) It's hard to automatically determine *why* a menu item was removed. We can't distinguish between
237
  * cosmetic changes like the hidden "welcome" items and people removing menus to deny access.
238
  */
@@ -246,6 +255,8 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
246
  //BuddyPress 2.3.4
247
  'index.php?page=bp-about' => true,
248
  'index.php?page=bp-credits' => true,
 
 
249
  //DW Question Answer 1.3.8.1
250
  'index.php?page=dwqa-about' => true,
251
  'index.php?page=dwqa-changelog' => true,
@@ -325,6 +336,10 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
325
  'index.php?page=simple-calendar_translators' => true,
326
  //Stripe For WooCommerce 3.2.12
327
  'wc_stripe' => 'submenu',
 
 
 
 
328
  );
329
 
330
  //AJAXify screen options
@@ -562,6 +577,9 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
562
  //Make sure Lodash doesn't conflict with the copy of Underscore that's bundled with WordPress.
563
  add_filter('script_loader_tag', array($this, 'lodash_noconflict'), 10, 2); //Filter exists since WP 4.1.
564
 
 
 
 
565
  //Compatibility fix for All In One Event Calendar; see the callback for details.
566
  add_action("admin_print_scripts-$page", array($this, 'dequeue_ai1ec_scripts'));
567
 
@@ -625,7 +643,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
625
  //Save the merged menu for later - the editor page will need it
626
  $this->merged_custom_menu = $custom_menu;
627
 
628
- do_action('admin_menu_editor-menu_merged', $custom_menu, $this->merged_custom_menu);
629
 
630
  //Convert our custom menu to the $menu + $submenu structure used by WP.
631
  //Note: This method sets up multiple internal fields and may cause side-effects.
@@ -633,6 +651,8 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
633
  $this->build_custom_wp_menu($this->merged_custom_menu['tree']);
634
  $this->user_cap_cache_enabled = false;
635
 
 
 
636
  if ( $this->is_access_test ) {
637
  $this->access_test_runner['wasCustomMenuApplied'] = true;
638
  $this->access_test_runner->setCurrentMenuItem($this->get_current_menu_item());
@@ -862,7 +882,15 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
862
 
863
  $wp_roles = ameRoleUtils::get_roles();
864
  foreach($wp_roles->roles as $role_id => $role) {
865
- $role['capabilities'] = $this->castValuesToBool($role['capabilities']);
 
 
 
 
 
 
 
 
866
  $roles[$role_id] = $role;
867
  }
868
 
@@ -953,7 +981,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
953
  $allRealCaps = ameRoleUtils::get_all_capabilities(true);
954
  //Similarly, capabilities that are directly assigned to users are probably real.
955
  foreach($users as $user) {
956
- $allRealCaps = array_merge($allRealCaps, $user['capabilities']);
957
  }
958
  //Role IDs can also be used as capabilities.
959
  foreach($roles as $roleId => $role) {
@@ -1046,12 +1074,12 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1046
 
1047
  $this->register_base_dependencies();
1048
 
1049
- //Move admin notices (e.g. "Settings saved") below editor tabs.
1050
- //This is a separate script because it has to run after common.js which is loaded in the page footer.
1051
  wp_enqueue_auto_versioned_script(
1052
- 'ame-editor-tab-fix',
1053
- plugins_url('js/editor-tab-fix.js', $this->plugin_file),
1054
- array('jquery', 'common'),
1055
  true
1056
  );
1057
 
@@ -1159,6 +1187,8 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1159
  'expandSelectedMenu' => isset($this->get['expand_menu']) && ($this->get['expand_menu'] === '1'),
1160
  'expandSelectedSubmenu' => isset($this->get['expand_submenu']) && ($this->get['expand_submenu'] === '1'),
1161
 
 
 
1162
  'isDemoMode' => defined('IS_DEMO_MODE'),
1163
  'isMasterMode' => defined('IS_MASTER_MODE'),
1164
  );
@@ -1719,34 +1749,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1719
 
1720
  if (!empty($topmenu['items'])) {
1721
  //Iterate over submenu items
1722
- foreach ($topmenu['items'] as &$item){
1723
- if ( !ameMenuItem::get($item, 'custom') ) {
1724
- $template_id = ameMenuItem::template_id($item);
1725
-
1726
- //Is this item present in the default WP menu?
1727
- if (isset($this->item_templates[$template_id])){
1728
- //Yes, load defaults from that item
1729
- $item['defaults'] = $this->item_templates[$template_id]['defaults'];
1730
- $this->item_templates[$template_id]['used'] = true;
1731
- //Add valid, non-custom items to the position index.
1732
- $positions_by_template[$template_id] = ameMenuItem::get($item, 'position', 0);
1733
- //We must move orphaned items elsewhere. Use the default location if possible.
1734
- if ( isset($topmenu['missing']) && $topmenu['missing'] ) {
1735
- $orphans[] = $item;
1736
- }
1737
- } else if ( empty($item['separator']) ) {
1738
- //Record as missing, unless it's a menu separator
1739
- $item['missing'] = true;
1740
-
1741
- $temp = ameMenuItem::apply_defaults($item);
1742
- $temp = $this->set_final_menu_capability($temp);
1743
- $this->add_access_lookup($temp, 'submenu', true);
1744
- }
1745
- } else {
1746
- //What if the parent of this custom item is missing?
1747
- //Right now the custom item will just disappear.
1748
- }
1749
- }
1750
  }
1751
  }
1752
 
@@ -1849,6 +1852,50 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1849
  return $tree;
1850
  }
1851
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1852
  /**
1853
  * Add a page and its required capability to the page access lookup.
1854
  *
@@ -1928,6 +1975,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1928
  $new_menu = array();
1929
  $new_submenu = array();
1930
  $this->title_lookups = array();
 
1931
 
1932
  //Prepare the top menu
1933
  $first_nonseparator_found = false;
@@ -1948,23 +1996,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1948
  }
1949
 
1950
  //Prepare the submenu of this menu
1951
- $new_items = array();
1952
- if( !empty($topmenu['items']) ){
1953
- $items = $topmenu['items'];
1954
-
1955
- foreach ($items as $item) {
1956
- $item = $this->prepare_for_output($item, 'submenu', $topmenu);
1957
- $new_items[] = $item;
1958
-
1959
- //Make a note of the page's correct title so we can fix it later if necessary.
1960
- $this->title_lookups[$item['file']] = !empty($item['page_title']) ? $item['page_title'] : $item['menu_title'];
1961
- }
1962
-
1963
- //Sort by position
1964
- usort($new_items, 'ameMenuItem::compare_position');
1965
- }
1966
-
1967
- $topmenu['items'] = $new_items;
1968
  $new_tree[] = $topmenu;
1969
  }
1970
 
@@ -1983,84 +2015,175 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1983
 
1984
  //Convert the prepared tree to the internal WordPress format.
1985
  foreach($new_tree as $topmenu) {
1986
- $trueAccess = isset($this->page_access_lookup[$topmenu['url']]) ? $this->page_access_lookup[$topmenu['url']] : null;
1987
- if ( ($trueAccess === 'do_not_allow') && ($topmenu['access_level'] !== $trueAccess) ) {
1988
- $topmenu['access_level'] = $trueAccess;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1989
  $reason = sprintf(
1990
  'There is a hidden menu item with the same URL (%1$s) but a higher priority.',
1991
- $topmenu['url']
1992
  );
1993
  $item['access_decision_reason'] = $reason;
1994
 
1995
- if ( isset($topmenu['access_check_log']) ) {
1996
- $topmenu['access_check_log'][] = sprintf(
1997
  '+ Override: %1$s Setting the capability to "%2$s".',
1998
  $reason,
1999
  $trueAccess
2000
  );
2001
- $topmenu['access_check_log'][] = str_repeat('=', 79);
2002
  }
2003
  }
2004
 
2005
- if ( !isset($this->reverse_item_lookup[$topmenu['url']]) ) { //Prefer sub-menus.
2006
- if ( $this->is_item_visitable($topmenu) ) {
2007
- $this->reverse_item_lookup[$topmenu['url']] = $topmenu;
2008
- }
2009
  }
2010
 
2011
- $has_submenu_icons = false;
2012
- foreach($topmenu['items'] as $item) {
2013
- $trueAccess = isset($this->page_access_lookup[$item['url']]) ? $this->page_access_lookup[$item['url']] : null;
2014
- if ( ($trueAccess === 'do_not_allow') && ($item['access_level'] !== $trueAccess) ) {
2015
- $item['access_level'] = $trueAccess;
2016
- $reason = sprintf(
2017
- 'There is a hidden menu item with the same URL (%1$s) but a higher priority.',
2018
- $item['url']
2019
- );
2020
- $item['access_decision_reason'] = $reason;
2021
 
2022
- if ( isset($item['access_check_log']) ) {
2023
- $item['access_check_log'][] = sprintf(
2024
- '+ Override: %1$s Setting the capability to "%2$s".',
2025
- $reason,
2026
- $trueAccess
2027
- );
2028
- $item['access_check_log'][] = str_repeat('=', 79);
2029
- }
2030
- }
2031
 
2032
- if ( $this->is_item_visitable($item) ) {
2033
- $this->reverse_item_lookup[$item['url']] = $item;
2034
- }
2035
 
2036
- //Skip missing and hidden items
2037
- if ( !empty($item['missing']) || !empty($item['hidden']) ) {
2038
- continue;
2039
- }
2040
 
2041
- //Keep track of which menus have items with icons. Ignore hidden items.
2042
- $has_submenu_icons = $has_submenu_icons
2043
- || (!empty($item['has_submenu_icon']) && $item['access_level'] !== 'do_not_allow');
 
2044
 
2045
- $new_submenu[$topmenu['file']][] = $this->convert_to_wp_format($item);
2046
- }
 
 
 
2047
 
2048
- //Skip missing and hidden menus.
2049
- if ( !empty($topmenu['missing']) || !empty($topmenu['hidden']) ) {
2050
- continue;
2051
- }
2052
 
2053
- //The ame-has-submenu-icons class lets us change the appearance of all submenu items at once,
2054
- //without having to add classes/styles to each item individually.
2055
- if ( $has_submenu_icons && (strpos($topmenu['css_class'], 'ame-has-submenu-icons') === false) ) {
2056
- $topmenu['css_class'] .= ' ame-has-submenu-icons';
2057
- }
 
 
 
 
 
 
 
 
 
2058
 
2059
- $new_menu[] = $this->convert_to_wp_format($topmenu);
 
2060
  }
2061
 
2062
- $this->custom_wp_menu = $new_menu;
2063
- $this->custom_wp_submenu = $new_submenu;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2064
  }
2065
 
2066
  /**
@@ -2452,6 +2575,14 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
2452
  do_action('admin_menu_editor-footer-' . $this->current_tab, $action);
2453
  }
2454
 
 
 
 
 
 
 
 
 
2455
  private function repair_database() {
2456
  global $wpdb; /** @var wpdb $wpdb */
2457
 
@@ -2603,6 +2734,11 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
2603
  wp_die(implode("<br>\n", $messages));
2604
  }
2605
 
 
 
 
 
 
2606
  //Redirect back to the editor and display the success message.
2607
  $query = array('message' => 1);
2608
 
@@ -2735,6 +2871,9 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
2735
  //bbPress override support.
2736
  $this->options['bbpress_override_enabled'] = !empty($this->post['bbpress_override_enabled']);
2737
 
 
 
 
2738
  //Active modules.
2739
  $activeModules = isset($this->post['active_modules']) ? (array)$this->post['active_modules'] : array();
2740
  $activeModules = array_fill_keys(array_map('strval', $activeModules), true);
@@ -2748,6 +2887,36 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
2748
  }
2749
  }
2750
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2751
  private function display_editor_ui() {
2752
  //Prepare a bunch of parameters for the editor.
2753
  $editor_data = array(
@@ -2884,7 +3053,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
2884
  * Display the tabs for the settings page.
2885
  */
2886
  public function display_editor_tabs() {
2887
- echo '<h2 class="nav-tab-wrapper">';
2888
  foreach($this->tabs as $slug => $title) {
2889
  printf(
2890
  '<a href="%s" id="%s" class="nav-tab%s">%s</a>',
@@ -3262,11 +3431,12 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
3262
  * would not be highlighted properly when the user visits them.
3263
  */
3264
  public function enqueue_menu_fix_script() {
 
 
3265
  //Compatibility fix for PRO Theme 1.1.5.
3266
  //This custom admin theme expands the current admin menu via JavaScript by using a "ready" handler.
3267
  //We need to ensure that we highlight the correct current menu before that happens. This means we
3268
  //have to enqueue the script in the header and with a higher priority than the PRO Theme script.
3269
- $inFooter = true;
3270
  if ( class_exists('PROTheme', false) ) {
3271
  $inFooter = false;
3272
  }
@@ -4578,6 +4748,12 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
4578
  'requiredPhpVersion' => '5.3',
4579
  'title' => 'Dashboard Widgets',
4580
  ),
 
 
 
 
 
 
4581
  'plugin-visibility' => array(
4582
  'relativePath' => 'modules/plugin-visibility/plugin-visibility.php',
4583
  'className' => 'amePluginVisibility',
@@ -4649,6 +4825,13 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
4649
  return true;
4650
  }
4651
 
 
 
 
 
 
 
 
4652
  } //class
4653
 
4654
 
113
  */
114
  private $caps_used_in_menu = array();
115
 
116
+ /**
117
+ * @var bool Tue if the last displayed custom menu had more than two levels.
118
+ */
119
+ private $custom_menu_is_deep = false;
120
+
121
  public $is_access_test = false;
122
  private $test_menu = null;
123
  /**
211
  //with any role editing plugin. Disabled by default due to risk of conflicts and the performance impact.
212
  'bbpress_override_enabled' => false,
213
 
214
+ //Experimental: Allow more than two levels of menus.
215
+ 'deep_nesting_enabled' => null,
216
+ 'was_nesting_ever_changed' => false,
217
+
218
  //Which modules are active or inactive. Format: ['module-id' => true/false].
219
  'is_active_module' => array(
220
  'highlight-new-menus' => false,
241
  *
242
  * We can't automatically detect menus like that. Here's why:
243
  * 1) Most plugins remove them too late, e.g. in admin_head. By that point, output has already started.
244
+ * We need to finalize the list of menu items and their permissions before that.
245
  * 2) It's hard to automatically determine *why* a menu item was removed. We can't distinguish between
246
  * cosmetic changes like the hidden "welcome" items and people removing menus to deny access.
247
  */
255
  //BuddyPress 2.3.4
256
  'index.php?page=bp-about' => true,
257
  'index.php?page=bp-credits' => true,
258
+ //BuddyBoss 1.5.9
259
+ 'admin.php?page=buddyboss-platform' => 'submenu',
260
  //DW Question Answer 1.3.8.1
261
  'index.php?page=dwqa-about' => true,
262
  'index.php?page=dwqa-changelog' => true,
336
  'index.php?page=simple-calendar_translators' => true,
337
  //Stripe For WooCommerce 3.2.12
338
  'wc_stripe' => 'submenu',
339
+ //WP Grid Builder 1.5.9
340
+ 'admin.php?page=wpgb-card-builder' => true,
341
+ 'admin.php?page=wpgb-grid-settings' => true,
342
+ 'admin.php?page=wpgb-facet-settings' => true,
343
  );
344
 
345
  //AJAXify screen options
577
  //Make sure Lodash doesn't conflict with the copy of Underscore that's bundled with WordPress.
578
  add_filter('script_loader_tag', array($this, 'lodash_noconflict'), 10, 2); //Filter exists since WP 4.1.
579
 
580
+ //Let modules do something when loading a specific tab but before output starts.
581
+ add_action('load-' . $page, array($this, 'trigger_tab_load_event'));
582
+
583
  //Compatibility fix for All In One Event Calendar; see the callback for details.
584
  add_action("admin_print_scripts-$page", array($this, 'dequeue_ai1ec_scripts'));
585
 
643
  //Save the merged menu for later - the editor page will need it
644
  $this->merged_custom_menu = $custom_menu;
645
 
646
+ do_action('admin_menu_editor-menu_merged', $this->merged_custom_menu);
647
 
648
  //Convert our custom menu to the $menu + $submenu structure used by WP.
649
  //Note: This method sets up multiple internal fields and may cause side-effects.
651
  $this->build_custom_wp_menu($this->merged_custom_menu['tree']);
652
  $this->user_cap_cache_enabled = false;
653
 
654
+ do_action('admin_menu_editor-menu_built', $this->merged_custom_menu, $this);
655
+
656
  if ( $this->is_access_test ) {
657
  $this->access_test_runner['wasCustomMenuApplied'] = true;
658
  $this->access_test_runner->setCurrentMenuItem($this->get_current_menu_item());
882
 
883
  $wp_roles = ameRoleUtils::get_roles();
884
  foreach($wp_roles->roles as $role_id => $role) {
885
+ //There is at least one plugin that creates a custom role without a "capabilities" key.
886
+ //We need to check for that to avoid an "undefined array key" warning.
887
+ if ( array_key_exists('capabilities', $role) ) {
888
+ //Some plugins use 1, 0, null, or other truthy/falsy values for capability settings.
889
+ //AME uses booleans. It helps avoid bugs and it's also what WordPress core does.
890
+ $role['capabilities'] = $this->castValuesToBool($role['capabilities']);
891
+ } else {
892
+ $role['capabilities'] = array();
893
+ }
894
  $roles[$role_id] = $role;
895
  }
896
 
981
  $allRealCaps = ameRoleUtils::get_all_capabilities(true);
982
  //Similarly, capabilities that are directly assigned to users are probably real.
983
  foreach($users as $user) {
984
+ $allRealCaps = $allRealCaps + $user['capabilities'];
985
  }
986
  //Role IDs can also be used as capabilities.
987
  foreach($roles as $roleId => $role) {
1074
 
1075
  $this->register_base_dependencies();
1076
 
1077
+ //Tab utilities and fixes.
1078
+ //This is a separate script because some of it has to run after common.js, which is loaded in the page footer.
1079
  wp_enqueue_auto_versioned_script(
1080
+ 'ame-settings-tab-utils',
1081
+ plugins_url('js/tab-utils.js', $this->plugin_file),
1082
+ array('jquery', 'ame-lodash', 'common'),
1083
  true
1084
  );
1085
 
1187
  'expandSelectedMenu' => isset($this->get['expand_menu']) && ($this->get['expand_menu'] === '1'),
1188
  'expandSelectedSubmenu' => isset($this->get['expand_submenu']) && ($this->get['expand_submenu'] === '1'),
1189
 
1190
+ 'deepNestingEnabled' => $this->options['deep_nesting_enabled'],
1191
+
1192
  'isDemoMode' => defined('IS_DEMO_MODE'),
1193
  'isMasterMode' => defined('IS_MASTER_MODE'),
1194
  );
1749
 
1750
  if (!empty($topmenu['items'])) {
1751
  //Iterate over submenu items
1752
+ $this->merge_children($topmenu, $positions_by_template, $orphans);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1753
  }
1754
  }
1755
 
1852
  return $tree;
1853
  }
1854
 
1855
+ /**
1856
+ * Merge the children of a menu item with the default values from the WordPress menu.
1857
+ * This section was extracted to a method just to make it possible to call it recursively.
1858
+ *
1859
+ * @param array $menu
1860
+ * @param array $positions_by_template
1861
+ * @param array $orphans
1862
+ */
1863
+ private function merge_children(&$menu, &$positions_by_template, &$orphans) {
1864
+ foreach ($menu['items'] as &$item){
1865
+ if ( !ameMenuItem::get($item, 'custom') ) {
1866
+ $template_id = ameMenuItem::template_id($item);
1867
+
1868
+ //Is this item present in the default WP menu?
1869
+ if (isset($this->item_templates[$template_id])){
1870
+ //Yes, load defaults from that item
1871
+ $item['defaults'] = $this->item_templates[$template_id]['defaults'];
1872
+ $this->item_templates[$template_id]['used'] = true;
1873
+ //Add valid, non-custom items to the position index.
1874
+ $positions_by_template[$template_id] = ameMenuItem::get($item, 'position', 0);
1875
+ //We must move orphaned items elsewhere. Use the default location if possible.
1876
+ if ( isset($menu['missing']) && $menu['missing'] ) {
1877
+ $orphans[] = $item;
1878
+ }
1879
+ } else if ( empty($item['separator']) ) {
1880
+ //Record as missing, unless it's a menu separator
1881
+ $item['missing'] = true;
1882
+
1883
+ $temp = ameMenuItem::apply_defaults($item);
1884
+ $temp = $this->set_final_menu_capability($temp);
1885
+ $this->add_access_lookup($temp, 'submenu', true);
1886
+ }
1887
+ } else {
1888
+ //What if the parent of this custom item is missing?
1889
+ //Right now the custom item will just disappear.
1890
+ }
1891
+
1892
+ if ( !empty($item['items']) ) {
1893
+ //Recursively merge children of submenu items.
1894
+ $this->merge_children($item, $positions_by_template, $orphans);
1895
+ }
1896
+ }
1897
+ }
1898
+
1899
  /**
1900
  * Add a page and its required capability to the page access lookup.
1901
  *
1975
  $new_menu = array();
1976
  $new_submenu = array();
1977
  $this->title_lookups = array();
1978
+ $this->custom_menu_is_deep = false;
1979
 
1980
  //Prepare the top menu
1981
  $first_nonseparator_found = false;
1996
  }
1997
 
1998
  //Prepare the submenu of this menu
1999
+ $topmenu['items'] = $this->prepare_children_for_output($topmenu);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2000
  $new_tree[] = $topmenu;
2001
  }
2002
 
2015
 
2016
  //Convert the prepared tree to the internal WordPress format.
2017
  foreach($new_tree as $topmenu) {
2018
+ $this->build_top_level_item($topmenu, $new_menu, $new_submenu);
2019
+ }
2020
+
2021
+ $this->custom_wp_menu = $new_menu;
2022
+ $this->custom_wp_submenu = $new_submenu;
2023
+ }
2024
+
2025
+ /**
2026
+ * Prepare all of the children (i.e. submenu items) of a menu for output.
2027
+ *
2028
+ * @param array $menu A menu item.
2029
+ * @return array
2030
+ */
2031
+ private function prepare_children_for_output($menu) {
2032
+ if ( empty($menu['items']) ) {
2033
+ return array();
2034
+ }
2035
+
2036
+ $new_items = array();
2037
+
2038
+ foreach ($menu['items'] as $item) {
2039
+ $item = $this->prepare_for_output($item, 'submenu', $menu);
2040
+
2041
+ //Make a note of the page's correct title so we can fix it later if necessary.
2042
+ $this->title_lookups[$item['file']] = !empty($item['page_title']) ? $item['page_title'] : $item['menu_title'];
2043
+
2044
+ if ( !empty($item['items']) ) {
2045
+ $item['items'] = $this->prepare_children_for_output($item);
2046
+ }
2047
+
2048
+ $new_items[] = $item;
2049
+ }
2050
+
2051
+ //Sort by position
2052
+ usort($new_items, 'ameMenuItem::compare_position');
2053
+
2054
+ return $new_items;
2055
+ }
2056
+
2057
+ /**
2058
+ * Convert one top level menu and all of its submenu items to the WP menu format.
2059
+ *
2060
+ * @param array $topmenu A menu item.
2061
+ * @param array $menu Top level menu list. The converted item will be added to this list.
2062
+ * @param array $submenu Submenu list. The converted submenus (if any) will be added to this list.
2063
+ */
2064
+ private function build_top_level_item($topmenu, &$menu, &$submenu) {
2065
+ $trueAccess = isset($this->page_access_lookup[$topmenu['url']]) ? $this->page_access_lookup[$topmenu['url']] : null;
2066
+ if ( ($trueAccess === 'do_not_allow') && ($topmenu['access_level'] !== $trueAccess) ) {
2067
+ $topmenu['access_level'] = $trueAccess;
2068
+ $reason = sprintf(
2069
+ 'There is a hidden menu item with the same URL (%1$s) but a higher priority.',
2070
+ $topmenu['url']
2071
+ );
2072
+ $item['access_decision_reason'] = $reason;
2073
+
2074
+ if ( isset($topmenu['access_check_log']) ) {
2075
+ $topmenu['access_check_log'][] = sprintf(
2076
+ '+ Override: %1$s Setting the capability to "%2$s".',
2077
+ $reason,
2078
+ $trueAccess
2079
+ );
2080
+ $topmenu['access_check_log'][] = str_repeat('=', 79);
2081
+ }
2082
+ }
2083
+
2084
+ if ( !isset($this->reverse_item_lookup[$topmenu['url']]) ) { //Prefer sub-menus.
2085
+ if ( $this->is_item_visitable($topmenu) ) {
2086
+ $this->reverse_item_lookup[$topmenu['url']] = $topmenu;
2087
+ }
2088
+ }
2089
+
2090
+ $has_submenu_icons = false;
2091
+ foreach($topmenu['items'] as $item) {
2092
+ $trueAccess = isset($this->page_access_lookup[$item['url']]) ? $this->page_access_lookup[$item['url']] : null;
2093
+ if ( ($trueAccess === 'do_not_allow') && ($item['access_level'] !== $trueAccess) ) {
2094
+ $item['access_level'] = $trueAccess;
2095
  $reason = sprintf(
2096
  'There is a hidden menu item with the same URL (%1$s) but a higher priority.',
2097
+ $item['url']
2098
  );
2099
  $item['access_decision_reason'] = $reason;
2100
 
2101
+ if ( isset($item['access_check_log']) ) {
2102
+ $item['access_check_log'][] = sprintf(
2103
  '+ Override: %1$s Setting the capability to "%2$s".',
2104
  $reason,
2105
  $trueAccess
2106
  );
2107
+ $item['access_check_log'][] = str_repeat('=', 79);
2108
  }
2109
  }
2110
 
2111
+ if ( $this->is_item_visitable($item) ) {
2112
+ $this->reverse_item_lookup[$item['url']] = $item;
 
 
2113
  }
2114
 
2115
+ //Skip missing and hidden items
2116
+ if ( !empty($item['missing']) || !empty($item['hidden']) ) {
2117
+ continue;
2118
+ }
 
 
 
 
 
 
2119
 
2120
+ //Keep track of which menus have items with icons. Ignore hidden items.
2121
+ $has_submenu_icons = $has_submenu_icons
2122
+ || (!empty($item['has_submenu_icon']) && $item['access_level'] !== 'do_not_allow');
 
 
 
 
 
 
2123
 
2124
+ if ( !empty($item['items']) ) {
2125
+ $this->build_nested_submenu($item, $menu, $submenu);
2126
+ }
2127
 
2128
+ $submenu[$topmenu['file']][] = $this->convert_to_wp_format($item);
2129
+ }
 
 
2130
 
2131
+ //Skip missing and hidden menus.
2132
+ if ( !empty($topmenu['missing']) || !empty($topmenu['hidden']) ) {
2133
+ return;
2134
+ }
2135
 
2136
+ //The ame-has-submenu-icons class lets us change the appearance of all submenu items at once,
2137
+ //without having to add classes/styles to each item individually.
2138
+ if ( $has_submenu_icons && (strpos($topmenu['css_class'], 'ame-has-submenu-icons') === false) ) {
2139
+ $topmenu['css_class'] .= ' ame-has-submenu-icons';
2140
+ }
2141
 
2142
+ $menu[] = $this->convert_to_wp_format($topmenu);
2143
+ }
 
 
2144
 
2145
+ /**
2146
+ * Generate WP-compatible menu items for deeply nested submenus - that is, third level and beyond.
2147
+ *
2148
+ * @param array $item
2149
+ * @param array $wpMenu
2150
+ * @param array $wpSubmenu
2151
+ */
2152
+ private function build_nested_submenu(&$item, &$wpMenu, &$wpSubmenu) {
2153
+ static $uniquePrefix = null, $submenuCounter = 0;
2154
+ if ( empty($item['items']) ) {
2155
+ return;
2156
+ }
2157
+
2158
+ $this->custom_menu_is_deep = true;
2159
 
2160
+ if ( $uniquePrefix === null ) {
2161
+ $uniquePrefix = (string) rand(1000, 9999);
2162
  }
2163
 
2164
+ $submenuCounter++;
2165
+ $uniqueClass = 'ame-ds-m' . $uniquePrefix . $submenuCounter;
2166
+ $submenuClass = 'ame-ds-child-of-' . $uniqueClass;
2167
+
2168
+ //Flag the parent item as having a submenu.
2169
+ $item['css_class'] .= ' ame-has-deep-submenu ' . $uniqueClass;
2170
+
2171
+ //Output the submenu itself as a separate top level menu. The Pro version will then use JS to move it
2172
+ //to the right place in the DOM and make it work like a nested submenu. The free version doesn't have
2173
+ //that feature, but the menu will still be usable.
2174
+ $containerTopLevelMenu = array_merge(
2175
+ $item,
2176
+ array(
2177
+ 'css_class' => 'menu-top ' . $submenuClass,
2178
+ 'icon_url' => 'dashicons-menu',
2179
+
2180
+ //To avoid ID clashes, it would be useful to give each menu a unique slug/URL.
2181
+ //However, that breaks menu URL generation because WP also uses the parent URL for that.
2182
+ //'file' => '#ds' . $submenuCounter . '-' . $item['file'],
2183
+ )
2184
+ );
2185
+
2186
+ $this->build_top_level_item($containerTopLevelMenu, $wpMenu, $wpSubmenu);
2187
  }
2188
 
2189
  /**
2575
  do_action('admin_menu_editor-footer-' . $this->current_tab, $action);
2576
  }
2577
 
2578
+ public function trigger_tab_load_event() {
2579
+ //Modules can use this hook in place of the "load-$page_hook" action. This way a module
2580
+ //doesn't need to know what the page hook is, and it can easily target a specific tab.
2581
+ if ( !empty($this->current_tab) ) {
2582
+ do_action('admin_menu_editor-load_tab-' . $this->current_tab);
2583
+ }
2584
+ }
2585
+
2586
  private function repair_database() {
2587
  global $wpdb; /** @var wpdb $wpdb */
2588
 
2734
  wp_die(implode("<br>\n", $messages));
2735
  }
2736
 
2737
+ //Save nesting settings.
2738
+ if ( $this->update_nesting_settings($post) ) {
2739
+ $this->save_options();
2740
+ }
2741
+
2742
  //Redirect back to the editor and display the success message.
2743
  $query = array('message' => 1);
2744
 
2871
  //bbPress override support.
2872
  $this->options['bbpress_override_enabled'] = !empty($this->post['bbpress_override_enabled']);
2873
 
2874
+ //Three level menus / deep nesting.
2875
+ $this->update_nesting_settings($this->post);
2876
+
2877
  //Active modules.
2878
  $activeModules = isset($this->post['active_modules']) ? (array)$this->post['active_modules'] : array();
2879
  $activeModules = array_fill_keys(array_map('strval', $activeModules), true);
2887
  }
2888
  }
2889
 
2890
+ /**
2891
+ * Update menu nesting/three level settings.
2892
+ *
2893
+ * Note: This method does not actually save the new settings to the database,
2894
+ * it just modifies them in memory.
2895
+ *
2896
+ * @param array $post
2897
+ * @return boolean True if settings were changed, false otherwise.
2898
+ */
2899
+ private function update_nesting_settings($post) {
2900
+ if ( !isset($post['deep_nesting_enabled']) ) {
2901
+ return false;
2902
+ }
2903
+
2904
+ $nesting_enabled = $this->json_decode($post['deep_nesting_enabled']);
2905
+ $valid_nesting_settings = array(null, true, false);
2906
+ if (
2907
+ in_array($nesting_enabled, $valid_nesting_settings, true)
2908
+ && ($nesting_enabled !== $this->options['deep_nesting_enabled'])
2909
+ ) {
2910
+ $this->options['deep_nesting_enabled'] = $nesting_enabled;
2911
+ if ( $nesting_enabled !== null ) {
2912
+ $this->options['was_nesting_ever_changed'] = true;
2913
+ }
2914
+ return true;
2915
+ }
2916
+
2917
+ return false;
2918
+ }
2919
+
2920
  private function display_editor_ui() {
2921
  //Prepare a bunch of parameters for the editor.
2922
  $editor_data = array(
3053
  * Display the tabs for the settings page.
3054
  */
3055
  public function display_editor_tabs() {
3056
+ echo '<h2 class="nav-tab-wrapper ws-ame-nav-tab-list">';
3057
  foreach($this->tabs as $slug => $title) {
3058
  printf(
3059
  '<a href="%s" id="%s" class="nav-tab%s">%s</a>',
3431
  * would not be highlighted properly when the user visits them.
3432
  */
3433
  public function enqueue_menu_fix_script() {
3434
+ $inFooter = !$this->is_custom_menu_deep();
3435
+
3436
  //Compatibility fix for PRO Theme 1.1.5.
3437
  //This custom admin theme expands the current admin menu via JavaScript by using a "ready" handler.
3438
  //We need to ensure that we highlight the correct current menu before that happens. This means we
3439
  //have to enqueue the script in the header and with a higher priority than the PRO Theme script.
 
3440
  if ( class_exists('PROTheme', false) ) {
3441
  $inFooter = false;
3442
  }
4748
  'requiredPhpVersion' => '5.3',
4749
  'title' => 'Dashboard Widgets',
4750
  ),
4751
+ 'redirector' => array(
4752
+ 'relativePath' => 'modules/redirector/redirector.php',
4753
+ 'className' => '\\YahnisElsts\\AdminMenuEditor\\Redirects\\Module',
4754
+ 'title' => 'Redirects',
4755
+ 'requiredPhpVersion' => '5.6.20', //Same as WP 5.8.
4756
+ ),
4757
  'plugin-visibility' => array(
4758
  'relativePath' => 'modules/plugin-visibility/plugin-visibility.php',
4759
  'className' => 'amePluginVisibility',
4825
  return true;
4826
  }
4827
 
4828
+ /**
4829
+ * @return bool
4830
+ */
4831
+ public function is_custom_menu_deep() {
4832
+ return $this->custom_menu_is_deep;
4833
+ }
4834
+
4835
  } //class
4836
 
4837
 
includes/menu-item.php CHANGED
@@ -257,7 +257,7 @@ abstract class ameMenuItem {
257
  //This is because the URL includes a "return" parameter that contains the current page's URL. It also makes
258
  //the template ID different on every page, so it's impossible to identify the menu. To fix that, lets remove
259
  //the "return" parameter from the ID.
260
- if ( ($parent_file === 'themes.php') && (strpos($item_file, 'customize.php?') === 0) ) {
261
  $item_file = remove_query_arg('return', $item_file);
262
  }
263
 
@@ -641,4 +641,45 @@ abstract class ameMenuItem {
641
  }
642
  return $url;
643
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
644
  }
257
  //This is because the URL includes a "return" parameter that contains the current page's URL. It also makes
258
  //the template ID different on every page, so it's impossible to identify the menu. To fix that, lets remove
259
  //the "return" parameter from the ID.
260
+ if ( strpos($item_file, 'customize.php?') === 0 ) {
261
  $item_file = remove_query_arg('return', $item_file);
262
  }
263
 
641
  }
642
  return $url;
643
  }
644
+
645
+ /**
646
+ * Remove the number of pending updates or other count elements from a menu title.
647
+ *
648
+ * @param string $menuTitle
649
+ * @return string
650
+ */
651
+ public static function remove_update_count($menuTitle) {
652
+ if ( (stripos($menuTitle, '<span') === false) || !class_exists('DOMDocument', false) ) {
653
+ return $menuTitle;
654
+ }
655
+
656
+ $dom = new DOMDocument();
657
+ $uniqueId = 'ame-rex-title-wrapper-' . time();
658
+ if ( @$dom->loadHTML('<div id="' . $uniqueId . '">' . $menuTitle . '</div>') ) {
659
+ /** @noinspection PhpComposerExtensionStubsInspection */
660
+ $xpath = new DOMXpath($dom);
661
+ $result = $xpath->query('//span[contains(@class,"update-plugins") or contains(@class,"awaiting-mod")]');
662
+ if ( $result->length > 0 ) {
663
+ //Remove all matched nodes. We must iterate backwards to prevent messing up the DOMNodeList.
664
+ for ($i = $result->length - 1; $i >= 0; $i--) {
665
+ $span = $result->item(0);
666
+ $span->parentNode->removeChild($span);
667
+ }
668
+
669
+ $innerHtml = '';
670
+ $children = $dom->getElementById($uniqueId)->childNodes;
671
+ //In theory, $children should always be a DOMNodeList, but there has been
672
+ //at least one report about the foreach() statement throwing a warning
673
+ //because $children was not iterable.
674
+ if ( $children instanceof Traversable ) {
675
+ foreach ($children as $child) {
676
+ $innerHtml .= $child->ownerDocument->saveHTML($child);
677
+ }
678
+ }
679
+
680
+ return $innerHtml;
681
+ }
682
+ }
683
+ return $menuTitle;
684
+ }
685
  }
includes/menu.php CHANGED
@@ -227,6 +227,7 @@ abstract class ameMenu {
227
  */
228
  public static function to_json($menu) {
229
  $menu = self::add_format_header($menu);
 
230
  $result = json_encode($menu);
231
  if ( !is_string($result) ) {
232
  $message = sprintf(
227
  */
228
  public static function to_json($menu) {
229
  $menu = self::add_format_header($menu);
230
+ //todo: Maybe use wp_json_encode() instead. At least one user had invalid UTF-8 characters in their menu.
231
  $result = json_encode($menu);
232
  if ( !is_string($result) ) {
233
  $message = sprintf(
includes/role-utils.php CHANGED
@@ -27,7 +27,10 @@ class ameRoleUtils {
27
  //Iterate over all known roles and collect their capabilities
28
  foreach($wp_roles->roles as $role){
29
  if ( !empty($role['capabilities']) && is_array($role['capabilities']) ){ //Being defensive here
30
- $capabilities = array_merge($capabilities, $role['capabilities']);
 
 
 
31
  }
32
  }
33
  $regular_cache = $capabilities;
@@ -42,7 +45,7 @@ class ameRoleUtils {
42
  'manage_network_options' => 1,
43
  'manage_network_plugins' => 1,
44
  );
45
- $capabilities = array_merge($capabilities, $multisite_caps);
46
  $multisite_cache = $capabilities;
47
  }
48
 
27
  //Iterate over all known roles and collect their capabilities
28
  foreach($wp_roles->roles as $role){
29
  if ( !empty($role['capabilities']) && is_array($role['capabilities']) ){ //Being defensive here
30
+ //We use the "+" operator instead of array_merge() to combine arrays because we don't want
31
+ //integer keys to be renumbered. Technically, capabilities should be strings and not integers,
32
+ //but in practice some plugins do create integer capabilities.
33
+ $capabilities = $capabilities + $role['capabilities'];
34
  }
35
  }
36
  $regular_cache = $capabilities;
45
  'manage_network_options' => 1,
46
  'manage_network_plugins' => 1,
47
  );
48
+ $capabilities = $capabilities + $multisite_caps;
49
  $multisite_cache = $capabilities;
50
  }
51
 
includes/settings-page.php CHANGED
@@ -354,6 +354,50 @@ $isProVersion = apply_filters('admin_menu_editor_is_pro', false);
354
  </td>
355
  </tr>
356
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  <tr>
358
  <th scope="row">
359
  WPML support
354
  </td>
355
  </tr>
356
 
357
+ <?php
358
+ //The free version lacks the ability to render deeply nested menus in the dashboard, so the nesting
359
+ //options are hidden by default. However, if the user somehow acquires a configuration where this
360
+ //feature is enabled (e.g. by importing config from the Pro version), the free version can display
361
+ //and even edit that configuration to a limited extent.
362
+ if ( $isProVersion || !empty($settings['was_nesting_ever_changed']) ):
363
+ ?>
364
+ <tr>
365
+ <th scope="row">
366
+ Three level menus
367
+ <a class="ws_tooltip_trigger ame-warning-tooltip"
368
+ title="Caution: Experimental feature.&lt;br&gt;
369
+ This feature might not work as expected and it could cause conflicts with other plugins or themes.">
370
+ <div class="dashicons dashicons-admin-tools"></div>
371
+ </a>
372
+ </th>
373
+ <td>
374
+ <fieldset>
375
+ <?php
376
+ $nestingOptions = array(
377
+ 'Ask on first use' => null,
378
+ 'Enabled' . ($isProVersion ? '' : ' (only in editor)') => true,
379
+ 'Disabled' => false,
380
+ );
381
+ foreach ($nestingOptions as $label => $nestingSetting):
382
+ ?>
383
+ <p>
384
+ <label>
385
+ <input type="radio" name="deep_nesting_enabled"
386
+ value="<?php echo esc_attr(json_encode($nestingSetting)); ?>"
387
+ <?php
388
+ if ( $settings['deep_nesting_enabled'] === $nestingSetting ) {
389
+ echo ' checked="checked"';
390
+ }
391
+ ?>>
392
+ <?php echo $label; ?>
393
+ </label>
394
+ </p>
395
+ <?php endforeach; ?>
396
+ </fieldset>
397
+ </td>
398
+ </tr>
399
+ <?php endif; ?>
400
+
401
  <tr>
402
  <th scope="row">
403
  WPML support
includes/shortcodes.php ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class ameCoreShortcodes {
4
+ protected static $allowedUserFields = array(
5
+ 'ID',
6
+ 'user_login',
7
+ 'display_name',
8
+ 'first_name',
9
+ 'last_name',
10
+ 'nickname',
11
+ 'description',
12
+ 'locale',
13
+ 'user_nicename',
14
+ 'user_url',
15
+ 'user_registered',
16
+ 'user_status',
17
+ );
18
+
19
+ public function register() {
20
+ add_shortcode('ame-wp-admin', array($this, 'handleAdminUrl'));
21
+ add_shortcode('ame-home-url', array($this, 'handleHomeUrl'));
22
+ add_shortcode('ame-user-info', array($this, 'handleUserInfo'));
23
+ }
24
+
25
+ /** @noinspection PhpUnusedParameterInspection Parameters are required by the shortcode API. */
26
+ public function handleAdminUrl($attributes = array(), $content = null, $tag = 'ame-wp-admin') {
27
+ if ( is_callable('self_admin_url') ) {
28
+ return self_admin_url();
29
+ }
30
+ return '[' . $tag . ']';
31
+ }
32
+
33
+ /** @noinspection PhpUnusedParameterInspection */
34
+ public function handleHomeUrl($attributes = array(), $content = null, $tag = 'ame-home-url') {
35
+ if ( is_callable('home_url') ) {
36
+ return home_url();
37
+ }
38
+ return '[' . $tag . ']';
39
+ }
40
+
41
+ public function handleUserInfo(
42
+ $attributes = array(), /** @noinspection PhpUnusedParameterInspection */
43
+ $content = null,
44
+ $tag = 'ame-user-info'
45
+ ) {
46
+ $attributes = shortcode_atts(
47
+ array(
48
+ 'field' => 'user_login',
49
+ 'placeholder' => '(No user)',
50
+ 'escape' => 'auto',
51
+ ),
52
+ $attributes,
53
+ $tag
54
+ );
55
+
56
+ $placeholder = $attributes['placeholder'];
57
+
58
+ $field = strtolower($attributes['field']);
59
+ if ( $field === 'id' ) {
60
+ $field = 'ID';
61
+ }
62
+ if ( !in_array($field, self::$allowedUserFields) ) {
63
+ return '(Error: Unsupported field)';
64
+ }
65
+
66
+ //Get the currently logged-in user.
67
+ $user = null;
68
+ if ( is_callable('wp_get_current_user') ) {
69
+ $user = wp_get_current_user();
70
+ }
71
+
72
+ //Display the placeholder text if nobody is logged in or the user doesn't exist.
73
+ if ( empty($user) || !isset($user->ID) || ($user->ID === 0) ) {
74
+ return $placeholder;
75
+ }
76
+
77
+ $escapingHandlers = array(
78
+ 'html' => 'esc_html',
79
+ 'attr' => 'esc_attr',
80
+ 'js' => 'esc_js',
81
+ 'none' => array($this, 'identity'),
82
+ );
83
+
84
+ $escape = $attributes['escape'];
85
+ //By default, escape HTML special characters only if in the Loop.
86
+ if ( $escape === 'auto' ) {
87
+ if ( is_callable('in_the_loop') && in_the_loop() ) {
88
+ $escape = 'html';
89
+ } else {
90
+ $escape = 'none';
91
+ }
92
+ }
93
+ if ( !array_key_exists($escape, $escapingHandlers) ) {
94
+ return '(Error: Unsupported escape setting)';
95
+ }
96
+ if ( is_callable($escapingHandlers[$escape]) ) {
97
+ $escapeCallback = $escapingHandlers[$escape];
98
+ } else {
99
+ return '(Error: The specified escape function is not available)';
100
+ }
101
+
102
+ if ( isset($user->$field) ) {
103
+ return call_user_func($escapeCallback, $user->$field);
104
+ }
105
+ return $placeholder;
106
+ }
107
+
108
+ protected function identity($value) {
109
+ return $value;
110
+ }
111
+ }
112
+
113
+ $wsAmeShortcodes = new ameCoreShortcodes();
114
+ $wsAmeShortcodes->register();
js/actor-manager.js CHANGED
@@ -9,6 +9,8 @@ var __extends = (this && this.__extends) || (function () {
9
  return extendStatics(d, b);
10
  };
11
  return function (d, b) {
 
 
12
  extendStatics(d, b);
13
  function __() { this.constructor = d; }
14
  d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
9
  return extendStatics(d, b);
10
  };
11
  return function (d, b) {
12
+ if (typeof b !== "function" && b !== null)
13
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
14
  extendStatics(d, b);
15
  function __() { this.constructor = d; }
16
  d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
js/editor-tab-fix.js DELETED
@@ -1,11 +0,0 @@
1
- jQuery(function($) {
2
- //On AME pages, move settings tabs after the heading. This is necessary to make them appear on the right side,
3
- //and WordPress breaks that by moving notices like "Settings saved" after the first H1 (see common.js).
4
- var menuEditorHeading = $('#ws_ame_editor_heading').first(),
5
- menuEditorTabs = $('.nav-tab-wrapper').first();
6
- menuEditorTabs = menuEditorTabs.add(menuEditorTabs.next('.clear'));
7
- if ((menuEditorHeading.length > 0) && (menuEditorTabs.length > 0)) {
8
- menuEditorTabs.insertAfter(menuEditorHeading);
9
- }
10
- });
11
-
 
 
 
 
 
 
 
 
 
 
 
js/jqueryui.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- // Type definitions for jQueryUI 1.9
2
  // Project: http://jqueryui.com/
3
- // Definitions by: Boris Yankov <https://github.com/borisyankov/>, John Reilly <https://github.com/johnnyreilly>
4
  // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
 
5
 
6
-
7
- /// <reference path="jquery.d.ts"/>
8
 
9
  declare namespace JQueryUI {
10
  // Accordion //////////////////////////////////////////////////
@@ -12,11 +12,11 @@ declare namespace JQueryUI {
12
  interface AccordionOptions extends AccordionEvents {
13
  active?: any; // boolean or number
14
  animate?: any; // boolean, number, string or object
15
- collapsible?: boolean;
16
- disabled?: boolean;
17
- event?: string;
18
- header?: string;
19
- heightStyle?: string;
20
  icons?: any;
21
  }
22
 
@@ -28,13 +28,13 @@ declare namespace JQueryUI {
28
  }
29
 
30
  interface AccordionEvent {
31
- (event: Event, ui: AccordionUIParams): void;
32
  }
33
 
34
  interface AccordionEvents {
35
- activate?: AccordionEvent;
36
- beforeActivate?: AccordionEvent;
37
- create?: AccordionEvent;
38
  }
39
 
40
  interface Accordion extends Widget, AccordionOptions {
@@ -45,12 +45,18 @@ declare namespace JQueryUI {
45
 
46
  interface AutocompleteOptions extends AutocompleteEvents {
47
  appendTo?: any; //Selector;
48
- autoFocus?: boolean;
49
- delay?: number;
50
- disabled?: boolean;
51
- minLength?: number;
52
  position?: any; // object
53
  source?: any; // [], string or ()
 
 
 
 
 
 
54
  }
55
 
56
  interface AutocompleteUIParams {
@@ -58,21 +64,22 @@ declare namespace JQueryUI {
58
  * The item selected from the menu, if any. Otherwise the property is null
59
  */
60
  item?: any;
 
61
  }
62
 
63
  interface AutocompleteEvent {
64
- (event: Event, ui: AutocompleteUIParams): void;
65
  }
66
 
67
  interface AutocompleteEvents {
68
- change?: AutocompleteEvent;
69
- close?: AutocompleteEvent;
70
- create?: AutocompleteEvent;
71
- focus?: AutocompleteEvent;
72
- open?: AutocompleteEvent;
73
- response?: AutocompleteEvent;
74
- search?: AutocompleteEvent;
75
- select?: AutocompleteEvent;
76
  }
77
 
78
  interface Autocomplete extends Widget, AutocompleteOptions {
@@ -84,11 +91,11 @@ declare namespace JQueryUI {
84
  // Button //////////////////////////////////////////////////
85
 
86
  interface ButtonOptions {
87
- disabled?: boolean;
88
  icons?: any;
89
- label?: string;
90
- text?: string|boolean;
91
- click?: (event?: Event) => void;
92
  }
93
 
94
  interface Button extends Widget, ButtonOptions {
@@ -105,19 +112,19 @@ declare namespace JQueryUI {
105
  /**
106
  * The dateFormat to be used for the altField option. This allows one date format to be shown to the user for selection purposes, while a different format is actually sent behind the scenes. For a full list of the possible formats see the formatDate function
107
  */
108
- altFormat?: string;
109
  /**
110
  * The text to display after each date field, e.g., to show the required format.
111
  */
112
- appendText?: string;
113
  /**
114
  * Set to true to automatically resize the input field to accommodate dates in the current dateFormat.
115
  */
116
- autoSize?: boolean;
117
  /**
118
  * A function that takes an input field and current datepicker instance and returns an options object to update the datepicker with. It is called just before the datepicker is displayed.
119
  */
120
- beforeShow?: (input: Element, inst: any) => JQueryUI.DatepickerOptions;
121
  /**
122
  * A function that takes a date as a parameter and must return an array with:
123
  * [0]: true/false indicating whether or not this date is selectable
@@ -125,59 +132,59 @@ declare namespace JQueryUI {
125
  * [2]: an optional popup tooltip for this date
126
  * The function is called for each day in the datepicker before it is displayed.
127
  */
128
- beforeShowDay?: (date: Date) => any[];
129
  /**
130
  * A URL of an image to use to display the datepicker when the showOn option is set to "button" or "both". If set, the buttonText option becomes the alt value and is not directly displayed.
131
  */
132
- buttonImage?: string;
133
  /**
134
  * Whether the button image should be rendered by itself instead of inside a button element. This option is only relevant if the buttonImage option has also been set.
135
  */
136
- buttonImageOnly?: boolean;
137
  /**
138
  * The text to display on the trigger button. Use in conjunction with the showOn option set to "button" or "both".
139
  */
140
- buttonText?: string;
141
  /**
142
  * A function to calculate the week of the year for a given date. The default implementation uses the ISO 8601 definition: weeks start on a Monday; the first week of the year contains the first Thursday of the year.
143
  */
144
- calculateWeek?: (date: Date) => string;
145
  /**
146
  * Whether the month should be rendered as a dropdown instead of text.
147
  */
148
- changeMonth?: boolean;
149
  /**
150
  * Whether the year should be rendered as a dropdown instead of text. Use the yearRange option to control which years are made available for selection.
151
  */
152
- changeYear?: boolean;
153
  /**
154
  * The text to display for the close link. Use the showButtonPanel option to display this button.
155
  */
156
- closeText?: string;
157
  /**
158
  * When true, entry in the input field is constrained to those characters allowed by the current dateFormat option.
159
  */
160
- constrainInput?: boolean;
161
  /**
162
  * The text to display for the current day link. Use the showButtonPanel option to display this button.
163
  */
164
- currentText?: string;
165
  /**
166
  * The format for parsed and displayed dates. For a full list of the possible formats see the formatDate function.
167
  */
168
- dateFormat?: string;
169
  /**
170
  * The list of long day names, starting from Sunday, for use as requested via the dateFormat option.
171
  */
172
- dayNames?: string[];
173
  /**
174
  * The list of minimised day names, starting from Sunday, for use as column headers within the datepicker.
175
  */
176
- dayNamesMin?: string[];
177
  /**
178
  * The list of abbreviated day names, starting from Sunday, for use as requested via the dateFormat option.
179
  */
180
- dayNamesShort?: string[];
181
  /**
182
  * Set the date to highlight on first opening if the field is blank. Specify either an actual date via a Date object or as a string in the current dateFormat, or a number of days from today (e.g. +7) or a string of values and periods ('y' for years, 'm' for months, 'w' for weeks, 'd' for days, e.g. '+1m +7d'), or null for today.
183
  * Multiple types supported:
@@ -189,23 +196,23 @@ declare namespace JQueryUI {
189
  /**
190
  * Control the speed at which the datepicker appears, it may be a time in milliseconds or a string representing one of the three predefined speeds ("slow", "normal", "fast").
191
  */
192
- duration?: string;
193
  /**
194
  * Set the first day of the week: Sunday is 0, Monday is 1, etc.
195
  */
196
- firstDay?: number;
197
  /**
198
  * When true, the current day link moves to the currently selected date instead of today.
199
  */
200
- gotoCurrent?: boolean;
201
  /**
202
  * Normally the previous and next links are disabled when not applicable (see the minDate and maxDate options). You can hide them altogether by setting this attribute to true.
203
  */
204
- hideIfNoPrevNext?: boolean;
205
  /**
206
  * Whether the current language is drawn from right to left.
207
  */
208
- isRTL?: boolean;
209
  /**
210
  * The maximum selectable date. When set to null, there is no maximum.
211
  * Multiple types supported:
@@ -225,19 +232,19 @@ declare namespace JQueryUI {
225
  /**
226
  * The list of full month names, for use as requested via the dateFormat option.
227
  */
228
- monthNames?: string[];
229
  /**
230
  * The list of abbreviated month names, as used in the month header on each datepicker and as requested via the dateFormat option.
231
  */
232
- monthNamesShort?: string[];
233
  /**
234
  * Whether the prevText and nextText options should be parsed as dates by the formatDate function, allowing them to display the target month names for example.
235
  */
236
- navigationAsDateFormat?: boolean;
237
  /**
238
  * The text to display for the next month link. With the standard ThemeRoller styling, this value is replaced by an icon.
239
  */
240
- nextText?: string;
241
  /**
242
  * The number of months to show at once.
243
  * Multiple types supported:
@@ -248,23 +255,23 @@ declare namespace JQueryUI {
248
  /**
249
  * Called when the datepicker moves to a new month and/or year. The function receives the selected year, month (1-12), and the datepicker instance as parameters. this refers to the associated input field.
250
  */
251
- onChangeMonthYear?: (year: number, month: number, inst: any) => void;
252
  /**
253
  * Called when the datepicker is closed, whether or not a date is selected. The function receives the selected date as text ("" if none) and the datepicker instance as parameters. this refers to the associated input field.
254
  */
255
- onClose?: (dateText: string, inst: any) => void;
256
  /**
257
  * Called when the datepicker is selected. The function receives the selected date as text and the datepicker instance as parameters. this refers to the associated input field.
258
  */
259
- onSelect?: (dateText: string, inst: any) => void;
260
  /**
261
  * The text to display for the previous month link. With the standard ThemeRoller styling, this value is replaced by an icon.
262
  */
263
- prevText?: string;
264
  /**
265
  * Whether days in other months shown before or after the current month are selectable. This only applies if the showOtherMonths option is set to true.
266
  */
267
- selectOtherMonths?: boolean;
268
  /**
269
  * The cutoff year for determining the century for a date (used in conjunction with dateFormat 'y'). Any dates entered with a year value less than or equal to the cutoff year are considered to be in the current century, while those greater than it are deemed to be in the previous century.
270
  * Multiple types supported:
@@ -275,23 +282,23 @@ declare namespace JQueryUI {
275
  /**
276
  * The name of the animation used to show and hide the datepicker. Use "show" (the default), "slideDown", "fadeIn", any of the jQuery UI effects. Set to an empty string to disable animation.
277
  */
278
- showAnim?: string;
279
  /**
280
  * Whether to display a button pane underneath the calendar. The button pane contains two buttons, a Today button that links to the current day, and a Done button that closes the datepicker. The buttons' text can be customized using the currentText and closeText options respectively.
281
  */
282
- showButtonPanel?: boolean;
283
  /**
284
  * When displaying multiple months via the numberOfMonths option, the showCurrentAtPos option defines which position to display the current month in.
285
  */
286
- showCurrentAtPos?: number;
287
  /**
288
  * Whether to show the month after the year in the header.
289
  */
290
- showMonthAfterYear?: boolean;
291
  /**
292
  * When the datepicker should appear. The datepicker can appear when the field receives focus ("focus"), when a button is clicked ("button"), or when either event occurs ("both").
293
  */
294
- showOn?: string;
295
  /**
296
  * If using one of the jQuery UI effects for the showAnim option, you can provide additional settings for that animation via this option.
297
  */
@@ -299,34 +306,42 @@ declare namespace JQueryUI {
299
  /**
300
  * Whether to display dates in other months (non-selectable) at the start or end of the current month. To make these days selectable use the selectOtherMonths option.
301
  */
302
- showOtherMonths?: boolean;
303
  /**
304
  * When true, a column is added to show the week of the year. The calculateWeek option determines how the week of the year is calculated. You may also want to change the firstDay option.
305
  */
306
- showWeek?: boolean;
307
  /**
308
  * Set how many months to move when clicking the previous/next links.
309
  */
310
- stepMonths?: number;
311
  /**
312
  * The text to display for the week of the year column heading. Use the showWeek option to display this column.
313
  */
314
- weekHeader?: string;
315
  /**
316
  * The range of years displayed in the year drop-down: either relative to today's year ("-nn:+nn"), relative to the currently selected year ("c-nn:c+nn"), absolute ("nnnn:nnnn"), or combinations of these formats ("nnnn:-nn"). Note that this option only affects what appears in the drop-down, to restrict which dates may be selected use the minDate and/or maxDate options.
317
  */
318
- yearRange?: string;
319
  /**
320
  * Additional text to display after the year in the month headers.
321
  */
322
- yearSuffix?: string;
 
 
 
 
 
 
 
 
323
  }
324
 
325
  interface DatepickerFormatDateOptions {
326
- dayNamesShort?: string[];
327
- dayNames?: string[];
328
- monthNamesShort?: string[];
329
- monthNames?: string[];
330
  }
331
 
332
  interface Datepicker extends Widget, DatepickerOptions {
@@ -342,67 +357,82 @@ declare namespace JQueryUI {
342
  // Dialog //////////////////////////////////////////////////
343
 
344
  interface DialogOptions extends DialogEvents {
345
- autoOpen?: boolean;
346
- buttons?: { [buttonText: string]: (event?: Event) => void } | DialogButtonOptions[];
347
- closeOnEscape?: boolean;
348
- closeText?: string;
349
- appendTo?: string;
350
- dialogClass?: string;
351
- disabled?: boolean;
352
- draggable?: boolean;
353
- height?: number | string;
354
- hide?: boolean | number | string | DialogShowHideOptions;
355
- maxHeight?: number;
356
- maxWidth?: number;
357
- minHeight?: number;
358
- minWidth?: number;
359
- modal?: boolean;
 
360
  position?: any; // object, string or []
361
- resizable?: boolean;
362
- show?: boolean | number | string | DialogShowHideOptions;
363
- stack?: boolean;
364
- title?: string;
365
  width?: any; // number or string
366
- zIndex?: number;
367
 
368
- open?: DialogEvent;
369
- close?: DialogEvent;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  }
371
 
372
  interface DialogButtonOptions {
373
  icons?: any;
374
- showText?: string | boolean;
375
- text?: string;
376
- click?: (eventObject: JQueryEventObject) => any;
377
  [attr: string]: any; // attributes for the <button> element
378
  }
379
 
380
  interface DialogShowHideOptions {
381
  effect: string;
382
- delay?: number;
383
- duration?: number;
384
- easing?: string;
385
  }
386
 
387
  interface DialogUIParams {
388
  }
389
 
390
  interface DialogEvent {
391
- (event: Event, ui: DialogUIParams): void;
392
  }
393
 
394
  interface DialogEvents {
395
- beforeClose?: DialogEvent;
396
- close?: DialogEvent;
397
- create?: DialogEvent;
398
- drag?: DialogEvent;
399
- dragStart?: DialogEvent;
400
- dragStop?: DialogEvent;
401
- focus?: DialogEvent;
402
- open?: DialogEvent;
403
- resize?: DialogEvent;
404
- resizeStart?: DialogEvent;
405
- resizeStop?: DialogEvent;
406
  }
407
 
408
  interface Dialog extends Widget, DialogOptions {
@@ -414,49 +444,58 @@ declare namespace JQueryUI {
414
  interface DraggableEventUIParams {
415
  helper: JQuery;
416
  position: { top: number; left: number; };
 
417
  offset: { top: number; left: number; };
418
  }
419
 
420
  interface DraggableEvent {
421
- (event: Event, ui: DraggableEventUIParams): void;
422
  }
423
 
424
  interface DraggableOptions extends DraggableEvents {
425
- disabled?: boolean;
426
- addClasses?: boolean;
427
  appendTo?: any;
428
- axis?: string;
429
- cancel?: string;
430
- connectToSortable?: string;
 
431
  containment?: any;
432
- cursor?: string;
433
  cursorAt?: any;
434
- delay?: number;
435
- distance?: number;
436
- grid?: number[];
437
  handle?: any;
438
  helper?: any;
439
  iframeFix?: any;
440
- opacity?: number;
441
- refreshPositions?: boolean;
442
  revert?: any;
443
- revertDuration?: number;
444
- scope?: string;
445
- scroll?: boolean;
446
- scrollSensitivity?: number;
447
- scrollSpeed?: number;
448
  snap?: any;
449
- snapMode?: string;
450
- snapTolerance?: number;
451
- stack?: string;
452
- zIndex?: number;
 
 
 
 
 
 
 
453
  }
454
 
455
  interface DraggableEvents {
456
- create?: DraggableEvent;
457
- start?: DraggableEvent;
458
- drag?: DraggableEvent;
459
- stop?: DraggableEvent;
460
  }
461
 
462
  interface Draggable extends Widget, DraggableOptions, DraggableEvent {
@@ -473,26 +512,27 @@ declare namespace JQueryUI {
473
  }
474
 
475
  interface DroppableEvent {
476
- (event: Event, ui: DroppableEventUIParam): void;
477
  }
478
 
479
  interface DroppableOptions extends DroppableEvents {
480
- disabled?: boolean;
481
  accept?: any;
482
- activeClass?: string;
483
- greedy?: boolean;
484
- hoverClass?: string;
485
- scope?: string;
486
- tolerance?: string;
 
 
487
  }
488
 
489
  interface DroppableEvents {
490
- create?: DroppableEvent;
491
- activate?: DroppableEvent;
492
- deactivate?: DroppableEvent;
493
- over?: DroppableEvent;
494
- out?: DroppableEvent;
495
- drop?: DroppableEvent;
496
  }
497
 
498
  interface Droppable extends Widget, DroppableOptions {
@@ -501,26 +541,26 @@ declare namespace JQueryUI {
501
  // Menu //////////////////////////////////////////////////
502
 
503
  interface MenuOptions extends MenuEvents {
504
- disabled?: boolean;
505
  icons?: any;
506
- menus?: string;
507
  position?: any; // TODO
508
- role?: string;
509
  }
510
 
511
  interface MenuUIParams {
512
- item?: JQuery;
513
  }
514
 
515
  interface MenuEvent {
516
- (event: Event, ui: MenuUIParams): void;
517
  }
518
 
519
  interface MenuEvents {
520
- blur?: MenuEvent;
521
- create?: MenuEvent;
522
- focus?: MenuEvent;
523
- select?: MenuEvent;
524
  }
525
 
526
  interface Menu extends Widget, MenuOptions {
@@ -530,22 +570,22 @@ declare namespace JQueryUI {
530
  // Progressbar //////////////////////////////////////////////////
531
 
532
  interface ProgressbarOptions extends ProgressbarEvents {
533
- disabled?: boolean;
534
- value?: number | boolean;
535
- max?: number;
536
  }
537
 
538
  interface ProgressbarUIParams {
539
  }
540
 
541
  interface ProgressbarEvent {
542
- (event: Event, ui: ProgressbarUIParams): void;
543
  }
544
 
545
  interface ProgressbarEvents {
546
- change?: ProgressbarEvent;
547
- complete?: ProgressbarEvent;
548
- create?: ProgressbarEvent;
549
  }
550
 
551
  interface Progressbar extends Widget, ProgressbarOptions {
@@ -556,24 +596,24 @@ declare namespace JQueryUI {
556
 
557
  interface ResizableOptions extends ResizableEvents {
558
  alsoResize?: any; // Selector, JQuery or Element
559
- animate?: boolean;
560
  animateDuration?: any; // number or string
561
- animateEasing?: string;
562
  aspectRatio?: any; // boolean or number
563
- autoHide?: boolean;
564
- cancel?: string;
565
  containment?: any; // Selector, Element or string
566
- delay?: number;
567
- disabled?: boolean;
568
- distance?: number;
569
- ghost?: boolean;
570
  grid?: any;
571
  handles?: any; // string or object
572
- helper?: string;
573
- maxHeight?: number;
574
- maxWidth?: number;
575
- minHeight?: number;
576
- minWidth?: number;
577
  }
578
 
579
  interface ResizableUIParams {
@@ -587,14 +627,14 @@ declare namespace JQueryUI {
587
  }
588
 
589
  interface ResizableEvent {
590
- (event: Event, ui: ResizableUIParams): void;
591
  }
592
 
593
  interface ResizableEvents {
594
- resize?: ResizableEvent;
595
- start?: ResizableEvent;
596
- stop?: ResizableEvent;
597
- create?: ResizableEvents;
598
  }
599
 
600
  interface Resizable extends Widget, ResizableOptions {
@@ -604,58 +644,111 @@ declare namespace JQueryUI {
604
  // Selectable //////////////////////////////////////////////////
605
 
606
  interface SelectableOptions extends SelectableEvents {
607
- autoRefresh?: boolean;
608
- cancel?: string;
609
- delay?: number;
610
- disabled?: boolean;
611
- distance?: number;
612
- filter?: string;
613
- tolerance?: string;
614
  }
615
 
616
  interface SelectableEvents {
617
- selected? (event: Event, ui: { selected?: Element; }): void;
618
- selecting? (event: Event, ui: { selecting?: Element; }): void;
619
- start? (event: Event, ui: any): void;
620
- stop? (event: Event, ui: any): void;
621
- unselected? (event: Event, ui: { unselected: Element; }): void;
622
- unselecting? (event: Event, ui: { unselecting: Element; }): void;
623
  }
624
 
625
  interface Selectable extends Widget, SelectableOptions {
626
  }
627
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
628
  // Slider //////////////////////////////////////////////////
629
 
630
  interface SliderOptions extends SliderEvents {
631
  animate?: any; // boolean, string or number
632
- disabled?: boolean;
633
- max?: number;
634
- min?: number;
635
- orientation?: string;
636
  range?: any; // boolean or string
637
- step?: number;
638
- value?: number;
639
- values?: number[];
640
- highlight?: boolean;
 
 
 
 
 
 
 
 
 
 
 
641
  }
642
 
643
  interface SliderUIParams {
644
- handle?: JQuery;
645
- value?: number;
646
- values?: number[];
647
  }
648
 
649
  interface SliderEvent {
650
- (event: Event, ui: SliderUIParams): void;
651
  }
652
 
653
  interface SliderEvents {
654
- change?: SliderEvent;
655
- create?: SliderEvent;
656
- slide?: SliderEvent;
657
- start?: SliderEvent;
658
- stop?: SliderEvent;
659
  }
660
 
661
  interface Slider extends Widget, SliderOptions {
@@ -666,30 +759,31 @@ declare namespace JQueryUI {
666
 
667
  interface SortableOptions extends SortableEvents {
668
  appendTo?: any; // jQuery, Element, Selector or string
669
- axis?: string;
 
670
  cancel?: any; // Selector
671
  connectWith?: any; // Selector
672
  containment?: any; // Element, Selector or string
673
- cursor?: string;
674
  cursorAt?: any;
675
- delay?: number;
676
- disabled?: boolean;
677
- distance?: number;
678
- dropOnEmpty?: boolean;
679
- forceHelperSize?: boolean;
680
- forcePlaceholderSize?: boolean;
681
- grid?: number[];
682
- helper?: string | ((event: Event, element: Sortable) => Element);
683
  handle?: any; // Selector or Element
684
  items?: any; // Selector
685
- opacity?: number;
686
- placeholder?: string;
687
  revert?: any; // boolean or number
688
- scroll?: boolean;
689
- scrollSensitivity?: number;
690
- scrollSpeed?: number;
691
- tolerance?: string;
692
- zIndex?: number;
693
  }
694
 
695
  interface SortableUIParams {
@@ -707,18 +801,18 @@ declare namespace JQueryUI {
707
  }
708
 
709
  interface SortableEvents {
710
- activate?: SortableEvent;
711
- beforeStop?: SortableEvent;
712
- change?: SortableEvent;
713
- deactivate?: SortableEvent;
714
- out?: SortableEvent;
715
- over?: SortableEvent;
716
- receive?: SortableEvent;
717
- remove?: SortableEvent;
718
- sort?: SortableEvent;
719
- start?: SortableEvent;
720
- stop?: SortableEvent;
721
- update?: SortableEvent;
722
  }
723
 
724
  interface Sortable extends Widget, SortableOptions, SortableEvents {
@@ -728,14 +822,14 @@ declare namespace JQueryUI {
728
  // Spinner //////////////////////////////////////////////////
729
 
730
  interface SpinnerOptions extends SpinnerEvents {
731
- culture?: string;
732
- disabled?: boolean;
733
  icons?: any;
734
  incremental?: any; // boolean or ()
735
  max?: any; // number or string
736
  min?: any; // number or string
737
- numberFormat?: string;
738
- page?: number;
739
  step?: any; // number or string
740
  }
741
 
@@ -744,15 +838,15 @@ declare namespace JQueryUI {
744
  }
745
 
746
  interface SpinnerEvent<T> {
747
- (event: Event, ui: T): void;
748
  }
749
 
750
  interface SpinnerEvents {
751
- change?: SpinnerEvent<{}>;
752
- create?: SpinnerEvent<{}>;
753
- spin?: SpinnerEvent<SpinnerUIParam>;
754
- start?: SpinnerEvent<{}>;
755
- stop?: SpinnerEvent<{}>;
756
  }
757
 
758
  interface Spinner extends Widget, SpinnerOptions {
@@ -763,14 +857,26 @@ declare namespace JQueryUI {
763
 
764
  interface TabsOptions extends TabsEvents {
765
  active?: any; // boolean or number
766
- collapsible?: boolean;
 
767
  disabled?: any; // boolean or []
768
- event?: string;
769
- heightStyle?: string;
770
  hide?: any; // boolean, number, string or object
771
  show?: any; // boolean, number, string or object
772
  }
773
 
 
 
 
 
 
 
 
 
 
 
 
774
  interface TabsActivationUIParams {
775
  newTab: JQuery;
776
  oldTab: JQuery;
@@ -791,15 +897,15 @@ declare namespace JQueryUI {
791
  }
792
 
793
  interface TabsEvent<UI> {
794
- (event: Event, ui: UI): void;
795
  }
796
 
797
  interface TabsEvents {
798
- activate?: TabsEvent<TabsActivationUIParams>;
799
- beforeActivate?: TabsEvent<TabsActivationUIParams>;
800
- beforeLoad?: TabsEvent<TabsBeforeLoadUIParams>;
801
- load?: TabsEvent<TabsCreateOrLoadUIParams>;
802
- create?: TabsEvent<TabsCreateOrLoadUIParams>;
803
  }
804
 
805
  interface Tabs extends Widget, TabsOptions {
@@ -810,25 +916,26 @@ declare namespace JQueryUI {
810
 
811
  interface TooltipOptions extends TooltipEvents {
812
  content?: any; // () or string
813
- disabled?: boolean;
814
  hide?: any; // boolean, number, string or object
815
- items?: string;
816
  position?: any; // TODO
817
  show?: any; // boolean, number, string or object
818
- tooltipClass?: string;
819
- track?: boolean;
 
820
  }
821
 
822
  interface TooltipUIParams {
823
  }
824
 
825
  interface TooltipEvent {
826
- (event: Event, ui: TooltipUIParams): void;
827
  }
828
 
829
  interface TooltipEvents {
830
- close?: TooltipEvent;
831
- open?: TooltipEvent;
832
  }
833
 
834
  interface Tooltip extends Widget, TooltipOptions {
@@ -839,86 +946,86 @@ declare namespace JQueryUI {
839
 
840
  interface EffectOptions {
841
  effect: string;
842
- easing?: string;
843
- duration?: number;
844
  complete: Function;
845
  }
846
 
847
  interface BlindEffect {
848
- direction?: string;
849
  }
850
 
851
  interface BounceEffect {
852
- distance?: number;
853
- times?: number;
854
  }
855
 
856
  interface ClipEffect {
857
- direction?: number;
858
  }
859
 
860
  interface DropEffect {
861
- direction?: number;
862
  }
863
 
864
  interface ExplodeEffect {
865
- pieces?: number;
866
  }
867
 
868
  interface FadeEffect { }
869
 
870
  interface FoldEffect {
871
  size?: any;
872
- horizFirst?: boolean;
873
  }
874
 
875
  interface HighlightEffect {
876
- color?: string;
877
  }
878
 
879
  interface PuffEffect {
880
- percent?: number;
881
  }
882
 
883
  interface PulsateEffect {
884
- times?: number;
885
  }
886
 
887
  interface ScaleEffect {
888
- direction?: string;
889
- origin?: string[];
890
- percent?: number;
891
- scale?: string;
892
  }
893
 
894
  interface ShakeEffect {
895
- direction?: string;
896
- distance?: number;
897
- times?: number;
898
  }
899
 
900
  interface SizeEffect {
901
  to?: any;
902
- origin?: string[];
903
- scale?: string;
904
  }
905
 
906
  interface SlideEffect {
907
- direction?: string;
908
- distance?: number;
909
  }
910
 
911
  interface TransferEffect {
912
- className?: string;
913
- to?: string;
914
  }
915
 
916
  interface JQueryPositionOptions {
917
- my?: string;
918
- at?: string;
919
  of?: any;
920
- collision?: string;
921
- using?: Function;
922
  within?: any;
923
  }
924
 
@@ -926,9 +1033,9 @@ declare namespace JQueryUI {
926
  // UI //////////////////////////////////////////////////
927
 
928
  interface MouseOptions {
929
- cancel?: string;
930
- delay?: number;
931
- distance?: number;
932
  }
933
 
934
  interface KeyCode {
@@ -971,6 +1078,7 @@ declare namespace JQueryUI {
971
  keyCode: KeyCode;
972
  menu: Menu;
973
  progressbar: Progressbar;
 
974
  slider: Slider;
975
  spinner: Spinner;
976
  tabs: Tabs;
@@ -982,11 +1090,22 @@ declare namespace JQueryUI {
982
  // Widget //////////////////////////////////////////////////
983
 
984
  interface WidgetOptions {
985
- disabled?: boolean;
986
  hide?: any;
987
  show?: any;
988
  }
989
 
 
 
 
 
 
 
 
 
 
 
 
990
  interface Widget {
991
  (methodName: string): JQuery;
992
  (options: WidgetOptions): JQuery;
@@ -995,8 +1114,8 @@ declare namespace JQueryUI {
995
  (optionLiteral: string, options: WidgetOptions): any;
996
  (optionLiteral: string, optionName: string, optionValue: any): JQuery;
997
 
998
- (name: string, prototype: any): JQuery;
999
- (name: string, base: Function, prototype: any): JQuery;
1000
  }
1001
 
1002
  ////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -1310,6 +1429,23 @@ interface JQuery {
1310
  * @param optionName 'buttonText'
1311
  */
1312
  datepicker(methodName: 'option', optionName: 'buttonText'): string;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1313
  /**
1314
  * Set the buttonText option, after initialization
1315
  *
@@ -1675,6 +1811,22 @@ interface JQuery {
1675
  selectable(optionLiteral: string, options: JQueryUI.SelectableOptions): any;
1676
  selectable(optionLiteral: string, optionName: string, optionValue: any): JQuery;
1677
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1678
  slider(): JQuery;
1679
  slider(methodName: 'destroy'): void;
1680
  slider(methodName: 'disable'): void;
@@ -1700,11 +1852,11 @@ interface JQuery {
1700
  sortable(methodName: 'disable'): void;
1701
  sortable(methodName: 'enable'): void;
1702
  sortable(methodName: 'widget'): JQuery;
1703
- sortable(methodName: 'toArray'): string[];
1704
  sortable(methodName: string): JQuery;
1705
  sortable(options: JQueryUI.SortableOptions): JQuery;
1706
  sortable(optionLiteral: string, optionName: string): any;
1707
- sortable(methodName: 'serialize', options?: { key?: string; attribute?: string; expression?: RegExp }): string;
1708
  sortable(optionLiteral: string, options: JQueryUI.SortableOptions): any;
1709
  sortable(optionLiteral: string, optionName: string, optionValue: any): JQuery;
1710
 
@@ -1728,10 +1880,13 @@ interface JQuery {
1728
  tabs(): JQuery;
1729
  tabs(methodName: 'destroy'): void;
1730
  tabs(methodName: 'disable'): void;
 
1731
  tabs(methodName: 'enable'): void;
 
1732
  tabs(methodName: 'load', index: number): void;
1733
  tabs(methodName: 'refresh'): void;
1734
  tabs(methodName: 'widget'): JQuery;
 
1735
  tabs(methodName: string): JQuery;
1736
  tabs(options: JQueryUI.TabsOptions): JQuery;
1737
  tabs(optionLiteral: string, optionName: string): any;
@@ -1752,39 +1907,39 @@ interface JQuery {
1752
  tooltip(optionLiteral: string, optionName: string, optionValue: any): JQuery;
1753
 
1754
 
1755
- addClass(classNames: string, speed?: number, callback?: Function): JQuery;
1756
- addClass(classNames: string, speed?: string, callback?: Function): JQuery;
1757
- addClass(classNames: string, speed?: number, easing?: string, callback?: Function): JQuery;
1758
- addClass(classNames: string, speed?: string, easing?: string, callback?: Function): JQuery;
1759
 
1760
- removeClass(classNames: string, speed?: number, callback?: Function): JQuery;
1761
- removeClass(classNames: string, speed?: string, callback?: Function): JQuery;
1762
- removeClass(classNames: string, speed?: number, easing?: string, callback?: Function): JQuery;
1763
- removeClass(classNames: string, speed?: string, easing?: string, callback?: Function): JQuery;
1764
 
1765
- switchClass(removeClassName: string, addClassName: string, duration?: number, easing?: string, complete?: Function): JQuery;
1766
- switchClass(removeClassName: string, addClassName: string, duration?: string, easing?: string, complete?: Function): JQuery;
1767
 
1768
- toggleClass(className: string, duration?: number, easing?: string, complete?: Function): JQuery;
1769
- toggleClass(className: string, duration?: string, easing?: string, complete?: Function): JQuery;
1770
- toggleClass(className: string, aswitch?: boolean, duration?: number, easing?: string, complete?: Function): JQuery;
1771
- toggleClass(className: string, aswitch?: boolean, duration?: string, easing?: string, complete?: Function): JQuery;
1772
 
1773
- effect(options: any): JQuery;
1774
- effect(effect: string, options?: any, duration?: number, complete?: Function): JQuery;
1775
- effect(effect: string, options?: any, duration?: string, complete?: Function): JQuery;
1776
 
1777
- hide(options: any): JQuery;
1778
- hide(effect: string, options?: any, duration?: number, complete?: Function): JQuery;
1779
- hide(effect: string, options?: any, duration?: string, complete?: Function): JQuery;
1780
 
1781
- show(options: any): JQuery;
1782
- show(effect: string, options?: any, duration?: number, complete?: Function): JQuery;
1783
- show(effect: string, options?: any, duration?: string, complete?: Function): JQuery;
1784
 
1785
- toggle(options: any): JQuery;
1786
- toggle(effect: string, options?: any, duration?: number, complete?: Function): JQuery;
1787
- toggle(effect: string, options?: any, duration?: string, complete?: Function): JQuery;
1788
 
1789
  position(options: JQueryUI.JQueryPositionOptions): JQuery;
1790
 
@@ -1794,7 +1949,7 @@ interface JQuery {
1794
  uniqueId(): JQuery;
1795
  removeUniqueId(): JQuery;
1796
  scrollParent(): JQuery;
1797
- zIndex(): JQuery;
1798
  zIndex(zIndex: number): JQuery;
1799
 
1800
  widget: JQueryUI.Widget;
1
+ // Type definitions for jQueryUI 1.12
2
  // Project: http://jqueryui.com/
3
+ // Definitions by: Boris Yankov <https://github.com/borisyankov>, John Reilly <https://github.com/johnnyreilly>, Dieter Oberkofler <https://github.com/doberkofler>
4
  // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5
+ // TypeScript Version: 2.3
6
 
7
+ /// <reference path="jquery.d.ts" />
 
8
 
9
  declare namespace JQueryUI {
10
  // Accordion //////////////////////////////////////////////////
12
  interface AccordionOptions extends AccordionEvents {
13
  active?: any; // boolean or number
14
  animate?: any; // boolean, number, string or object
15
+ collapsible?: boolean | undefined;
16
+ disabled?: boolean | undefined;
17
+ event?: string | undefined;
18
+ header?: string | undefined;
19
+ heightStyle?: string | undefined;
20
  icons?: any;
21
  }
22
 
28
  }
29
 
30
  interface AccordionEvent {
31
+ (event: JQueryEventObject, ui: AccordionUIParams): void;
32
  }
33
 
34
  interface AccordionEvents {
35
+ activate?: AccordionEvent | undefined;
36
+ beforeActivate?: AccordionEvent | undefined;
37
+ create?: AccordionEvent | undefined;
38
  }
39
 
40
  interface Accordion extends Widget, AccordionOptions {
45
 
46
  interface AutocompleteOptions extends AutocompleteEvents {
47
  appendTo?: any; //Selector;
48
+ autoFocus?: boolean | undefined;
49
+ delay?: number | undefined;
50
+ disabled?: boolean | undefined;
51
+ minLength?: number | undefined;
52
  position?: any; // object
53
  source?: any; // [], string or ()
54
+ classes?: AutocompleteClasses | undefined;
55
+ }
56
+
57
+ interface AutocompleteClasses {
58
+ "ui-autocomplete"?: string | undefined;
59
+ "ui-autocomplete-input"?: string | undefined;
60
  }
61
 
62
  interface AutocompleteUIParams {
64
  * The item selected from the menu, if any. Otherwise the property is null
65
  */
66
  item?: any;
67
+ content?: any;
68
  }
69
 
70
  interface AutocompleteEvent {
71
+ (event: JQueryEventObject, ui: AutocompleteUIParams): void;
72
  }
73
 
74
  interface AutocompleteEvents {
75
+ change?: AutocompleteEvent | undefined;
76
+ close?: AutocompleteEvent | undefined;
77
+ create?: AutocompleteEvent | undefined;
78
+ focus?: AutocompleteEvent | undefined;
79
+ open?: AutocompleteEvent | undefined;
80
+ response?: AutocompleteEvent | undefined;
81
+ search?: AutocompleteEvent | undefined;
82
+ select?: AutocompleteEvent | undefined;
83
  }
84
 
85
  interface Autocomplete extends Widget, AutocompleteOptions {
91
  // Button //////////////////////////////////////////////////
92
 
93
  interface ButtonOptions {
94
+ disabled?: boolean | undefined;
95
  icons?: any;
96
+ label?: string | undefined;
97
+ text?: string|boolean | undefined;
98
+ click?: ((event?: Event) => void) | undefined;
99
  }
100
 
101
  interface Button extends Widget, ButtonOptions {
112
  /**
113
  * The dateFormat to be used for the altField option. This allows one date format to be shown to the user for selection purposes, while a different format is actually sent behind the scenes. For a full list of the possible formats see the formatDate function
114
  */
115
+ altFormat?: string | undefined;
116
  /**
117
  * The text to display after each date field, e.g., to show the required format.
118
  */
119
+ appendText?: string | undefined;
120
  /**
121
  * Set to true to automatically resize the input field to accommodate dates in the current dateFormat.
122
  */
123
+ autoSize?: boolean | undefined;
124
  /**
125
  * A function that takes an input field and current datepicker instance and returns an options object to update the datepicker with. It is called just before the datepicker is displayed.
126
  */
127
+ beforeShow?: ((input: Element, inst: any) => JQueryUI.DatepickerOptions) | undefined;
128
  /**
129
  * A function that takes a date as a parameter and must return an array with:
130
  * [0]: true/false indicating whether or not this date is selectable
132
  * [2]: an optional popup tooltip for this date
133
  * The function is called for each day in the datepicker before it is displayed.
134
  */
135
+ beforeShowDay?: ((date: Date) => any[]) | undefined;
136
  /**
137
  * A URL of an image to use to display the datepicker when the showOn option is set to "button" or "both". If set, the buttonText option becomes the alt value and is not directly displayed.
138
  */
139
+ buttonImage?: string | undefined;
140
  /**
141
  * Whether the button image should be rendered by itself instead of inside a button element. This option is only relevant if the buttonImage option has also been set.
142
  */
143
+ buttonImageOnly?: boolean | undefined;
144
  /**
145
  * The text to display on the trigger button. Use in conjunction with the showOn option set to "button" or "both".
146
  */
147
+ buttonText?: string | undefined;
148
  /**
149
  * A function to calculate the week of the year for a given date. The default implementation uses the ISO 8601 definition: weeks start on a Monday; the first week of the year contains the first Thursday of the year.
150
  */
151
+ calculateWeek?: ((date: Date) => string) | undefined;
152
  /**
153
  * Whether the month should be rendered as a dropdown instead of text.
154
  */
155
+ changeMonth?: boolean | undefined;
156
  /**
157
  * Whether the year should be rendered as a dropdown instead of text. Use the yearRange option to control which years are made available for selection.
158
  */
159
+ changeYear?: boolean | undefined;
160
  /**
161
  * The text to display for the close link. Use the showButtonPanel option to display this button.
162
  */
163
+ closeText?: string | undefined;
164
  /**
165
  * When true, entry in the input field is constrained to those characters allowed by the current dateFormat option.
166
  */
167
+ constrainInput?: boolean | undefined;
168
  /**
169
  * The text to display for the current day link. Use the showButtonPanel option to display this button.
170
  */
171
+ currentText?: string | undefined;
172
  /**
173
  * The format for parsed and displayed dates. For a full list of the possible formats see the formatDate function.
174
  */
175
+ dateFormat?: string | undefined;
176
  /**
177
  * The list of long day names, starting from Sunday, for use as requested via the dateFormat option.
178
  */
179
+ dayNames?: string[] | undefined;
180
  /**
181
  * The list of minimised day names, starting from Sunday, for use as column headers within the datepicker.
182
  */
183
+ dayNamesMin?: string[] | undefined;
184
  /**
185
  * The list of abbreviated day names, starting from Sunday, for use as requested via the dateFormat option.
186
  */
187
+ dayNamesShort?: string[] | undefined;
188
  /**
189
  * Set the date to highlight on first opening if the field is blank. Specify either an actual date via a Date object or as a string in the current dateFormat, or a number of days from today (e.g. +7) or a string of values and periods ('y' for years, 'm' for months, 'w' for weeks, 'd' for days, e.g. '+1m +7d'), or null for today.
190
  * Multiple types supported:
196
  /**
197
  * Control the speed at which the datepicker appears, it may be a time in milliseconds or a string representing one of the three predefined speeds ("slow", "normal", "fast").
198
  */
199
+ duration?: string | undefined;
200
  /**
201
  * Set the first day of the week: Sunday is 0, Monday is 1, etc.
202
  */
203
+ firstDay?: number | undefined;
204
  /**
205
  * When true, the current day link moves to the currently selected date instead of today.
206
  */
207
+ gotoCurrent?: boolean | undefined;
208
  /**
209
  * Normally the previous and next links are disabled when not applicable (see the minDate and maxDate options). You can hide them altogether by setting this attribute to true.
210
  */
211
+ hideIfNoPrevNext?: boolean | undefined;
212
  /**
213
  * Whether the current language is drawn from right to left.
214
  */
215
+ isRTL?: boolean | undefined;
216
  /**
217
  * The maximum selectable date. When set to null, there is no maximum.
218
  * Multiple types supported:
232
  /**
233
  * The list of full month names, for use as requested via the dateFormat option.
234
  */
235
+ monthNames?: string[] | undefined;
236
  /**
237
  * The list of abbreviated month names, as used in the month header on each datepicker and as requested via the dateFormat option.
238
  */
239
+ monthNamesShort?: string[] | undefined;
240
  /**
241
  * Whether the prevText and nextText options should be parsed as dates by the formatDate function, allowing them to display the target month names for example.
242
  */
243
+ navigationAsDateFormat?: boolean | undefined;
244
  /**
245
  * The text to display for the next month link. With the standard ThemeRoller styling, this value is replaced by an icon.
246
  */
247
+ nextText?: string | undefined;
248
  /**
249
  * The number of months to show at once.
250
  * Multiple types supported:
255
  /**
256
  * Called when the datepicker moves to a new month and/or year. The function receives the selected year, month (1-12), and the datepicker instance as parameters. this refers to the associated input field.
257
  */
258
+ onChangeMonthYear?: ((year: number, month: number, inst: any) => void) | undefined;
259
  /**
260
  * Called when the datepicker is closed, whether or not a date is selected. The function receives the selected date as text ("" if none) and the datepicker instance as parameters. this refers to the associated input field.
261
  */
262
+ onClose?: ((dateText: string, inst: any) => void) | undefined;
263
  /**
264
  * Called when the datepicker is selected. The function receives the selected date as text and the datepicker instance as parameters. this refers to the associated input field.
265
  */
266
+ onSelect?: ((dateText: string, inst: any) => void) | undefined;
267
  /**
268
  * The text to display for the previous month link. With the standard ThemeRoller styling, this value is replaced by an icon.
269
  */
270
+ prevText?: string | undefined;
271
  /**
272
  * Whether days in other months shown before or after the current month are selectable. This only applies if the showOtherMonths option is set to true.
273
  */
274
+ selectOtherMonths?: boolean | undefined;
275
  /**
276
  * The cutoff year for determining the century for a date (used in conjunction with dateFormat 'y'). Any dates entered with a year value less than or equal to the cutoff year are considered to be in the current century, while those greater than it are deemed to be in the previous century.
277
  * Multiple types supported:
282
  /**
283
  * The name of the animation used to show and hide the datepicker. Use "show" (the default), "slideDown", "fadeIn", any of the jQuery UI effects. Set to an empty string to disable animation.
284
  */
285
+ showAnim?: string | undefined;
286
  /**
287
  * Whether to display a button pane underneath the calendar. The button pane contains two buttons, a Today button that links to the current day, and a Done button that closes the datepicker. The buttons' text can be customized using the currentText and closeText options respectively.
288
  */
289
+ showButtonPanel?: boolean | undefined;
290
  /**
291
  * When displaying multiple months via the numberOfMonths option, the showCurrentAtPos option defines which position to display the current month in.
292
  */
293
+ showCurrentAtPos?: number | undefined;
294
  /**
295
  * Whether to show the month after the year in the header.
296
  */
297
+ showMonthAfterYear?: boolean | undefined;
298
  /**
299
  * When the datepicker should appear. The datepicker can appear when the field receives focus ("focus"), when a button is clicked ("button"), or when either event occurs ("both").
300
  */
301
+ showOn?: string | undefined;
302
  /**
303
  * If using one of the jQuery UI effects for the showAnim option, you can provide additional settings for that animation via this option.
304
  */
306
  /**
307
  * Whether to display dates in other months (non-selectable) at the start or end of the current month. To make these days selectable use the selectOtherMonths option.
308
  */
309
+ showOtherMonths?: boolean | undefined;
310
  /**
311
  * When true, a column is added to show the week of the year. The calculateWeek option determines how the week of the year is calculated. You may also want to change the firstDay option.
312
  */
313
+ showWeek?: boolean | undefined;
314
  /**
315
  * Set how many months to move when clicking the previous/next links.
316
  */
317
+ stepMonths?: number | undefined;
318
  /**
319
  * The text to display for the week of the year column heading. Use the showWeek option to display this column.
320
  */
321
+ weekHeader?: string | undefined;
322
  /**
323
  * The range of years displayed in the year drop-down: either relative to today's year ("-nn:+nn"), relative to the currently selected year ("c-nn:c+nn"), absolute ("nnnn:nnnn"), or combinations of these formats ("nnnn:-nn"). Note that this option only affects what appears in the drop-down, to restrict which dates may be selected use the minDate and/or maxDate options.
324
  */
325
+ yearRange?: string | undefined;
326
  /**
327
  * Additional text to display after the year in the month headers.
328
  */
329
+ yearSuffix?: string | undefined;
330
+ /**
331
+ * Set to true to automatically hide the datepicker.
332
+ */
333
+ autohide?: boolean | undefined;
334
+ /**
335
+ * Set to date to automatically enddate the datepicker.
336
+ */
337
+ endDate?: Date | undefined;
338
  }
339
 
340
  interface DatepickerFormatDateOptions {
341
+ dayNamesShort?: string[] | undefined;
342
+ dayNames?: string[] | undefined;
343
+ monthNamesShort?: string[] | undefined;
344
+ monthNames?: string[] | undefined;
345
  }
346
 
347
  interface Datepicker extends Widget, DatepickerOptions {
357
  // Dialog //////////////////////////////////////////////////
358
 
359
  interface DialogOptions extends DialogEvents {
360
+ autoOpen?: boolean | undefined;
361
+ buttons?: { [buttonText: string]: (event?: Event) => void } | DialogButtonOptions[] | undefined;
362
+ closeOnEscape?: boolean | undefined;
363
+ classes?: DialogClasses | undefined;
364
+ closeText?: string | undefined;
365
+ appendTo?: string | undefined;
366
+ dialogClass?: string | undefined;
367
+ disabled?: boolean | undefined;
368
+ draggable?: boolean | undefined;
369
+ height?: number | string | undefined;
370
+ hide?: boolean | number | string | DialogShowHideOptions | undefined;
371
+ maxHeight?: number | undefined;
372
+ maxWidth?: number | undefined;
373
+ minHeight?: number | undefined;
374
+ minWidth?: number | undefined;
375
+ modal?: boolean | undefined;
376
  position?: any; // object, string or []
377
+ resizable?: boolean | undefined;
378
+ show?: boolean | number | string | DialogShowHideOptions | undefined;
379
+ stack?: boolean | undefined;
380
+ title?: string | undefined;
381
  width?: any; // number or string
382
+ zIndex?: number | undefined;
383
 
384
+ open?: DialogEvent | undefined;
385
+ close?: DialogEvent | undefined;
386
+ }
387
+
388
+ interface DialogClasses {
389
+ "ui-dialog"?: string | undefined;
390
+ "ui-dialog-content"?: string | undefined;
391
+ "ui-dialog-dragging"?: string | undefined;
392
+ "ui-dialog-resizing"?: string | undefined;
393
+ "ui-dialog-buttons"?: string | undefined;
394
+ "ui-dialog-titlebar"?: string | undefined;
395
+ "ui-dialog-title"?: string | undefined;
396
+ "ui-dialog-titlebar-close"?: string | undefined;
397
+ "ui-dialog-buttonpane"?: string | undefined;
398
+ "ui-dialog-buttonset"?: string | undefined;
399
+ "ui-widget-overlay"?: string | undefined;
400
  }
401
 
402
  interface DialogButtonOptions {
403
  icons?: any;
404
+ showText?: string | boolean | undefined;
405
+ text?: string | undefined;
406
+ click?: ((eventObject: JQueryEventObject) => any) | undefined;
407
  [attr: string]: any; // attributes for the <button> element
408
  }
409
 
410
  interface DialogShowHideOptions {
411
  effect: string;
412
+ delay?: number | undefined;
413
+ duration?: number | undefined;
414
+ easing?: string | undefined;
415
  }
416
 
417
  interface DialogUIParams {
418
  }
419
 
420
  interface DialogEvent {
421
+ (event: JQueryEventObject, ui: DialogUIParams): void;
422
  }
423
 
424
  interface DialogEvents {
425
+ beforeClose?: DialogEvent | undefined;
426
+ close?: DialogEvent | undefined;
427
+ create?: DialogEvent | undefined;
428
+ drag?: DialogEvent | undefined;
429
+ dragStart?: DialogEvent | undefined;
430
+ dragStop?: DialogEvent | undefined;
431
+ focus?: DialogEvent | undefined;
432
+ open?: DialogEvent | undefined;
433
+ resize?: DialogEvent | undefined;
434
+ resizeStart?: DialogEvent | undefined;
435
+ resizeStop?: DialogEvent | undefined;
436
  }
437
 
438
  interface Dialog extends Widget, DialogOptions {
444
  interface DraggableEventUIParams {
445
  helper: JQuery;
446
  position: { top: number; left: number; };
447
+ originalPosition: { top: number; left: number; };
448
  offset: { top: number; left: number; };
449
  }
450
 
451
  interface DraggableEvent {
452
+ (event: JQueryEventObject, ui: DraggableEventUIParams): void;
453
  }
454
 
455
  interface DraggableOptions extends DraggableEvents {
456
+ disabled?: boolean | undefined;
457
+ addClasses?: boolean | undefined;
458
  appendTo?: any;
459
+ axis?: string | undefined;
460
+ cancel?: string | undefined;
461
+ classes?: DraggableClasses | undefined;
462
+ connectToSortable?: Element | Element[] | JQuery | string | undefined;
463
  containment?: any;
464
+ cursor?: string | undefined;
465
  cursorAt?: any;
466
+ delay?: number | undefined;
467
+ distance?: number | undefined;
468
+ grid?: number[] | undefined;
469
  handle?: any;
470
  helper?: any;
471
  iframeFix?: any;
472
+ opacity?: number | undefined;
473
+ refreshPositions?: boolean | undefined;
474
  revert?: any;
475
+ revertDuration?: number | undefined;
476
+ scope?: string | undefined;
477
+ scroll?: boolean | undefined;
478
+ scrollSensitivity?: number | undefined;
479
+ scrollSpeed?: number | undefined;
480
  snap?: any;
481
+ snapMode?: string | undefined;
482
+ snapTolerance?: number | undefined;
483
+ stack?: string | undefined;
484
+ zIndex?: number | undefined;
485
+ }
486
+
487
+ interface DraggableClasses {
488
+ "ui-draggable"?: string | undefined;
489
+ "ui-draggable-disabled"?: string | undefined;
490
+ "ui-draggable-dragging"?: string | undefined;
491
+ "ui-draggable-handle"?: string | undefined;
492
  }
493
 
494
  interface DraggableEvents {
495
+ create?: DraggableEvent | undefined;
496
+ start?: DraggableEvent | undefined;
497
+ drag?: DraggableEvent | undefined;
498
+ stop?: DraggableEvent | undefined;
499
  }
500
 
501
  interface Draggable extends Widget, DraggableOptions, DraggableEvent {
512
  }
513
 
514
  interface DroppableEvent {
515
+ (event: JQueryEventObject, ui: DroppableEventUIParam): void;
516
  }
517
 
518
  interface DroppableOptions extends DroppableEvents {
 
519
  accept?: any;
520
+ activeClass?: string | undefined;
521
+ addClasses?: boolean | undefined;
522
+ disabled?: boolean | undefined;
523
+ greedy?: boolean | undefined;
524
+ hoverClass?: string | undefined;
525
+ scope?: string | undefined;
526
+ tolerance?: string | undefined;
527
  }
528
 
529
  interface DroppableEvents {
530
+ create?: DroppableEvent | undefined;
531
+ activate?: DroppableEvent | undefined;
532
+ deactivate?: DroppableEvent | undefined;
533
+ over?: DroppableEvent | undefined;
534
+ out?: DroppableEvent | undefined;
535
+ drop?: DroppableEvent | undefined;
536
  }
537
 
538
  interface Droppable extends Widget, DroppableOptions {
541
  // Menu //////////////////////////////////////////////////
542
 
543
  interface MenuOptions extends MenuEvents {
544
+ disabled?: boolean | undefined;
545
  icons?: any;
546
+ menus?: string | undefined;
547
  position?: any; // TODO
548
+ role?: string | undefined;
549
  }
550
 
551
  interface MenuUIParams {
552
+ item?: JQuery | undefined;
553
  }
554
 
555
  interface MenuEvent {
556
+ (event: JQueryEventObject, ui: MenuUIParams): void;
557
  }
558
 
559
  interface MenuEvents {
560
+ blur?: MenuEvent | undefined;
561
+ create?: MenuEvent | undefined;
562
+ focus?: MenuEvent | undefined;
563
+ select?: MenuEvent | undefined;
564
  }
565
 
566
  interface Menu extends Widget, MenuOptions {
570
  // Progressbar //////////////////////////////////////////////////
571
 
572
  interface ProgressbarOptions extends ProgressbarEvents {
573
+ disabled?: boolean | undefined;
574
+ value?: number | boolean | undefined;
575
+ max?: number | undefined;
576
  }
577
 
578
  interface ProgressbarUIParams {
579
  }
580
 
581
  interface ProgressbarEvent {
582
+ (event: JQueryEventObject, ui: ProgressbarUIParams): void;
583
  }
584
 
585
  interface ProgressbarEvents {
586
+ change?: ProgressbarEvent | undefined;
587
+ complete?: ProgressbarEvent | undefined;
588
+ create?: ProgressbarEvent | undefined;
589
  }
590
 
591
  interface Progressbar extends Widget, ProgressbarOptions {
596
 
597
  interface ResizableOptions extends ResizableEvents {
598
  alsoResize?: any; // Selector, JQuery or Element
599
+ animate?: boolean | undefined;
600
  animateDuration?: any; // number or string
601
+ animateEasing?: string | undefined;
602
  aspectRatio?: any; // boolean or number
603
+ autoHide?: boolean | undefined;
604
+ cancel?: string | undefined;
605
  containment?: any; // Selector, Element or string
606
+ delay?: number | undefined;
607
+ disabled?: boolean | undefined;
608
+ distance?: number | undefined;
609
+ ghost?: boolean | undefined;
610
  grid?: any;
611
  handles?: any; // string or object
612
+ helper?: string | undefined;
613
+ maxHeight?: number | undefined;
614
+ maxWidth?: number | undefined;
615
+ minHeight?: number | undefined;
616
+ minWidth?: number | undefined;
617
  }
618
 
619
  interface ResizableUIParams {
627
  }
628
 
629
  interface ResizableEvent {
630
+ (event: JQueryEventObject, ui: ResizableUIParams): void;
631
  }
632
 
633
  interface ResizableEvents {
634
+ resize?: ResizableEvent | undefined;
635
+ start?: ResizableEvent | undefined;
636
+ stop?: ResizableEvent | undefined;
637
+ create?: ResizableEvent | undefined;
638
  }
639
 
640
  interface Resizable extends Widget, ResizableOptions {
644
  // Selectable //////////////////////////////////////////////////
645
 
646
  interface SelectableOptions extends SelectableEvents {
647
+ autoRefresh?: boolean | undefined;
648
+ cancel?: string | undefined;
649
+ delay?: number | undefined;
650
+ disabled?: boolean | undefined;
651
+ distance?: number | undefined;
652
+ filter?: string | undefined;
653
+ tolerance?: string | undefined;
654
  }
655
 
656
  interface SelectableEvents {
657
+ selected? (event: JQueryEventObject, ui: { selected?: Element | undefined; }): void;
658
+ selecting? (event: JQueryEventObject, ui: { selecting?: Element | undefined; }): void;
659
+ start? (event: JQueryEventObject, ui: any): void;
660
+ stop? (event: JQueryEventObject, ui: any): void;
661
+ unselected? (event: JQueryEventObject, ui: { unselected: Element; }): void;
662
+ unselecting? (event: JQueryEventObject, ui: { unselecting: Element; }): void;
663
  }
664
 
665
  interface Selectable extends Widget, SelectableOptions {
666
  }
667
 
668
+ // SelectMenu //////////////////////////////////////////////////
669
+
670
+ interface SelectMenuOptions extends SelectMenuEvents {
671
+ appendTo?: string | undefined;
672
+ classes?: SelectMenuClasses | undefined;
673
+ disabled?: boolean | undefined;
674
+ icons?: any;
675
+ position?: JQueryPositionOptions | undefined;
676
+ width?: number | undefined;
677
+ }
678
+
679
+ interface SelectMenuClasses {
680
+ "ui-selectmenu-button"?: string | undefined;
681
+ "ui-selectmenu-button-closed"?: string | undefined;
682
+ "ui-selectmenu-button-open"?: string | undefined;
683
+ "ui-selectmenu-text"?: string | undefined;
684
+ "ui-selectmenu-icon"?: string | undefined;
685
+ "ui-selectmenu-menu"?: string | undefined;
686
+ "ui-selectmenu-open"?: string | undefined;
687
+ "ui-selectmenu-optgroup"?: string | undefined;
688
+ }
689
+
690
+ interface SelectMenuUIParams {
691
+ item?: JQuery | undefined;
692
+ }
693
+
694
+ interface SelectMenuEvent {
695
+ (event: JQueryEventObject, ui: SelectMenuUIParams): void;
696
+ }
697
+
698
+ interface SelectMenuEvents {
699
+ change?: SelectMenuEvent | undefined;
700
+ close?: SelectMenuEvent | undefined;
701
+ create?: SelectMenuEvent | undefined;
702
+ focus?: SelectMenuEvent | undefined;
703
+ open?: SelectMenuEvent | undefined;
704
+ select?: SelectMenuEvent | undefined;
705
+ }
706
+
707
+ interface SelectMenu extends Widget, SelectMenuOptions {
708
+ }
709
+
710
  // Slider //////////////////////////////////////////////////
711
 
712
  interface SliderOptions extends SliderEvents {
713
  animate?: any; // boolean, string or number
714
+ disabled?: boolean | undefined;
715
+ max?: number | undefined;
716
+ min?: number | undefined;
717
+ orientation?: string | undefined;
718
  range?: any; // boolean or string
719
+ step?: number | undefined;
720
+ value?: number | undefined;
721
+ values?: number[] | undefined;
722
+ highlight?: boolean | undefined;
723
+ classes? : SliderClasses | undefined;
724
+ }
725
+
726
+ interface SliderClasses {
727
+ "ui-slider"?: string | undefined;
728
+ "ui-slider-horizontal"?: string | undefined;
729
+ "ui-slider-vertical"?: string | undefined;
730
+ "ui-slider-handle"?: string | undefined;
731
+ "ui-slider-range"?: string | undefined;
732
+ "ui-slider-range-min"?: string | undefined;
733
+ "ui-slider-range-max"?: string | undefined;
734
  }
735
 
736
  interface SliderUIParams {
737
+ handle?: JQuery | undefined;
738
+ value?: number | undefined;
739
+ values?: number[] | undefined;
740
  }
741
 
742
  interface SliderEvent {
743
+ (event: JQueryEventObject, ui: SliderUIParams): void;
744
  }
745
 
746
  interface SliderEvents {
747
+ change?: SliderEvent | undefined;
748
+ create?: SliderEvent | undefined;
749
+ slide?: SliderEvent | undefined;
750
+ start?: SliderEvent | undefined;
751
+ stop?: SliderEvent | undefined;
752
  }
753
 
754
  interface Slider extends Widget, SliderOptions {
759
 
760
  interface SortableOptions extends SortableEvents {
761
  appendTo?: any; // jQuery, Element, Selector or string
762
+ attribute?: string | undefined;
763
+ axis?: string | undefined;
764
  cancel?: any; // Selector
765
  connectWith?: any; // Selector
766
  containment?: any; // Element, Selector or string
767
+ cursor?: string | undefined;
768
  cursorAt?: any;
769
+ delay?: number | undefined;
770
+ disabled?: boolean | undefined;
771
+ distance?: number | undefined;
772
+ dropOnEmpty?: boolean | undefined;
773
+ forceHelperSize?: boolean | undefined;
774
+ forcePlaceholderSize?: boolean | undefined;
775
+ grid?: number[] | undefined;
776
+ helper?: string | ((event: JQueryEventObject, element: Sortable) => Element) | undefined;
777
  handle?: any; // Selector or Element
778
  items?: any; // Selector
779
+ opacity?: number | undefined;
780
+ placeholder?: string | undefined;
781
  revert?: any; // boolean or number
782
+ scroll?: boolean | undefined;
783
+ scrollSensitivity?: number | undefined;
784
+ scrollSpeed?: number | undefined;
785
+ tolerance?: string | undefined;
786
+ zIndex?: number | undefined;
787
  }
788
 
789
  interface SortableUIParams {
801
  }
802
 
803
  interface SortableEvents {
804
+ activate?: SortableEvent | undefined;
805
+ beforeStop?: SortableEvent | undefined;
806
+ change?: SortableEvent | undefined;
807
+ deactivate?: SortableEvent | undefined;
808
+ out?: SortableEvent | undefined;
809
+ over?: SortableEvent | undefined;
810
+ receive?: SortableEvent | undefined;
811
+ remove?: SortableEvent | undefined;
812
+ sort?: SortableEvent | undefined;
813
+ start?: SortableEvent | undefined;
814
+ stop?: SortableEvent | undefined;
815
+ update?: SortableEvent | undefined;
816
  }
817
 
818
  interface Sortable extends Widget, SortableOptions, SortableEvents {
822
  // Spinner //////////////////////////////////////////////////
823
 
824
  interface SpinnerOptions extends SpinnerEvents {
825
+ culture?: string | undefined;
826
+ disabled?: boolean | undefined;
827
  icons?: any;
828
  incremental?: any; // boolean or ()
829
  max?: any; // number or string
830
  min?: any; // number or string
831
+ numberFormat?: string | undefined;
832
+ page?: number | undefined;
833
  step?: any; // number or string
834
  }
835
 
838
  }
839
 
840
  interface SpinnerEvent<T> {
841
+ (event: JQueryEventObject, ui: T): void;
842
  }
843
 
844
  interface SpinnerEvents {
845
+ change?: SpinnerEvent<{}> | undefined;
846
+ create?: SpinnerEvent<{}> | undefined;
847
+ spin?: SpinnerEvent<SpinnerUIParam> | undefined;
848
+ start?: SpinnerEvent<{}> | undefined;
849
+ stop?: SpinnerEvent<{}> | undefined;
850
  }
851
 
852
  interface Spinner extends Widget, SpinnerOptions {
857
 
858
  interface TabsOptions extends TabsEvents {
859
  active?: any; // boolean or number
860
+ classes?: TabClasses | undefined;
861
+ collapsible?: boolean | undefined;
862
  disabled?: any; // boolean or []
863
+ event?: string | undefined;
864
+ heightStyle?: string | undefined;
865
  hide?: any; // boolean, number, string or object
866
  show?: any; // boolean, number, string or object
867
  }
868
 
869
+ interface TabClasses {
870
+ "ui-tabs"?: string | undefined;
871
+ "ui-tabs-collapsible"?: string | undefined;
872
+ "ui-tabs-nav"?: string | undefined;
873
+ "ui-tabs-tab"?: string | undefined;
874
+ "ui-tabs-active"?: string | undefined;
875
+ "ui-tabs-loading"?: string | undefined;
876
+ "ui-tabs-anchor"?: string | undefined;
877
+ "ui-tabs-panel"?: string | undefined;
878
+ }
879
+
880
  interface TabsActivationUIParams {
881
  newTab: JQuery;
882
  oldTab: JQuery;
897
  }
898
 
899
  interface TabsEvent<UI> {
900
+ (event: JQueryEventObject, ui: UI): void;
901
  }
902
 
903
  interface TabsEvents {
904
+ activate?: TabsEvent<TabsActivationUIParams> | undefined;
905
+ beforeActivate?: TabsEvent<TabsActivationUIParams> | undefined;
906
+ beforeLoad?: TabsEvent<TabsBeforeLoadUIParams> | undefined;
907
+ load?: TabsEvent<TabsCreateOrLoadUIParams> | undefined;
908
+ create?: TabsEvent<TabsCreateOrLoadUIParams> | undefined;
909
  }
910
 
911
  interface Tabs extends Widget, TabsOptions {
916
 
917
  interface TooltipOptions extends TooltipEvents {
918
  content?: any; // () or string
919
+ disabled?: boolean | undefined;
920
  hide?: any; // boolean, number, string or object
921
+ items?: string|JQuery | undefined;
922
  position?: any; // TODO
923
  show?: any; // boolean, number, string or object
924
+ tooltipClass?: string | undefined; // deprecated in jQuery UI 1.12
925
+ track?: boolean | undefined;
926
+ classes?: {[key: string]: string} | undefined;
927
  }
928
 
929
  interface TooltipUIParams {
930
  }
931
 
932
  interface TooltipEvent {
933
+ (event: JQueryEventObject, ui: TooltipUIParams): void;
934
  }
935
 
936
  interface TooltipEvents {
937
+ close?: TooltipEvent | undefined;
938
+ open?: TooltipEvent | undefined;
939
  }
940
 
941
  interface Tooltip extends Widget, TooltipOptions {
946
 
947
  interface EffectOptions {
948
  effect: string;
949
+ easing?: string | undefined;
950
+ duration?: number | undefined;
951
  complete: Function;
952
  }
953
 
954
  interface BlindEffect {
955
+ direction?: string | undefined;
956
  }
957
 
958
  interface BounceEffect {
959
+ distance?: number | undefined;
960
+ times?: number | undefined;
961
  }
962
 
963
  interface ClipEffect {
964
+ direction?: number | undefined;
965
  }
966
 
967
  interface DropEffect {
968
+ direction?: number | undefined;
969
  }
970
 
971
  interface ExplodeEffect {
972
+ pieces?: number | undefined;
973
  }
974
 
975
  interface FadeEffect { }
976
 
977
  interface FoldEffect {
978
  size?: any;
979
+ horizFirst?: boolean | undefined;
980
  }
981
 
982
  interface HighlightEffect {
983
+ color?: string | undefined;
984
  }
985
 
986
  interface PuffEffect {
987
+ percent?: number | undefined;
988
  }
989
 
990
  interface PulsateEffect {
991
+ times?: number | undefined;
992
  }
993
 
994
  interface ScaleEffect {
995
+ direction?: string | undefined;
996
+ origin?: string[] | undefined;
997
+ percent?: number | undefined;
998
+ scale?: string | undefined;
999
  }
1000
 
1001
  interface ShakeEffect {
1002
+ direction?: string | undefined;
1003
+ distance?: number | undefined;
1004
+ times?: number | undefined;
1005
  }
1006
 
1007
  interface SizeEffect {
1008
  to?: any;
1009
+ origin?: string[] | undefined;
1010
+ scale?: string | undefined;
1011
  }
1012
 
1013
  interface SlideEffect {
1014
+ direction?: string | undefined;
1015
+ distance?: number | undefined;
1016
  }
1017
 
1018
  interface TransferEffect {
1019
+ className?: string | undefined;
1020
+ to?: string | undefined;
1021
  }
1022
 
1023
  interface JQueryPositionOptions {
1024
+ my?: string | undefined;
1025
+ at?: string | undefined;
1026
  of?: any;
1027
+ collision?: string | undefined;
1028
+ using?: Function | undefined;
1029
  within?: any;
1030
  }
1031
 
1033
  // UI //////////////////////////////////////////////////
1034
 
1035
  interface MouseOptions {
1036
+ cancel?: string | undefined;
1037
+ delay?: number | undefined;
1038
+ distance?: number | undefined;
1039
  }
1040
 
1041
  interface KeyCode {
1078
  keyCode: KeyCode;
1079
  menu: Menu;
1080
  progressbar: Progressbar;
1081
+ selectmenu: SelectMenu;
1082
  slider: Slider;
1083
  spinner: Spinner;
1084
  tabs: Tabs;
1090
  // Widget //////////////////////////////////////////////////
1091
 
1092
  interface WidgetOptions {
1093
+ disabled?: boolean | undefined;
1094
  hide?: any;
1095
  show?: any;
1096
  }
1097
 
1098
+ interface WidgetCommonProperties {
1099
+ element: JQuery;
1100
+ defaultElement : string;
1101
+ document: Document;
1102
+ namespace: string;
1103
+ uuid: string;
1104
+ widgetEventPrefix: string;
1105
+ widgetFullName: string;
1106
+ window: Window;
1107
+ }
1108
+
1109
  interface Widget {
1110
  (methodName: string): JQuery;
1111
  (options: WidgetOptions): JQuery;
1114
  (optionLiteral: string, options: WidgetOptions): any;
1115
  (optionLiteral: string, optionName: string, optionValue: any): JQuery;
1116
 
1117
+ <T>(name: string, prototype: T & ThisType<T & WidgetCommonProperties>): JQuery;
1118
+ <T>(name: string, base: Function, prototype: T & ThisType<T & WidgetCommonProperties> ): JQuery;
1119
  }
1120
 
1121
  ////////////////////////////////////////////////////////////////////////////////////////////////////
1429
  * @param optionName 'buttonText'
1430
  */
1431
  datepicker(methodName: 'option', optionName: 'buttonText'): string;
1432
+
1433
+ /**
1434
+ * Get the autohide option, after initialization
1435
+ *
1436
+ * @param methodName 'option'
1437
+ * @param optionName 'autohide'
1438
+ */
1439
+ datepicker(methodName: 'option', optionName: 'autohide'): boolean;
1440
+
1441
+
1442
+ /**
1443
+ * Get the endDate after initialization
1444
+ *
1445
+ * @param methodName 'option'
1446
+ * @param optionName 'endDate'
1447
+ */
1448
+ datepicker(methodName: 'option', optionName: 'endDate'): Date;
1449
  /**
1450
  * Set the buttonText option, after initialization
1451
  *
1811
  selectable(optionLiteral: string, options: JQueryUI.SelectableOptions): any;
1812
  selectable(optionLiteral: string, optionName: string, optionValue: any): JQuery;
1813
 
1814
+ selectmenu(): JQuery;
1815
+ selectmenu(methodName: 'close'): JQuery;
1816
+ selectmenu(methodName: 'destroy'): JQuery;
1817
+ selectmenu(methodName: 'disable'): JQuery;
1818
+ selectmenu(methodName: 'enable'): JQuery;
1819
+ selectmenu(methodName: 'instance'): any;
1820
+ selectmenu(methodName: 'menuWidget'): JQuery;
1821
+ selectmenu(methodName: 'open'): JQuery;
1822
+ selectmenu(methodName: 'refresh'): JQuery;
1823
+ selectmenu(methodName: 'widget'): JQuery;
1824
+ selectmenu(methodName: string): JQuery;
1825
+ selectmenu(options: JQueryUI.SelectMenuOptions): JQuery;
1826
+ selectmenu(optionLiteral: string, optionName: string): any;
1827
+ selectmenu(optionLiteral: string, options: JQueryUI.SelectMenuOptions): any;
1828
+ selectmenu(optionLiteral: string, optionName: string, optionValue: any): JQuery;
1829
+
1830
  slider(): JQuery;
1831
  slider(methodName: 'destroy'): void;
1832
  slider(methodName: 'disable'): void;
1852
  sortable(methodName: 'disable'): void;
1853
  sortable(methodName: 'enable'): void;
1854
  sortable(methodName: 'widget'): JQuery;
1855
+ sortable(methodName: 'toArray', options?: { attribute?: string | undefined; }): string[];
1856
  sortable(methodName: string): JQuery;
1857
  sortable(options: JQueryUI.SortableOptions): JQuery;
1858
  sortable(optionLiteral: string, optionName: string): any;
1859
+ sortable(methodName: 'serialize', options?: { key?: string | undefined; attribute?: string | undefined; expression?: RegExp | undefined }): string;
1860
  sortable(optionLiteral: string, options: JQueryUI.SortableOptions): any;
1861
  sortable(optionLiteral: string, optionName: string, optionValue: any): JQuery;
1862
 
1880
  tabs(): JQuery;
1881
  tabs(methodName: 'destroy'): void;
1882
  tabs(methodName: 'disable'): void;
1883
+ tabs(methodName: 'disable', index: number): void;
1884
  tabs(methodName: 'enable'): void;
1885
+ tabs(methodName: 'enable', index: number): void;
1886
  tabs(methodName: 'load', index: number): void;
1887
  tabs(methodName: 'refresh'): void;
1888
  tabs(methodName: 'widget'): JQuery;
1889
+ tabs(methodName: 'select', index: number): JQuery;
1890
  tabs(methodName: string): JQuery;
1891
  tabs(options: JQueryUI.TabsOptions): JQuery;
1892
  tabs(optionLiteral: string, optionName: string): any;
1907
  tooltip(optionLiteral: string, optionName: string, optionValue: any): JQuery;
1908
 
1909
 
1910
+ addClass(classNames: string, speed?: number, callback?: Function): this;
1911
+ addClass(classNames: string, speed?: string, callback?: Function): this;
1912
+ addClass(classNames: string, speed?: number, easing?: string, callback?: Function): this;
1913
+ addClass(classNames: string, speed?: string, easing?: string, callback?: Function): this;
1914
 
1915
+ removeClass(classNames: string, speed?: number, callback?: Function): this;
1916
+ removeClass(classNames: string, speed?: string, callback?: Function): this;
1917
+ removeClass(classNames: string, speed?: number, easing?: string, callback?: Function): this;
1918
+ removeClass(classNames: string, speed?: string, easing?: string, callback?: Function): this;
1919
 
1920
+ switchClass(removeClassName: string, addClassName: string, duration?: number, easing?: string, complete?: Function): this;
1921
+ switchClass(removeClassName: string, addClassName: string, duration?: string, easing?: string, complete?: Function): this;
1922
 
1923
+ toggleClass(className: string, duration?: number, easing?: string, complete?: Function): this;
1924
+ toggleClass(className: string, duration?: string, easing?: string, complete?: Function): this;
1925
+ toggleClass(className: string, aswitch?: boolean, duration?: number, easing?: string, complete?: Function): this;
1926
+ toggleClass(className: string, aswitch?: boolean, duration?: string, easing?: string, complete?: Function): this;
1927
 
1928
+ effect(options: any): this;
1929
+ effect(effect: string, options?: any, duration?: number, complete?: Function): this;
1930
+ effect(effect: string, options?: any, duration?: string, complete?: Function): this;
1931
 
1932
+ hide(options: any): this;
1933
+ hide(effect: string, options?: any, duration?: number, complete?: Function): this;
1934
+ hide(effect: string, options?: any, duration?: string, complete?: Function): this;
1935
 
1936
+ show(options: any): this;
1937
+ show(effect: string, options?: any, duration?: number, complete?: Function): this;
1938
+ show(effect: string, options?: any, duration?: string, complete?: Function): this;
1939
 
1940
+ toggle(options: any): this;
1941
+ toggle(effect: string, options?: any, duration?: number, complete?: Function): this;
1942
+ toggle(effect: string, options?: any, duration?: string, complete?: Function): this;
1943
 
1944
  position(options: JQueryUI.JQueryPositionOptions): JQuery;
1945
 
1949
  uniqueId(): JQuery;
1950
  removeUniqueId(): JQuery;
1951
  scrollParent(): JQuery;
1952
+ zIndex(): number;
1953
  zIndex(zIndex: number): JQuery;
1954
 
1955
  widget: JQueryUI.Widget;
js/menu-editor.js CHANGED
@@ -49,6 +49,8 @@
49
  * @property {string} wsEditorData.setTestConfigurationNonce
50
  * @property {string} wsEditorData.testAccessNonce
51
  *
 
 
52
  * @property {boolean} wsEditorData.isDemoMode
53
  * @property {boolean} wsEditorData.isMasterMode
54
  */
@@ -191,6 +193,11 @@ var itemTemplates = {
191
  }
192
  };
193
 
 
 
 
 
 
194
  /**
195
  * Set an input field to a value. The only difference from jQuery.val() is that
196
  * setting a checkbox to true/false will check/clear it.
@@ -241,25 +248,25 @@ function randomMenuId(prefix, size){
241
  AmeEditorApi.randomMenuId = randomMenuId;
242
 
243
  function outputWpMenu(menu){
244
- var menuCopy = $.extend(true, {}, menu);
245
- var menuBox = $('#ws_menu_box');
246
 
247
  //Remove the current menu data
248
- menuBox.empty();
249
- $('#ws_submenu_box').empty();
250
 
251
  //Display the new menu
252
- var i = 0;
253
- for (var filename in menuCopy){
 
254
  if (!menuCopy.hasOwnProperty(filename)){
255
  continue;
256
  }
257
- outputTopMenu(menuCopy[filename]);
258
- i++;
259
  }
260
 
261
  //Automatically select the first top-level menu
262
- menuBox.find('.ws_menu:first').trigger('click');
 
 
263
  }
264
 
265
  /**
@@ -311,70 +318,779 @@ function loadMenuConfiguration(adminMenu) {
311
  $(document).trigger('menuConfigurationLoaded.adminMenuEditor', adminMenu);
312
  }
313
 
314
- /*
315
- * Create edit widgets for a top-level menu and its submenus and append them all to the DOM.
316
- *
317
- * Inputs :
318
- * menu - an object containing menu data
319
- * afterNode - if specified, the new menu widget will be inserted after this node. Otherwise,
320
- * it will be added to the end of the list.
321
- * Outputs :
322
- * Object with two fields - 'menu' and 'submenu' - containing the DOM nodes of the created widgets.
323
- */
324
- function outputTopMenu(menu, afterNode){
325
- //Create the menu widget
326
- var menu_obj = buildMenuItem(menu, true);
327
 
328
- if ( (typeof afterNode !== 'undefined') && (afterNode !== null) ){
329
- $(afterNode).after(menu_obj);
330
- } else {
331
- menu_obj.appendTo('#ws_menu_box');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  }
333
 
334
- //Create a container for menu items, even if there are none
335
- var submenu = buildSubmenu(menu.items, menu_obj.attr('id'));
336
- submenu.appendTo('#ws_submenu_box');
337
- menu_obj.data('submenu_id', submenu.attr('id'));
 
 
 
 
 
 
 
338
 
339
- //Note: Update the menu only after its children are ready. It needs the submenu items to decide whether to display
340
- //the access checkbox as checked or indeterminate.
341
- updateItemEditor(menu_obj);
 
 
 
 
342
 
343
- return {
344
- 'menu' : menu_obj,
345
- 'submenu' : submenu
346
- };
347
- }
 
 
348
 
349
- /*
350
- * Create and populate a submenu container.
351
- */
352
- function buildSubmenu(items, parentMenuId){
353
- //Create a container for menu items, even if there are none
354
- var submenu = $('<div class="ws_submenu" style="display:none;"></div>');
355
- submenu.attr('id', 'ws-submenu-'+(wsIdCounter++));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
 
357
- if (parentMenuId) {
358
- submenu.data('parent_menu_id', parentMenuId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  }
360
 
361
- //Only show menus that have items.
362
- //Skip arrays (with a length) because filled menus are encoded as custom objects.
363
- var entry = null;
364
- if (items) {
365
- $.each(items, function(index, item) {
366
- entry = buildMenuItem(item, false);
367
- if ( entry ){
368
- submenu.append(entry);
369
- updateItemEditor(entry);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  }
371
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  }
373
 
374
- //Make the submenu sortable
375
- makeBoxSortable(submenu);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
 
377
- return submenu;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  }
379
 
380
  /**
@@ -437,22 +1153,32 @@ function buildMenuItem(itemData, isTopLevel) {
437
  }),
438
 
439
  'drop' : (function(event, ui){
440
- var droppedItemData = readItemState(ui.draggable);
441
- var new_item = buildMenuItem(droppedItemData, false);
 
 
 
 
 
 
 
 
 
 
442
 
443
- var sourceSubmenu = ui.draggable.parent();
444
- var submenu = $('#' + item.data('submenu_id'));
445
- submenu.append(new_item);
446
 
447
  if ( !event.ctrlKey ) {
448
- ui.draggable.remove();
449
  }
450
 
451
- updateItemEditor(new_item);
452
 
453
  //Moving an item can change aggregate menu permissions. Update the UI accordingly.
454
  updateParentAccessUi(submenu);
455
- updateParentAccessUi(sourceSubmenu);
 
 
456
  })
457
  });
458
  }
@@ -1051,7 +1777,7 @@ function buildEditboxFields(fieldContainer, entry, isTopLevel){
1051
  //noinspection JSUnusedLocalSymbols
1052
  function buildEditboxField(entry, field_name, field_settings){
1053
  //Build a form field of the appropriate type
1054
- var inputBox = null;
1055
  var basicTextField = '<input type="text" class="ws_field_value">';
1056
  //noinspection FallthroughInSwitchStatementJS
1057
  switch(field_settings.type){
@@ -1142,7 +1868,7 @@ function buildEditboxField(entry, field_name, field_settings){
1142
  .attr('src', wsEditorData.imagesUrl + '/transparent16.png')
1143
  ).data('field_name', field_name);
1144
 
1145
- var visible = true;
1146
  if (typeof field_settings.visible === 'function') {
1147
  visible = field_settings.visible(entry, field_name);
1148
  } else {
@@ -1543,25 +2269,36 @@ AmeEditorApi.forEachMenuItem = function(callback, skipSeparators) {
1543
  /**
1544
  * Select the first menu item that has the specified URL.
1545
  *
1546
- * @param {string} boxSelector
1547
  * @param {string} url
1548
- * @param {boolean|null} [expandProperties]
1549
  * @returns {JQuery}
1550
  */
1551
- AmeEditorApi.selectMenuItemByUrl = function(boxSelector, url, expandProperties) {
1552
  if (typeof expandProperties === 'undefined') {
1553
  expandProperties = null;
1554
  }
1555
 
1556
- var box = $(boxSelector);
1557
- if (box.is('#ws_submenu_box')) {
1558
- box = box.find('.ws_submenu:visible').first();
 
 
 
 
1559
  }
1560
 
1561
- var containerNode =
 
 
 
 
 
 
 
1562
  box.find('.ws_container')
1563
  .filter(function() {
1564
- var itemUrl = AmeEditorApi.getItemDisplayUrl($(this).data('menu_item'));
1565
  return (itemUrl === url);
1566
  })
1567
  .first();
@@ -1570,7 +2307,7 @@ AmeEditorApi.selectMenuItemByUrl = function(boxSelector, url, expandProperties)
1570
  AmeEditorApi.selectItem(containerNode);
1571
 
1572
  if (expandProperties !== null) {
1573
- var expandLink = containerNode.find('.ws_edit_link').first();
1574
  if (expandLink.hasClass('ws_edit_link_expanded') !== expandProperties) {
1575
  expandLink.trigger('click');
1576
  }
@@ -1655,9 +2392,9 @@ function readMenuTreeState(){
1655
 
1656
  /**
1657
  * Losslessly compress the admin menu configuration.
1658
- *
1659
  * This is a JS port of the ameMenu::compress() function defined in /includes/menu.php.
1660
- *
1661
  * @param {Object} adminMenu
1662
  * @returns {Object}
1663
  */
@@ -1963,7 +2700,7 @@ function actorCanAccessMenu(menuItem, actor) {
1963
  //By default, any actor that has the required cap has access to the menu.
1964
  //Users can override this on a per-menu basis.
1965
  var requiredCap = getFieldValue(menuItem, 'access_level', '< Error: access_level is missing! >');
1966
- var actorHasAccess = false;
1967
  if (menuItem.grant_access.hasOwnProperty(actor)) {
1968
  actorHasAccess = menuItem.grant_access[actor];
1969
  } else {
@@ -2052,6 +2789,9 @@ var generalComponentVisibility = {};
2052
  var isDomReadyDone = false;
2053
 
2054
  function ameOnDomReady() {
 
 
 
2055
  isDomReadyDone = true;
2056
 
2057
  //Some editor elements are only available in the Pro version.
@@ -2081,11 +2821,9 @@ function ameOnDomReady() {
2081
  /***************************************************************************
2082
  Event handlers for editor widgets
2083
  ***************************************************************************/
2084
- var menuEditorNode = $('#ws_menu_editor'),
2085
- submenuBox = $('#ws_submenu_box'),
2086
- submenuDropZone = submenuBox.closest('.ws_main_container').find('.ws_dropzone');
2087
 
2088
- var currentVisibleSubmenu = null;
2089
 
2090
  /**
2091
  * Select a menu item and show its submenu.
@@ -2093,32 +2831,7 @@ function ameOnDomReady() {
2093
  * @param {JQuery|HTMLElement} container Menu container node.
2094
  */
2095
  function selectItem(container) {
2096
- if (container.hasClass('ws_active')) {
2097
- //The menu item is already selected.
2098
- return;
2099
- }
2100
-
2101
- //Highlight the active item and un-highlight the previous one
2102
- container.addClass('ws_active');
2103
- container.siblings('.ws_active').removeClass('ws_active');
2104
- if (container.hasClass('ws_menu')) {
2105
- //Show/hide the appropriate submenu
2106
- if ( currentVisibleSubmenu ){
2107
- currentVisibleSubmenu.hide();
2108
- }
2109
- currentVisibleSubmenu = $('#' + container.data('submenu_id')).show();
2110
-
2111
- updateSubmenuBoxHeight(container);
2112
-
2113
- currentVisibleSubmenu.closest('.ws_main_container')
2114
- .find('.ws_toolbar .ws_delete_menu_button')
2115
- .toggleClass('ws_button_disabled', !canDeleteItem(getSelectedSubmenuItem()));
2116
- }
2117
-
2118
- //Make the "delete" button appear disabled if you can't delete this item.
2119
- container.closest('.ws_main_container')
2120
- .find('.ws_toolbar .ws_delete_menu_button')
2121
- .toggleClass('ws_button_disabled', !canDeleteItem(container));
2122
  }
2123
  AmeEditorApi.selectItem = selectItem;
2124
 
@@ -2128,6 +2841,15 @@ function ameOnDomReady() {
2128
  }));
2129
 
2130
  function updateSubmenuBoxHeight(selectedMenu) {
 
 
 
 
 
 
 
 
 
2131
  //Make the submenu box tall enough to reach the selected item.
2132
  //This prevents the menu tip (if any) from floating in empty space.
2133
  if (selectedMenu.hasClass('ws_menu_separator')) {
@@ -2253,7 +2975,8 @@ function ameOnDomReady() {
2253
  field.removeClass('ws_input_default');
2254
  }
2255
 
2256
- if (field.hasClass('ws_input_default') && (value == defaultValue)) {
 
2257
  value = null; //null = use default.
2258
  }
2259
 
@@ -2324,22 +3047,26 @@ function ameOnDomReady() {
2324
  * @param containerNode
2325
  * @param {String|Object.<String, Boolean>} actor
2326
  * @param {Boolean} [allowAccess]
 
2327
  */
2328
- function setActorAccessForTreeAndUpdateUi(containerNode, actor, allowAccess) {
2329
  setActorAccess(containerNode, actor, allowAccess);
2330
 
2331
  //Apply the same permissions to sub-menus.
2332
- var subMenuId = containerNode.data('submenu_id');
2333
- if (subMenuId && containerNode.hasClass('ws_menu')) {
2334
  $('.ws_item', '#' + subMenuId).each(function() {
2335
- var node = $(this);
2336
- setActorAccess(node, actor, allowAccess);
2337
- updateItemEditor(node);
2338
  });
2339
  }
2340
 
2341
  updateItemEditor(containerNode);
2342
- updateParentAccessUi(containerNode);
 
 
 
 
2343
  }
2344
 
2345
  /**
@@ -2502,13 +3229,15 @@ function ameOnDomReady() {
2502
 
2503
  var isDropdownBeingHidden = false, isSuggestionClick = false;
2504
 
 
 
2505
  //Show/hide the capability drop-down list when the trigger button is clicked
2506
  $('#ws_trigger_capability_dropdown').on('mousedown click', onDropdownTriggerClicked);
2507
  menuEditorNode.on('mousedown click', '.ws_cap_selector_trigger', onDropdownTriggerClicked);
2508
 
2509
  function onDropdownTriggerClicked(event){
2510
  /* jshint validthis:true */
2511
- var inputBox = null;
2512
  var button = $(this);
2513
 
2514
  var isInAccessEditor = false;
@@ -2516,7 +3245,7 @@ function ameOnDomReady() {
2516
 
2517
  //Find the input associated with the button that was clicked.
2518
  if ( button.attr('id') === 'ws_trigger_capability_dropdown' ) {
2519
- inputBox = $('#ws_extra_capability');
2520
  isInAccessEditor = true;
2521
  } else {
2522
  inputBox = button.closest('.ws_edit_field').find('.ws_field_value').first();
@@ -2567,14 +3296,14 @@ function ameOnDomReady() {
2567
  } else {
2568
  currentDropdownOwnerMenu = currentDropdownOwner.closest('.ws_container').data('menu_item');
2569
  }
2570
-
2571
  capSelectorDropdown.focus();
2572
 
2573
  capSuggestionFeature.show();
2574
  }
2575
 
2576
  //Also show it when the user presses the down arrow in the input field (doesn't work in Opera).
2577
- $('#ws_extra_capability').bind('keyup', function(event){
2578
  if ( event.which === 40 ){
2579
  $('#ws_trigger_capability_dropdown').trigger('click');
2580
  }
@@ -3511,92 +4240,106 @@ function ameOnDomReady() {
3511
  return false;
3512
  });
3513
 
 
 
3514
  /*************************************************************************
3515
  Menu toolbar buttons
3516
  *************************************************************************/
3517
  function getSelectedMenu() {
3518
- return $('#ws_menu_box').find('.ws_active');
3519
  }
3520
  AmeEditorApi.getSelectedMenu = getSelectedMenu;
3521
 
3522
  //Show/Hide menu
3523
- $('#ws_hide_menu').on('click', function (event) {
3524
- event.preventDefault();
 
 
 
 
 
 
 
 
 
 
3525
 
3526
- //Get the selected menu
3527
- var selection = getSelectedMenu();
3528
- if (!selection.length) {
3529
- return;
3530
  }
3531
-
3532
- toggleItemHiddenFlag(selection);
3533
- });
3534
 
3535
  //Hide a menu and deny access.
3536
- menuEditorNode.find('.ws_toolbar').on('click', '.ws_hide_and_deny_button', function() {
3537
- var $box = $(this).closest('.ws_main_container').find('.ws_box'),
3538
- selection = $box.is('#ws_menu_box') ? getSelectedMenu() : getSelectedSubmenuItem();
3539
- if (selection.length < 1) {
3540
- return;
3541
- }
 
 
 
 
 
 
3542
 
3543
- function objectFillKeys(keys, value) {
3544
- var result = {};
3545
- _.forEach(keys, function(key) {
3546
- result[key] = value;
3547
- });
3548
- return result;
3549
- }
3550
 
3551
- if (actorSelectorWidget.selectedActor === null) {
3552
- //Hide from everyone except Super Admin and the current user.
3553
- var menuItem = selection.data('menu_item'),
3554
- validActors = _.keys(wsEditorData.actors),
3555
- alwaysAllowedActors = _.intersection(
3556
- ['special:super_admin', 'user:' + wsEditorData.currentUserLogin],
3557
- validActors
3558
- ),
3559
- victims = _.difference(validActors, alwaysAllowedActors),
3560
- shouldHide;
3561
-
3562
- //First, lets check who has access. Maybe this item is already hidden from the victims.
3563
- shouldHide = _.some(victims, _.curry(actorCanAccessMenu, 2)(menuItem));
3564
-
3565
- var keepEnabled = objectFillKeys(alwaysAllowedActors, true),
3566
- hideAllExceptAllowed = _.assign(objectFillKeys(victims, false), keepEnabled);
3567
-
3568
- walkMenuTree(selection, function(container, item) {
3569
- var newAccess;
3570
- if (shouldHide) {
3571
- //Yay, hide it now!
3572
- newAccess = hideAllExceptAllowed;
3573
- //Only update had_access_before_hiding if this item isn't hidden yet or the field is missing.
3574
- //We don't want to double-hide an item.
3575
- var actorsWithAccess = _.filter(victims, function(actor) {
3576
- return actorCanAccessMenu(item, actor);
3577
- });
3578
- if ((actorsWithAccess.length) > 0 || _.isEmpty(_.get(item, 'had_access_before_hiding', null))) {
3579
- item.had_access_before_hiding = actorsWithAccess;
 
 
 
 
 
 
 
 
 
3580
  }
3581
- } else {
3582
- //Give back access to the roles and users who previously had access.
3583
- //Careful, don't give access to roles that no longer exist.
3584
- var actorsWhoHadAccess = _.get(item, 'had_access_before_hiding', []) || [];
3585
- actorsWhoHadAccess = _.intersection(actorsWhoHadAccess, validActors);
3586
-
3587
- newAccess = _.assign(objectFillKeys(actorsWhoHadAccess, true), keepEnabled);
3588
- delete item.had_access_before_hiding;
3589
- }
3590
 
3591
- setActorAccess(container, newAccess);
3592
- updateItemEditor(container);
3593
- });
3594
 
3595
- } else {
3596
- //Just toggle the checkbox.
3597
- selection.find('input.ws_actor_access_checkbox').trigger('click');
 
3598
  }
3599
- });
3600
 
3601
  //Delete error dialog. It shows up when the user tries to delete one of the default menus.
3602
  var menuDeletionDialog = $('#ws-ame-menu-deletion-error').dialog({
@@ -3675,47 +4418,13 @@ function ameOnDomReady() {
3675
  $('#ws_hide_menu_from_everyone').on('click', function() {
3676
  menuDeletionCallback('all');
3677
  });
3678
- $('#ws_hide_menu_except_current_user').on('click', function() {
3679
  menuDeletionCallback('except_current_user');
3680
  });
3681
- $('#ws_hide_menu_except_administrator').on('click', function() {
3682
  menuDeletionCallback('except_administrator');
3683
  });
3684
 
3685
- /**
3686
- * Check if it's possible to delete a menu item.
3687
- *
3688
- * @param {JQuery} containerNode
3689
- * @returns {boolean}
3690
- */
3691
- function canDeleteItem(containerNode) {
3692
- if (!containerNode || (containerNode.length < 1)) {
3693
- return false;
3694
- }
3695
-
3696
- var menuItem = containerNode.data('menu_item');
3697
- var isDefaultItem =
3698
- ( menuItem.template_id !== '')
3699
- && ( menuItem.template_id !== wsEditorData.unclickableTemplateId)
3700
- && ( menuItem.template_id !== wsEditorData.embeddedPageTemplateId)
3701
- && (!menuItem.separator);
3702
-
3703
- var otherCopiesExist = false;
3704
- if (isDefaultItem) {
3705
- //Check if there are any other menus with the same template ID.
3706
- $('#ws_menu_editor').find('.ws_container').each(function() {
3707
- var otherItem = $(this).data('menu_item');
3708
- if ((menuItem !== otherItem) && (menuItem.template_id === otherItem.template_id)) {
3709
- otherCopiesExist = true;
3710
- return false;
3711
- }
3712
- return true;
3713
- });
3714
- }
3715
-
3716
- return (!isDefaultItem || otherCopiesExist);
3717
- }
3718
-
3719
  /**
3720
  * Attempt to delete a menu item. Will check if the item can actually be deleted and ask the user for confirmation.
3721
  * UI callback.
@@ -3738,8 +4447,8 @@ function ameOnDomReady() {
3738
 
3739
  //Different versions get slightly different options because only the Pro version has
3740
  //role-specific permissions.
3741
- $('#ws_hide_menu_except_current_user').toggleClass('hidden', !wsEditorData.wsMenuEditorPro);
3742
- $('#ws_hide_menu_except_administrator').toggleClass('hidden', wsEditorData.wsMenuEditorPro);
3743
 
3744
  menuDeletionDialog.dialog('open');
3745
 
@@ -3748,17 +4457,12 @@ function ameOnDomReady() {
3748
  }
3749
 
3750
  if (shouldDelete) {
3751
- //Delete this menu's submenu first, if any.
3752
- var submenuId = selection.data('submenu_id');
3753
- if (submenuId) {
3754
- $('#' + submenuId).remove();
3755
- }
3756
- var parentSubmenu = selection.closest('.ws_submenu');
3757
 
3758
  //Delete the menu.
3759
- selection.remove();
3760
 
3761
- if (parentSubmenu) {
3762
  //Refresh permissions UI for this menu's parent (if any).
3763
  updateParentAccessUi(parentSubmenu);
3764
  }
@@ -3766,172 +4470,204 @@ function ameOnDomReady() {
3766
  }
3767
 
3768
  //Delete menu
3769
- $('#ws_delete_menu').on('click', function (event) {
3770
- event.preventDefault();
 
 
 
 
 
 
 
 
 
 
3771
 
3772
- //Get the selected menu
3773
- var selection = getSelectedMenu();
3774
- if (!selection.length) {
3775
- return;
3776
  }
3777
-
3778
- tryDeleteItem(selection);
3779
- });
3780
 
3781
  //Copy menu
3782
- $('#ws_copy_menu').on('click', function (event) {
3783
- event.preventDefault();
3784
 
3785
- //Get the selected menu
3786
- var selection = $('#ws_menu_box').find('.ws_active');
3787
- if (!selection.length) {
3788
- return;
3789
- }
 
 
 
 
3790
 
3791
- //Store a copy of the current menu state in clipboard
3792
- menu_in_clipboard = readItemState(selection);
3793
- });
 
3794
 
3795
  //Cut menu
3796
- $('#ws_cut_menu').on('click', function (event) {
3797
- event.preventDefault();
3798
 
3799
- //Get the selected menu
3800
- var selection = $('#ws_menu_box').find('.ws_active');
3801
- if (!selection.length) {
3802
- return;
3803
- }
 
 
 
 
 
 
3804
 
3805
- //Store a copy of the current menu state in clipboard
3806
- menu_in_clipboard = readItemState(selection);
3807
 
3808
- //Remove the original menu and submenu
3809
- $('#'+selection.data('submenu_id')).remove();
3810
- selection.remove();
3811
- });
3812
 
3813
- //Paste menu
3814
- function pasteMenu(menu, afterMenu) {
3815
- //The user shouldn't need to worry about giving separators a unique filename.
3816
- if (menu.separator) {
3817
- menu.defaults.file = randomMenuId('separator_');
3818
  }
 
3819
 
3820
- //If we're pasting from a sub-menu, we may need to fix some properties
3821
- //that are blank for sub-menu items but required for top-level menus.
3822
- if (getFieldValue(menu, 'css_class', '') == '') {
3823
- menu.css_class = 'menu-top';
3824
- }
3825
- if (getFieldValue(menu, 'icon_url', '') == '') {
3826
- menu.icon_url = 'dashicons-admin-generic';
3827
- }
3828
- if (getFieldValue(menu, 'hookname', '') == '') {
3829
- menu.hookname = randomMenuId();
3830
- }
 
3831
 
3832
- //Paste the menu after the specified one, or at the end of the list.
3833
- if (afterMenu) {
3834
- return outputTopMenu(menu, afterMenu);
3835
- } else {
3836
- return outputTopMenu(menu);
3837
- }
3838
- }
3839
 
3840
- $('#ws_paste_menu').on('click', function (event) {
3841
- event.preventDefault();
3842
 
3843
- //Check if anything has been copied/cut
3844
- if (!menu_in_clipboard) {
3845
- return;
3846
  }
3847
-
3848
- var menu = $.extend(true, {}, menu_in_clipboard);
3849
-
3850
- //Get the selected menu
3851
- var selection = $('#ws_menu_box').find('.ws_active');
3852
- //Paste the menu after the selection.
3853
- pasteMenu(menu, (selection.length > 0) ? selection : null);
3854
- });
3855
 
3856
  //New menu
3857
- $('#ws_new_menu').on('click', function (event) {
3858
- event.preventDefault();
 
 
 
 
 
 
 
 
 
 
 
 
3859
 
3860
- ws_paste_count++;
3861
 
3862
- //The new menu starts out rather bare
3863
- var randomId = randomMenuId();
3864
- var menu = $.extend(true, {}, wsEditorData.blankMenuItem, {
3865
- custom: true, //Important : flag the new menu as custom, or it won't show up after saving.
3866
- template_id : '',
3867
- menu_title : 'Custom Menu ' + ws_paste_count,
3868
- file : randomId,
3869
- items: []
3870
- });
3871
- menu.defaults = $.extend(true, {}, itemTemplates.getDefaults(''));
3872
 
3873
- //Make it accessible only to the current actor if one is selected.
3874
- if (actorSelectorWidget.selectedActor !== null) {
3875
- denyAccessForAllExcept(menu, actorSelectorWidget.selectedActor);
3876
- }
3877
 
3878
- //Insert the new menu
3879
- var selection = $('#ws_menu_box').find('.ws_active');
3880
- var result = outputTopMenu(menu, (selection.length > 0) ? selection : null);
 
 
 
3881
 
3882
- //The menus's editbox is always open
3883
- result.menu.find('.ws_edit_link').trigger('click');
3884
- });
3885
 
3886
- //New separator
3887
- $('#ws_new_separator, #ws_new_submenu_separator').on('click', function (event) {
3888
- event.preventDefault();
 
3889
 
3890
- ws_paste_count++;
3891
-
3892
- //The new menu starts out rather bare
3893
- var randomId = randomMenuId('separator_');
3894
- var menu = $.extend(true, {}, wsEditorData.blankMenuItem, {
3895
- separator: true, //Flag as a separator
3896
- custom: false, //Separators don't need to flagged as custom to be retained.
3897
- items: [],
3898
- defaults: {
3899
- separator: true,
3900
- css_class : 'wp-menu-separator',
3901
- access_level : 'read',
3902
- file : randomId,
3903
- hookname : randomId
3904
  }
3905
- });
3906
 
3907
- if ( $(this).attr('id').indexOf('submenu') === -1 ) {
3908
- //Insert in the top-level menu.
3909
- var selection = $('#ws_menu_box').find('.ws_active');
3910
- outputTopMenu(menu, (selection.length > 0) ? selection : null);
3911
- } else {
3912
- //Insert in the currently visible submenu.
3913
- pasteItem(menu);
 
 
 
 
 
 
 
 
 
 
 
3914
  }
3915
- });
3916
 
3917
  //Toggle all menus for the currently selected actor
3918
- $('#ws_toggle_all_menus').on('click', function(event) {
3919
- event.preventDefault();
 
 
 
 
 
 
 
 
3920
 
3921
- if ( actorSelectorWidget.selectedActor === null ) {
3922
- alert("This button enables/disables all menus for the selected role. To use it, click a role and then click this button again.");
3923
- return;
3924
- }
3925
 
3926
- var topMenuNodes = $('.ws_menu', '#ws_menu_box');
3927
- //Look at the first menu's permissions and set everything to the opposite.
3928
- var allow = ! actorCanAccessMenu(topMenuNodes.eq(0).data('menu_item'), actorSelectorWidget.selectedActor);
3929
 
3930
- topMenuNodes.each(function() {
3931
- var containerNode = $(this);
3932
- setActorAccessForTreeAndUpdateUi(containerNode, actorSelectorWidget.selectedActor, allow);
3933
- });
3934
- });
 
3935
 
3936
  //Copy all menu permissions from one role to another.
3937
  var copyPermissionsDialog = $('#ws-ame-copy-permissions-dialog').dialog({
@@ -3944,38 +4680,44 @@ function ameOnDomReady() {
3944
  var sourceActorList = $('#ame-copy-source-actor'), destinationActorList = $('#ame-copy-destination-actor');
3945
 
3946
  //The "Copy permissions" toolbar button.
3947
- $('#ws_copy_role_permissions').on('click', function(event) {
3948
- event.preventDefault();
3949
-
3950
- var previousSource = sourceActorList.val();
3951
-
3952
- //Populate source/destination lists.
3953
- sourceActorList.find('option').not('[disabled]').remove();
3954
- destinationActorList.find('option').not('[disabled]').remove();
3955
- $.each(actorSelectorWidget.getVisibleActors(), function(index, actor) {
3956
- var option = $('<option>', {
3957
- val: actor.id,
3958
- text: actorSelectorWidget.getNiceName(actor)
 
 
 
 
 
 
 
 
3959
  });
3960
- sourceActorList.append(option);
3961
- destinationActorList.append(option.clone());
3962
- });
3963
 
3964
- //Pre-select the current actor as the destination.
3965
- if (actorSelectorWidget.selectedActor !== null) {
3966
- destinationActorList.val(actorSelectorWidget.selectedActor);
3967
- }
3968
 
3969
- //Restore the previous source selection.
3970
- if (previousSource) {
3971
- sourceActorList.val(previousSource);
3972
- }
3973
- if (!sourceActorList.val()) {
3974
- sourceActorList.find('option').first().prop('selected', true); //Fallback.
3975
- }
3976
 
3977
- copyPermissionsDialog.dialog('open');
3978
- });
 
3979
 
3980
  //Actually copy the permissions when the user click the confirmation button.
3981
  var copyConfirmationButton = $('#ws-ame-confirm-copy-permissions');
@@ -3989,15 +4731,11 @@ function ameOnDomReady() {
3989
  }
3990
 
3991
  //Iterate over all menu items and copy the permissions from one actor to the other.
3992
- var allMenuNodes = $('.ws_menu', '#ws_menu_box').add('.ws_item', submenuBox);
3993
- allMenuNodes.each(function() {
3994
- var node = $(this);
3995
- var menuItem = node.data('menu_item');
3996
-
3997
  //Only change permissions when they don't match. This ensures we won't unnecessarily overwrite default
3998
  //permissions and bloat the configuration with extra grant_access entries.
3999
- var sourceAccess = actorCanAccessMenu(menuItem, sourceActor);
4000
- var destinationAccess = actorCanAccessMenu(menuItem, destinationActor);
4001
  if (sourceAccess !== destinationAccess) {
4002
  setActorAccess(node, destinationActor, sourceAccess);
4003
  //Note: In theory, we could also look at the default permissions for destinationActor and
@@ -4030,30 +4768,44 @@ function ameOnDomReady() {
4030
  });
4031
 
4032
  //Sort menus in ascending or descending order.
4033
- menuEditorNode.find('.ws_toolbar').on('click', '.ws_sort_menus_button', function(event) {
4034
- event.preventDefault();
4035
-
4036
- var button = $(this),
4037
- direction = button.data('sort-direction') || 'asc',
4038
- menuBox = $(this).closest('.ws_main_container').find('.ws_box').first();
 
 
 
 
 
4039
 
4040
- if (menuBox.is('#ws_submenu_box')) {
4041
- menuBox = menuBox.find('.ws_submenu:visible').first();
4042
- }
4043
 
4044
- if (menuBox.length > 0) {
4045
- sortMenuItems(menuBox, direction);
4046
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4047
 
4048
- //When sorting the top level menu also sort submenus, but leave the first item unmoved.
4049
- //Moving the first item would change the parent menu URL (WP always links it to the first item),
4050
- //which can be unexpected and confusing. The user can always move the first item manually.
4051
- if (menuBox.is('#ws_menu_box')) {
4052
- $('#ws_submenu_box').find('.ws_submenu').each(function() {
4053
- sortMenuItems($(this), direction, true);
4054
- });
4055
  }
4056
- });
4057
 
4058
  /**
4059
  * Sort menu items by title.
@@ -4105,173 +4857,28 @@ function ameOnDomReady() {
4105
  }
4106
 
4107
  //Toggle the second row of toolbar buttons.
4108
- $('#ws_toggle_toolbar').on('click', function() {
4109
- var visible = menuEditorNode.find('.ws_second_toolbar_row').toggle().is(':visible');
4110
- if (typeof $['cookie'] !== 'undefined') {
4111
- $.cookie('ame-show-second-toolbar', visible ? '1' : '0', {expires: 90});
 
 
 
 
 
 
4112
  }
4113
- });
4114
 
4115
 
4116
  /*************************************************************************
4117
  Item toolbar buttons
4118
  *************************************************************************/
4119
  function getSelectedSubmenuItem() {
4120
- return $('#ws_submenu_box').find('.ws_submenu:visible .ws_active');
4121
- }
4122
-
4123
- //Show/Hide item
4124
- $('#ws_hide_item').on('click', function (event) {
4125
- event.preventDefault();
4126
-
4127
- //Get the selected item
4128
- var selection = getSelectedSubmenuItem();
4129
- if (!selection.length) {
4130
- return;
4131
- }
4132
-
4133
- //Mark the item as hidden/visible
4134
- toggleItemHiddenFlag(selection);
4135
- });
4136
-
4137
- //Delete item
4138
- $('#ws_delete_item').on('click', function (event) {
4139
- event.preventDefault();
4140
-
4141
- var selection = getSelectedSubmenuItem();
4142
- if (!selection.length) {
4143
- return;
4144
- }
4145
-
4146
- tryDeleteItem(selection);
4147
- });
4148
-
4149
- //Copy item
4150
- $('#ws_copy_item').on('click', function (event) {
4151
- event.preventDefault();
4152
-
4153
- //Get the selected item
4154
- var selection = getSelectedSubmenuItem();
4155
- if (!selection.length) {
4156
- return;
4157
- }
4158
-
4159
- //Store a copy of item state in the clipboard
4160
- menu_in_clipboard = readItemState(selection);
4161
- });
4162
-
4163
- //Cut item
4164
- $('#ws_cut_item').on('click', function (event) {
4165
- event.preventDefault();
4166
-
4167
- //Get the selected item
4168
- var selection = getSelectedSubmenuItem();
4169
- if (!selection.length) {
4170
- return;
4171
- }
4172
-
4173
- //Store a copy of item state in the clipboard
4174
- menu_in_clipboard = readItemState(selection);
4175
-
4176
- var submenu = selection.parent();
4177
- //Remove the original item
4178
- selection.remove();
4179
- updateParentAccessUi(submenu);
4180
- });
4181
-
4182
- //Paste item
4183
- function pasteItem(item, targetSubmenu) {
4184
- //We're pasting this item into a sub-menu, so it can't have a sub-menu of its own.
4185
- //Instead, any sub-menu items belonging to this item will be pasted after the item.
4186
- var newItems = [];
4187
- for (var file in item.items) {
4188
- if (item.items.hasOwnProperty(file)) {
4189
- newItems.push(buildMenuItem(item.items[file], false));
4190
- }
4191
- }
4192
- item.items = [];
4193
-
4194
- newItems.unshift(buildMenuItem(item, false));
4195
-
4196
- //Paste into the currently visible submenu by default.
4197
- targetSubmenu = targetSubmenu || $('#ws_submenu_box').find('.ws_submenu:visible');
4198
- //Get the selected menu
4199
- var selection = targetSubmenu.find('.ws_active');
4200
- for(var i = 0; i < newItems.length; i++) {
4201
- if (selection.length > 0) {
4202
- //If an item is selected add the pasted items after it
4203
- selection.after(newItems[i]);
4204
- } else {
4205
- //Otherwise add the pasted items at the end
4206
- targetSubmenu.append(newItems[i]);
4207
- }
4208
-
4209
- updateItemEditor(newItems[i]);
4210
- newItems[i].show();
4211
- }
4212
-
4213
- updateParentAccessUi(targetSubmenu);
4214
  }
4215
 
4216
- $('#ws_paste_item').on('click', function (event) {
4217
- event.preventDefault();
4218
-
4219
- //Check if anything has been copied/cut
4220
- if (!menu_in_clipboard) {
4221
- return;
4222
- }
4223
-
4224
- //You can only add separators to submenus in the Pro version.
4225
- if ( menu_in_clipboard.separator && !wsEditorData.wsMenuEditorPro ) {
4226
- return;
4227
- }
4228
-
4229
- //Paste it.
4230
- var item = $.extend(true, {}, menu_in_clipboard);
4231
- pasteItem(item);
4232
- });
4233
-
4234
- //New item
4235
- $('#ws_new_item').on('click', function (event) {
4236
- event.preventDefault();
4237
-
4238
- if ($('.ws_submenu:visible').length < 1) {
4239
- return; //Abort if no submenu visible
4240
- }
4241
-
4242
- ws_paste_count++;
4243
-
4244
- var entry = $.extend(true, {}, wsEditorData.blankMenuItem, {
4245
- custom: true,
4246
- template_id : '',
4247
- menu_title : 'Custom Item ' + ws_paste_count,
4248
- file : randomMenuId(),
4249
- items: []
4250
- });
4251
- entry.defaults = $.extend(true, {}, itemTemplates.getDefaults(''));
4252
-
4253
- //Make it accessible to only the currently selected actor.
4254
- if (actorSelectorWidget.selectedActor !== null) {
4255
- denyAccessForAllExcept(entry, actorSelectorWidget.selectedActor);
4256
- }
4257
-
4258
- var menu = buildMenuItem(entry);
4259
-
4260
- //Insert the item into the currently open submenu.
4261
- var visibleSubmenu = $('#ws_submenu_box').find('.ws_submenu:visible');
4262
- var selection = visibleSubmenu.find('.ws_active');
4263
- if (selection.length > 0) {
4264
- selection.after(menu);
4265
- } else {
4266
- visibleSubmenu.append(menu);
4267
- }
4268
- updateItemEditor(menu);
4269
-
4270
- //The items's editbox is always open
4271
- menu.find('.ws_edit_link').trigger('click');
4272
-
4273
- updateParentAccessUi(menu);
4274
- });
4275
 
4276
  //==============================================
4277
  // Main buttons
@@ -4337,6 +4944,8 @@ function ameOnDomReady() {
4337
  $('#ws_data_length').val(data.length);
4338
  $('#ws_selected_actor').val(actorSelectorWidget.selectedActor === null ? '' : actorSelectorWidget.selectedActor);
4339
 
 
 
4340
  var selectedMenu = getSelectedMenu();
4341
  if (selectedMenu.length > 0) {
4342
  $('#ws_selected_menu_url').val(AmeEditorApi.getItemDisplayUrl(selectedMenu.data('menu_item')));
@@ -4464,6 +5073,7 @@ function ameOnDomReady() {
4464
  closeText: ' ',
4465
  modal: true
4466
  });
 
4467
 
4468
  $('#ws_cancel_import').on('click', function(){
4469
  $('#import_dialog').dialog('close');
@@ -4472,7 +5082,7 @@ function ameOnDomReady() {
4472
  $('#ws_import_menu').on('click', function(){
4473
  $('#import_progress_notice, #import_progress_notice2, #import_complete_notice, #ws_import_error').hide();
4474
  $('#ws_import_panel').show();
4475
- $('#import_menu_form').resetForm();
4476
  //The "Upload" button is disabled until the user selects a file
4477
  $('#ws_start_import').attr('disabled', 'disabled');
4478
 
@@ -4490,7 +5100,7 @@ function ameOnDomReady() {
4490
  function handleUnexpectedImportError(xhr, errorMessage) {
4491
  //The server-side code didn't catch this error, so it's probably something serious
4492
  //and retrying won't work.
4493
- $('#import_menu_form').resetForm();
4494
  $('#ws_import_panel').hide();
4495
 
4496
  //Display error information.
@@ -4501,7 +5111,7 @@ function ameOnDomReady() {
4501
  }
4502
 
4503
  //AJAXify the upload form
4504
- $('#import_menu_form').ajaxForm({
4505
  dataType : 'json',
4506
  beforeSubmit: function(formData) {
4507
 
@@ -4539,7 +5149,7 @@ function ameOnDomReady() {
4539
  if ( typeof data.error !== 'undefined' ){
4540
  alert(data.error);
4541
  //Let the user try again
4542
- $('#import_menu_form').resetForm();
4543
  importDialog.find('.hide-when-uploading').show();
4544
  }
4545
 
@@ -4578,53 +5188,35 @@ function ameOnDomReady() {
4578
  }),
4579
 
4580
  'drop' : (function(event, ui){
4581
- var droppedItemData = readItemState(ui.draggable);
4582
- var newItemNodes = pasteMenu(droppedItemData);
 
 
 
 
 
 
4583
 
4584
  //If the item was originally a top level menu, also move its original submenu items.
4585
- if (getFieldValue(droppedItemData, 'parent') === null) {
4586
- var droppedItemFile = getFieldValue(droppedItemData, 'file');
4587
- var nearbyItems = $(ui.draggable).siblings('.ws_item');
4588
  nearbyItems.each(function() {
4589
- var containerNode = $(this),
4590
  submenuItem = containerNode.data('menu_item');
4591
 
4592
  //Was this item originally a child of the dragged menu?
4593
  if (getFieldValue(submenuItem, 'parent') === droppedItemFile) {
4594
- pasteItem(submenuItem, newItemNodes.submenu);
4595
  if ( !event.ctrlKey ) {
4596
- containerNode.remove();
4597
  }
4598
  }
4599
  });
4600
  }
4601
 
4602
  if ( !event.ctrlKey ) {
4603
- ui.draggable.remove();
4604
- }
4605
- })
4606
- });
4607
-
4608
- //...and to drag top level menus to a sub-menu.
4609
- submenuBox.closest('.ws_main_container').droppable({
4610
- 'hoverClass' : 'ws_top_to_submenu_drop_hover',
4611
-
4612
- 'accept' : (function(thing){
4613
- var visibleSubmenu = $('#ws_submenu_box').find('.ws_submenu:visible');
4614
- return (
4615
- //Accept top-level menus
4616
- thing.hasClass('ws_menu') &&
4617
-
4618
- //Prevent users from dropping a menu on its own sub-menu.
4619
- (visibleSubmenu.attr('id') !== thing.data('submenu_id'))
4620
- );
4621
- }),
4622
-
4623
- 'drop' : (function(event, ui){
4624
- var droppedItemData = readItemState(ui.draggable);
4625
- pasteItem(droppedItemData);
4626
- if ( !event.ctrlKey ) {
4627
- ui.draggable.remove();
4628
  }
4629
  })
4630
  });
@@ -5098,7 +5690,7 @@ var domCheckIntervalId = window.setInterval(function () {
5098
  domCheckAttempts++;
5099
 
5100
  if ($ && $.isReady) {
5101
- isDomReadyDone = true;
5102
  ameOnDomReady();
5103
  }
5104
  }, 1000);
49
  * @property {string} wsEditorData.setTestConfigurationNonce
50
  * @property {string} wsEditorData.testAccessNonce
51
  *
52
+ * @property {string|null} wsEditorData.deepNestingEnabled
53
+ *
54
  * @property {boolean} wsEditorData.isDemoMode
55
  * @property {boolean} wsEditorData.isMasterMode
56
  */
193
  }
194
  };
195
 
196
+ /**
197
+ * @type {AmeMenuPresenter}
198
+ */
199
+ let menuPresenter;
200
+
201
  /**
202
  * Set an input field to a value. The only difference from jQuery.val() is that
203
  * setting a checkbox to true/false will check/clear it.
248
  AmeEditorApi.randomMenuId = randomMenuId;
249
 
250
  function outputWpMenu(menu){
251
+ const menuCopy = $.extend(true, {}, menu);
 
252
 
253
  //Remove the current menu data
254
+ menuPresenter.clear();
 
255
 
256
  //Display the new menu
257
+ const firstColumn = menuPresenter.getColumnImmediate(1);
258
+ const itemList = firstColumn.getVisibleItemList();
259
+ for (let filename in menuCopy){
260
  if (!menuCopy.hasOwnProperty(filename)){
261
  continue;
262
  }
263
+ firstColumn.outputItem(menuCopy[filename], null, itemList);
 
264
  }
265
 
266
  //Automatically select the first top-level menu
267
+ if (itemList) {
268
+ itemList.find('.ws_menu:first').trigger('click');
269
+ }
270
  }
271
 
272
  /**
318
  $(document).trigger('menuConfigurationLoaded.adminMenuEditor', adminMenu);
319
  }
320
 
321
+ /**
322
+ * Check if it's possible to delete a menu item.
323
+ *
324
+ * @param {JQuery} containerNode
325
+ * @returns {boolean}
326
+ */
327
+ function canDeleteItem(containerNode) {
328
+ if (!containerNode || (containerNode.length < 1)) {
329
+ return false;
330
+ }
 
 
 
331
 
332
+ var menuItem = containerNode.data('menu_item');
333
+ var isDefaultItem =
334
+ ( menuItem.template_id !== '')
335
+ && ( menuItem.template_id !== wsEditorData.unclickableTemplateId)
336
+ && ( menuItem.template_id !== wsEditorData.embeddedPageTemplateId)
337
+ && (!menuItem.separator);
338
+
339
+ var otherCopiesExist = false;
340
+ if (isDefaultItem) {
341
+ //Check if there are any other menus with the same template ID.
342
+ $('#ws_menu_editor').find('.ws_container').each(function() {
343
+ var otherItem = $(this).data('menu_item');
344
+ if ((menuItem !== otherItem) && (menuItem.template_id === otherItem.template_id)) {
345
+ otherCopiesExist = true;
346
+ return false;
347
+ }
348
+ return true;
349
+ });
350
+ }
351
+
352
+ return (!isDefaultItem || otherCopiesExist);
353
  }
354
 
355
+ /**
356
+ * Get or create the submenu container of a menu item.
357
+ *
358
+ * @param {JQuery|null} container
359
+ * @param {AmeEditorColumn} [nextColumn]
360
+ * @return {JQuery|null}
361
+ */
362
+ function getSubmenuOf(container, nextColumn) {
363
+ if (!container || (container.length < 1)) {
364
+ return null;
365
+ }
366
 
367
+ const submenuId = container.data('submenu_id');
368
+ if (submenuId) {
369
+ let $submenu = $('#' + submenuId).first();
370
+ if ($submenu.length > 0) {
371
+ return $submenu;
372
+ }
373
+ }
374
 
375
+ //If a submenu doesn't exist yet, create it in the next column.
376
+ if (nextColumn) {
377
+ return createSubmenuFor(container, nextColumn);
378
+ } else {
379
+ return null;
380
+ }
381
+ }
382
 
383
+ /**
384
+ * Create a submenu container for a menu item.
385
+ * @param {JQuery} container
386
+ * @param {AmeEditorColumn} nextColumn
387
+ * @return {JQuery}
388
+ */
389
+ function createSubmenuFor(container, nextColumn) {
390
+ const $submenu = nextColumn.buildSubmenuContainer(container.attr('id'));
391
+ nextColumn.appendSubmenuContainer($submenu);
392
+ container.data('submenu_id', $submenu.attr('id'))
393
+ return $submenu;
394
+ }
395
+
396
+ /**
397
+ * @param {Number} level
398
+ * @param {JQuery|null} predecessor
399
+ * @param {JQuery|null} [container]
400
+ * @param {Function} [getNextColumn]
401
+ * @constructor
402
+ */
403
+ function AmeEditorColumn(level, predecessor, container, getNextColumn) {
404
+ const self = this;
405
+
406
+ this.level = level;
407
+ this.usesSubmenuContainers = (this.level > 1);
408
+
409
+ let isNewContainer = false;
410
+ if ((typeof container === 'undefined') || (container === null)) {
411
+ isNewContainer = true;
412
+ container = $('#ame-submenu-column-template').first().clone();
413
+ container.attr('id', '');
414
+ container.find('.ws_box').first().attr('id', '');
415
+ container.show().insertAfter(predecessor);
416
+ }
417
+ container.data('ame-menu-level', level);
418
+ container.addClass('ame-editor-column-' + level);
419
+
420
+ this.container = container;
421
+ this.menuBox = container.find('.ws_box').first();
422
+ this.dropZone = container.children('.ws_dropzone').first();
423
+ this.visibleItemList = null;
424
+
425
+ if (!this.usesSubmenuContainers) {
426
+ this.menuBox.addClass('ame-visible-item-list');
427
+ }
428
+
429
+ if (typeof getNextColumn !== 'undefined') {
430
+ this.getNextColumn = getNextColumn;
431
+ } else {
432
+ this.getNextColumn = function(callback) {
433
+ callback(null);
434
+ };
435
+ }
436
 
437
+ this.container.children('.ws_toolbar').on('click', '.ws_button', function() {
438
+ const $button = $(this);
439
+ let buttonAction = $button.data('ame-button-action') || 'unknown';
440
+ let selectedItem = self.getSelectedItem();
441
+ self.container.trigger(
442
+ 'adminMenuEditor:action-' + buttonAction,
443
+ [(selectedItem.length > 0) ? selectedItem : null, self, $button]
444
+ );
445
+ return false;
446
+ });
447
+
448
+ if (isNewContainer && (this.dropZone.length > 0)) {
449
+ this.container.closest('.ws_main_container').droppable({
450
+ 'hoverClass' : 'ws_top_to_submenu_drop_hover',
451
+
452
+ 'accept' : (function(thing) {
453
+ const visibleSubmenu = self.getVisibleItemList();
454
+ if (!visibleSubmenu || (visibleSubmenu.length < 1)) {
455
+ return false; //Can't drop anything on a non-existent submenu.
456
+ }
457
+
458
+ function isParentOf(menuItem, something) {
459
+ const parent = getParentMenuNode(something)
460
+ if (menuItem.is(parent)) {
461
+ return true;
462
+ } else if (parent.length > 0) {
463
+ return isParentOf(menuItem, parent);
464
+ }
465
+ return false;
466
+ }
467
+
468
+ const thingContainer = thing.closest('.ws_main_container');
469
+ return (
470
+ //Accept only menus from other columns.
471
+ !self.container.is(thingContainer) &&
472
+
473
+ //Prevent users from dropping a parent menu on one of its own sub-menus.
474
+ !isParentOf(thing, visibleSubmenu)
475
+ );
476
+ }),
477
+
478
+ 'drop' : (function(event, ui){
479
+ const droppedItemData = readItemState(ui.draggable);
480
+ self.pasteItem(droppedItemData, null);
481
+ if ( !event.ctrlKey ) {
482
+ self.destroyItem(ui.draggable);
483
+ }
484
+ })
485
+ });
486
+ }
487
  }
488
 
489
+ /**
490
+ * Create editor widgets for a menu item and its submenus.
491
+ *
492
+ * @param {Object} itemData An object containing menu data.
493
+ * @param {JQuery|null} [afterNode] Insert the widget after this node. If it's NULL, the widget
494
+ * will be added to the end fo the list.
495
+ * @param {JQuery} [itemList] The container where to insert the widget. Defaults to the currently
496
+ * visible item list. For columns that don't use submenu containers, it's always the menuBox.
497
+ * @return {Object} Object with two fields - 'menu' and 'submenu' - containing the jQuery objects
498
+ * of the created widgets.
499
+ */
500
+ AmeEditorColumn.prototype.outputItem = function(itemData, afterNode, itemList) {
501
+ if (!itemList) {
502
+ itemList = this.getVisibleItemList();
503
+ }
504
+ const self = this;
505
+
506
+ //Create the menu widget
507
+ const isTopLevel = this.level <= 1;
508
+ const $item = buildMenuItem(itemData, isTopLevel);
509
+
510
+ if ((typeof afterNode !== 'undefined') && (afterNode !== null)) {
511
+ $(afterNode).after($item);
512
+ } else {
513
+ $item.appendTo(itemList);
514
+ }
515
+
516
+ const children = (typeof itemData.items !== 'undefined') ? itemData.items : [];
517
+ const hasChildren = !_.isEmpty(children);
518
+ let $submenu = null;
519
+
520
+ this.getNextColumn(
521
+ /**
522
+ * @param {AmeEditorColumn|null} nextColumn
523
+ */
524
+ function (nextColumn) {
525
+ if (nextColumn) {
526
+ //Create a submenu container even if this item doesn't have children.
527
+ //The user could add submenu items later.
528
+ $submenu = createSubmenuFor($item, nextColumn);
529
+
530
+ //Output children.
531
+ if (hasChildren) {
532
+ $.each(children, function (index, item) {
533
+ nextColumn.outputItem(item, null, $submenu);
534
+ });
535
+ }
536
+ } else {
537
+ //TODO: This branch could be optimized by letting the recursive outputItem call know that there is no next column.
538
+ //There is no next column, so any submenu items that belong to this item will be
539
+ //displayed in the same column, below the item.
540
+ if (hasChildren) {
541
+ let $previousItem = $item;
542
+ $.each(children, function (index, child) {
543
+ const result = self.outputItem(child, $previousItem, itemList);
544
+ if (result && result.menu) {
545
+ $previousItem = result.menu;
546
+ }
547
+ });
548
+ }
549
+ }
550
+
551
+ //Note: Update the menu only after its children are ready. It needs the submenu items to decide
552
+ //whether to display the access checkbox as checked or indeterminate.
553
+ updateItemEditor($item);
554
+ },
555
+ hasChildren
556
+ );
557
+
558
+ //Note that $submenu could still be NULL at this point if the "get next column" callback
559
+ //is called asynchronously.
560
+ return {
561
+ 'menu': $item,
562
+ 'submenu': $submenu
563
+ };
564
+ };
565
+
566
+ /**
567
+ * Paste a menu item in this column.
568
+ *
569
+ * @param {Object} item
570
+ * @param {JQuery|null} [afterItem] Defaults to the current selection. Set to NULL to paste at the end of the list.
571
+ * @param {JQuery} [itemList]
572
+ */
573
+ AmeEditorColumn.prototype.pasteItem = function(item, afterItem, itemList) {
574
+ if (typeof afterItem === 'undefined') {
575
+ afterItem = this.getSelectedItem();
576
+ if (afterItem.length < 1) {
577
+ afterItem = null;
578
  }
579
+ }
580
+
581
+ if (!itemList) {
582
+ itemList = this.getVisibleItemList();
583
+ }
584
+
585
+ //The user shouldn't need to worry about giving separators a unique filename.
586
+ if (item.separator) {
587
+ item.defaults.file = randomMenuId('separator_');
588
+ }
589
+
590
+ //If we're pasting from a sub-menu into the top level, we may need to fix some properties
591
+ //that are blank for sub-menu items but required for top level menus.
592
+ const isTopLevel = this.level <= 1;
593
+ if (isTopLevel) {
594
+ function isNonEmptyString(value) {
595
+ return (typeof value === 'string') && (value !== '');
596
+ }
597
+
598
+ if (!isNonEmptyString(getFieldValue(item, 'css_class', ''))) {
599
+ item.css_class = 'menu-top';
600
+ }
601
+ if (!isNonEmptyString(getFieldValue(item, 'icon_url', ''))) {
602
+ item.icon_url = 'dashicons-admin-generic';
603
+ }
604
+ if (!isNonEmptyString(getFieldValue(item, 'hookname', ''))) {
605
+ item.hookname = randomMenuId();
606
+ }
607
+ }
608
+
609
+ const result = this.outputItem(item, afterItem, itemList);
610
+
611
+ if (this.level > 1) {
612
+ updateParentAccessUi(itemList);
613
+ }
614
+
615
+ return result;
616
+ };
617
+
618
+ /**
619
+ * @return {JQuery|null}
620
+ */
621
+ AmeEditorColumn.prototype.getVisibleItemList = function() {
622
+ if (this.usesSubmenuContainers) {
623
+ if (this.visibleItemList) {
624
+ return this.visibleItemList;
625
+ }
626
+
627
+ const $list = this.menuBox.children('.ws_submenu:visible').first().addClass('ame-visible-item-list');
628
+ if ($list && ($list.length > 0)) {
629
+ this.visibleItemList = $list;
630
+ }
631
+ return $list;
632
+ } else {
633
+ return this.menuBox;
634
+ }
635
+ };
636
+
637
+ /**
638
+ * @param {JQuery|null} $submenu
639
+ */
640
+ AmeEditorColumn.prototype.setVisibleItemList = function($submenu) {
641
+ //Do nothing if the new list is the same as the old one.
642
+ if (($submenu === this.visibleItemList) || ($submenu && ($submenu.is(this.visibleItemList)))) {
643
+ return;
644
+ }
645
+
646
+ if (this.visibleItemList) {
647
+ this.visibleItemList.hide().removeClass('ame-visible-item-list');
648
+ }
649
+ this.visibleItemList = $submenu;
650
+
651
+ if (this.visibleItemList) {
652
+ this.visibleItemList.show().addClass('ame-visible-item-list');
653
+ }
654
+
655
+ //Each item list/submenu has its own own selected item, so switching to a different item list
656
+ //also effectively changes the selected item.
657
+ this.selectionHasChanged();
658
+ };
659
+
660
+ /**
661
+ * @return {JQuery}
662
+ */
663
+ AmeEditorColumn.prototype.getAllItemLists = function() {
664
+ if (this.usesSubmenuContainers) {
665
+ return this.menuBox.children('.ws_submenu');
666
+ }
667
+ return this.menuBox;
668
+ };
669
+
670
+ /**
671
+ * @return {JQuery}
672
+ */
673
+ AmeEditorColumn.prototype.getSelectedItem = function() {
674
+ const list = this.getVisibleItemList();
675
+ if (list && (list.length > 0)) {
676
+ return list.children('.ws_active').first();
677
+ }
678
+ return $([]);
679
+ };
680
+
681
+ /**
682
+ * @param {JQuery} container
683
+ */
684
+ AmeEditorColumn.prototype.selectItem = function(container) {
685
+ if (container.hasClass('ws_active')) {
686
+ //The menu item is already selected.
687
+ return;
688
+ }
689
+
690
+ //Highlight the active item and un-highlight the previous one
691
+ container.addClass('ws_active');
692
+ container.siblings('.ws_active').removeClass('ws_active');
693
+
694
+ this.selectionHasChanged(container);
695
+ };
696
+
697
+ /**
698
+ * @param {JQuery|null} [$item]
699
+ */
700
+ AmeEditorColumn.prototype.selectionHasChanged = function($item) {
701
+ if (typeof $item === 'undefined') {
702
+ $item = this.getSelectedItem();
703
+ }
704
+ if (!$item || ($item.length < 1)) {
705
+ $item = null;
706
+ }
707
+
708
+ //Make the "delete" button appear disabled if you can't delete this item.
709
+ this.container.find('.ws_toolbar .ws_delete_menu_button')
710
+ .toggleClass('ws_button_disabled', !canDeleteItem($item))
711
+
712
+ const self = this;
713
+ this.getNextColumn(function(nextColumn) {
714
+ if (nextColumn) {
715
+ nextColumn.setVisibleItemList(getSubmenuOf($item, nextColumn));
716
+ if ($item) {
717
+ self.updateSubmenuBoxHeight($item, nextColumn);
718
+ }
719
+ }
720
+ }, false);
721
+ };
722
+
723
+ /**
724
+ * @param {JQuery} selectedMenu
725
+ * @param {AmeEditorColumn} nextColumn
726
+ */
727
+ AmeEditorColumn.prototype.updateSubmenuBoxHeight = function updateSubmenuBoxHeight(selectedMenu, nextColumn) {
728
+ if (!nextColumn || (nextColumn === this)) {
729
+ return;
730
+ }
731
+ let mainMenuBox = this.menuBox,
732
+ submenuBox = nextColumn.menuBox,
733
+ submenuDropZone = nextColumn.dropZone;
734
+
735
+ //Make the submenu box tall enough to reach the selected item.
736
+ //This prevents the menu tip (if any) from floating in empty space.
737
+ if (selectedMenu.hasClass('ws_menu_separator')) {
738
+ submenuBox.css('min-height', '');
739
+ } else {
740
+ var menuTipHeight = 30,
741
+ empiricalExtraHeight = 4,
742
+ verticalBoxOffset = (submenuBox.offset().top - mainMenuBox.offset().top),
743
+ minSubmenuHeight = (selectedMenu.offset().top - mainMenuBox.offset().top)
744
+ - verticalBoxOffset
745
+ + menuTipHeight - submenuDropZone.outerHeight() + empiricalExtraHeight;
746
+ minSubmenuHeight = Math.max(minSubmenuHeight, 0);
747
+ submenuBox.css('min-height', minSubmenuHeight);
748
+ }
749
  }
750
 
751
+ AmeEditorColumn.prototype.buildSubmenuContainer = function(parentMenuId) {
752
+ //Create a container for menu items.
753
+ const submenu = $('<div class="ws_submenu" style="display:none;"></div>');
754
+ submenu.attr('id', 'ws-submenu-'+(wsIdCounter++));
755
+
756
+ if (parentMenuId) {
757
+ submenu.data('parent_menu_id', parentMenuId);
758
+ }
759
+
760
+ //Make the submenu sortable
761
+ makeBoxSortable(submenu);
762
+
763
+ return submenu;
764
+ };
765
+
766
+ AmeEditorColumn.prototype.appendSubmenuContainer = function($submenu) {
767
+ this.usesSubmenuContainers = true;
768
+ $submenu.appendTo(this.menuBox);
769
+ };
770
+
771
+ /**
772
+ * Delete a menu item and all of its children.
773
+ *
774
+ * @param {JQuery} container
775
+ */
776
+ AmeEditorColumn.prototype.destroyItem = function(container) {
777
+ const wasSelected = container.is('.ws_active');
778
+
779
+ //Recursively destroy any submenu items.
780
+ const submenuId = container.data('submenu_id');
781
+ if (submenuId) {
782
+ const self = this;
783
+ const $submenu = $('#' + submenuId);
784
+ $submenu.children('.ws_container').each(function() {
785
+ self.destroyItem($(this));
786
+ });
787
+ $submenu.remove();
788
+ }
789
+
790
+ //Destroy the item itself.
791
+ container.remove();
792
+
793
+ if (wasSelected) {
794
+ this.selectionHasChanged();
795
+ }
796
+ };
797
+
798
+ /**
799
+ * Remove all items and item lists from this column.
800
+ *
801
+ * Note: Does not remove item submenus that are in other columns.
802
+ */
803
+ AmeEditorColumn.prototype.reset = function() {
804
+ this.menuBox.empty();
805
+ this.visibleItemList = null;
806
+ this.selectionHasChanged(null);
807
+ };
808
+
809
+ /**
810
+ *
811
+ * @param {JQuery} editorNode
812
+ * @param {Boolean|null|string} [deepNestingEnabled]
813
+ * @param {Number} [maxLevels]
814
+ * @param {Number} [initialLevels]
815
+ * @constructor
816
+ */
817
+ function AmeMenuPresenter(editorNode, deepNestingEnabled, maxLevels, initialLevels ) {
818
+ const self = this;
819
+ this.editorNode = editorNode;
820
+
821
+ if (typeof deepNestingEnabled === 'string') {
822
+ deepNestingEnabled = (deepNestingEnabled === '1');
823
+ }
824
+ this.isDeepNestingEnabled = (typeof deepNestingEnabled !== 'undefined') ? deepNestingEnabled : null;
825
+ this.nestingQueryPromise = null;
826
+
827
+ if (typeof maxLevels === 'undefined') {
828
+ maxLevels = 3;
829
+ }
830
+ if (typeof initialLevels === 'undefined') {
831
+ if (this.isDeepNestingEnabled) {
832
+ //If additional levels are enabled, show the maximum number of levels.
833
+ initialLevels = maxLevels;
834
+ } else {
835
+ //WordPress only supports up to two levels by default.
836
+ initialLevels = Math.min(maxLevels, 2);
837
+ }
838
+ }
839
+ if (initialLevels > this.maxLevels) {
840
+ initialLevels = this.maxLevels;
841
+ }
842
+
843
+ this.maxLevels = maxLevels;
844
+
845
+ const $topLevelContainer = this.editorNode.find('#ws_menu_box').first().closest('.ws_main_container');
846
+ this.columns = [
847
+ //Empty zeroth column.
848
+ new AmeEditorColumn(0, null, $()),
849
+ //The first column contains top level menus.
850
+ new AmeEditorColumn(1, null, $topLevelContainer, makeNextColumnGetter(1))
851
+ ];
852
+ this.currentLevels = this.columns.length - 1;
853
+
854
+ function makeNextColumnGetter(ownLevel) {
855
+ if (ownLevel >= self.maxLevels) {
856
+ //This column will never have a next column, so we can just use NULL.
857
+ return function(callback) {
858
+ callback(null);
859
+ };
860
+ }
861
+ return function(callback, createIfNotExists) {
862
+ self.getColumn(ownLevel + 1, callback, createIfNotExists);
863
+ };
864
+ }
865
+
866
+ /**
867
+ * @param {Number} level
868
+ * @return {AmeEditorColumn}
869
+ */
870
+ function createColumn(level) {
871
+ if (level > self.maxLevels) {
872
+ throw new Error('Cannot exceed maximum nesting level: ' + self.maxLevels);
873
+ }
874
+ if (typeof self.columns[level] !== 'undefined') {
875
+ throw new Error('Cannot overwrite an existing column ' + level);
876
+ }
877
+
878
+ let predecessor;
879
+ if (typeof self.columns[level - 1] !== 'undefined') {
880
+ predecessor = self.columns[level - 1].container;
881
+ } else {
882
+ predecessor = self.columns[self.currentLevels].container;
883
+ }
884
+
885
+ let newColumn = new AmeEditorColumn(level, predecessor, null, makeNextColumnGetter(level));
886
+ self.columns.push(newColumn);
887
+
888
+ if (level > self.currentLevels) {
889
+ self.currentLevels = level;
890
+ }
891
+
892
+ return newColumn;
893
+ }
894
+
895
+ /**
896
+ * Can we create another column?
897
+ *
898
+ * @param {Number} level
899
+ * @param {Function} callback
900
+ */
901
+ function queryCanCreateColumn(level, callback) {
902
+ if (
903
+ (level > self.maxLevels) //Do not exceed the maximum depth.
904
+ || (typeof self.columns[level] !== 'undefined') //Do not overwrite existing columns.
905
+ ) {
906
+ callback(false);
907
+ return;
908
+ }
909
+
910
+ //WordPress core only supports two admin menu levels. We call anything beyond that "deep".
911
+ const isDeep = (level > 2);
912
+ if (!isDeep) {
913
+ callback(true);
914
+ return;
915
+ }
916
+ //Do we already know if we can create deeply nested menus?
917
+ if (self.isDeepNestingEnabled !== null) {
918
+ callback(self.isDeepNestingEnabled);
919
+ return;
920
+ }
921
+
922
+ //If we're already waiting for a decision, just add another callback to the queue.
923
+ if (self.nestingQueryPromise !== null) {
924
+ self.nestingQueryPromise.always(function() {
925
+ callback(self.isDeepNestingEnabled);
926
+ });
927
+ return;
928
+ }
929
+
930
+ //Let's allow other code/plugins to decide this. Scripts can add deferred objects or promises
931
+ //to an array. All deferred objects must resolve successfully to enable deep nesting.
932
+ let deferreds = [];
933
+ self.editorNode.trigger('adminMenuEditor:queryDeepNesting', [deferreds]);
934
+
935
+ if (deferreds.length > 0) {
936
+ self.nestingQueryPromise = $.when.apply($, deferreds)
937
+ .done(function() {
938
+ self.isDeepNestingEnabled = true;
939
+ })
940
+ .fail(function() {
941
+ self.isDeepNestingEnabled = false;
942
+ })
943
+ .always(function() {
944
+ callback(self.isDeepNestingEnabled);
945
+ });
946
+ } else {
947
+ //Deep nesting is disabled by default.
948
+ self.isDeepNestingEnabled = false;
949
+ callback(self.isDeepNestingEnabled);
950
+ }
951
+ }
952
+
953
+ /**
954
+ * Get or create a column. The callback will be called with one argument: either the column object,
955
+ * or NULL if the column does not exist and could not be created.
956
+ *
957
+ * @param {Number} level
958
+ * @param {Function} callback
959
+ * @param {Boolean} [createIfNotExists] Defaults to true.
960
+ */
961
+ this.getColumn = function(level, callback, createIfNotExists) {
962
+ if (typeof this.columns[level] !== 'undefined') {
963
+ callback(this.columns[level]);
964
+ return;
965
+ }
966
+
967
+ if (typeof createIfNotExists === 'undefined') {
968
+ createIfNotExists = true;
969
+ }
970
+
971
+ if (createIfNotExists) {
972
+ queryCanCreateColumn(level, function (isAllowed) {
973
+ //It could be that another callback has already created the next column,
974
+ //so we need to check again if it exists.
975
+ if (typeof self.columns[level] !== 'undefined') {
976
+ callback(self.columns[level]);
977
+ } else if (isAllowed) {
978
+ callback(createColumn(level));
979
+ } else {
980
+ callback(null);
981
+ }
982
+ });
983
+ } else {
984
+ callback(null);
985
+ }
986
+ };
987
+
988
+ /**
989
+ * Get or create a column. Like getColumn(), but it will default to not creating deeply nested
990
+ * menu levels unless that feature is already enabled.
991
+ *
992
+ * @param {Number} level
993
+ * @return {AmeEditorColumn|null}
994
+ */
995
+ this.getColumnImmediate = function(level) {
996
+ if (typeof this.columns[level] !== 'undefined') {
997
+ return this.columns[level];
998
+ }
999
+ if (level > this.maxLevels) {
1000
+ return null;
1001
+ }
1002
+
1003
+ if ((level <= 2) || (this.isDeepNestingEnabled === true)) {
1004
+ return createColumn(level);
1005
+ }
1006
+ return null;
1007
+ };
1008
+
1009
+ /**
1010
+ * Get the column that contains a specific item.
1011
+ *
1012
+ * @param {JQuery} container Menu item container.
1013
+ * @return {AmeEditorColumn|null}
1014
+ */
1015
+ this.getItemColumn = function(container) {
1016
+ if (!container) {
1017
+ return null;
1018
+ }
1019
+ const level = container.closest('.ws_main_container').data('ame-menu-level');
1020
+ if (typeof level === 'undefined') {
1021
+ return null;
1022
+ }
1023
+ return this.getColumnImmediate(level);
1024
+ };
1025
+
1026
+ /**
1027
+ * Create editor widgets for a menu item and its submenus and append them all to the DOM.
1028
+ *
1029
+ * @param {Number} level
1030
+ * @param {Object} itemData
1031
+ * @param {JQuery} [afterNode] Insert the widget after this node.
1032
+ */
1033
+ this.outputMenuItem = function(level, itemData, afterNode) {
1034
+ const column = this.getColumnImmediate(level);
1035
+ return column.outputItem(itemData, afterNode);
1036
+ }
1037
+
1038
+ /**
1039
+ * Select a menu item and show its submenu.
1040
+ *
1041
+ * @param {JQuery} container
1042
+ */
1043
+ this.selectItem = function(container) {
1044
+ const thisColumn = this.getColumnImmediate(container.closest('.ws_main_container').data('ame-menu-level'));
1045
+ if (thisColumn) {
1046
+ thisColumn.selectItem(container);
1047
+ }
1048
+ };
1049
+
1050
+ /**
1051
+ * Delete a menu item and all of its children.
1052
+ *
1053
+ * @param {JQuery} container
1054
+ */
1055
+ this.destroyItem = function(container) {
1056
+ const column = this.getItemColumn(container);
1057
+ if (column) {
1058
+ column.destroyItem(container);
1059
+ }
1060
+ };
1061
+
1062
+ /**
1063
+ * Delete all items and reset all columns.
1064
+ */
1065
+ this.clear = function() {
1066
+ for (let level = 0; level < this.columns.length; level++) {
1067
+ if (typeof this.columns[level] !== 'undefined') {
1068
+ this.columns[level].reset();
1069
+ }
1070
+ }
1071
+ };
1072
+
1073
+ //Initialisation.
1074
+ for (let level = this.currentLevels + 1; level <= initialLevels; level++) {
1075
+ createColumn(level);
1076
+ }
1077
+ }
1078
 
1079
+ /*
1080
+ * Create edit widgets for a top-level menu and its submenus and append them all to the DOM.
1081
+ *
1082
+ * Inputs :
1083
+ * menu - an object containing menu data
1084
+ * afterNode - if specified, the new menu widget will be inserted after this node. Otherwise,
1085
+ * it will be added to the end of the list.
1086
+ * Outputs :
1087
+ * Object with two fields - 'menu' and 'submenu' - containing the DOM nodes of the created widgets.
1088
+ */
1089
+ function outputTopMenu(menu, afterNode){
1090
+ if (!menuPresenter) {
1091
+ throw new Error('outputTopMenu cannot be called before the menu presenter has been initialised.');
1092
+ }
1093
+ return menuPresenter.outputMenuItem(1, menu, afterNode);
1094
  }
1095
 
1096
  /**
1153
  }),
1154
 
1155
  'drop' : (function(event, ui){
1156
+ const column = menuPresenter.getItemColumn(item);
1157
+ if (!column) {
1158
+ return;
1159
+ }
1160
+ const nextColumn = menuPresenter.getColumnImmediate(column.level + 1);
1161
+ const submenu = getSubmenuOf(item, nextColumn);
1162
+ if (!submenu || !nextColumn) {
1163
+ return;
1164
+ }
1165
+
1166
+ const droppedItemData = readItemState(ui.draggable);
1167
+ const sourceSubmenu = ui.draggable.parent();
1168
 
1169
+ let result = nextColumn.outputItem(droppedItemData, null, submenu);
 
 
1170
 
1171
  if ( !event.ctrlKey ) {
1172
+ menuPresenter.destroyItem(ui.draggable);
1173
  }
1174
 
1175
+ updateItemEditor(result.menu);
1176
 
1177
  //Moving an item can change aggregate menu permissions. Update the UI accordingly.
1178
  updateParentAccessUi(submenu);
1179
+ if (sourceSubmenu) {
1180
+ updateParentAccessUi(sourceSubmenu);
1181
+ }
1182
  })
1183
  });
1184
  }
1777
  //noinspection JSUnusedLocalSymbols
1778
  function buildEditboxField(entry, field_name, field_settings){
1779
  //Build a form field of the appropriate type
1780
+ var inputBox;
1781
  var basicTextField = '<input type="text" class="ws_field_value">';
1782
  //noinspection FallthroughInSwitchStatementJS
1783
  switch(field_settings.type){
1868
  .attr('src', wsEditorData.imagesUrl + '/transparent16.png')
1869
  ).data('field_name', field_name);
1870
 
1871
+ var visible;
1872
  if (typeof field_settings.visible === 'function') {
1873
  visible = field_settings.visible(entry, field_name);
1874
  } else {
2269
  /**
2270
  * Select the first menu item that has the specified URL.
2271
  *
2272
+ * @param {number|string} selectorOrLevel
2273
  * @param {string} url
2274
+ * @param {null|Boolean} [expandProperties]
2275
  * @returns {JQuery}
2276
  */
2277
+ AmeEditorApi.selectMenuItemByUrl = function(selectorOrLevel, url, expandProperties) {
2278
  if (typeof expandProperties === 'undefined') {
2279
  expandProperties = null;
2280
  }
2281
 
2282
+ let level;
2283
+ if (selectorOrLevel === '#ws_menu_box') {
2284
+ level = 1;
2285
+ } else if (selectorOrLevel === '#ws_submenu_box') {
2286
+ level = 2;
2287
+ } else {
2288
+ level = selectorOrLevel;
2289
  }
2290
 
2291
+ const column = menuPresenter.getColumnImmediate(level);
2292
+ if (!column) {
2293
+ return $([]);
2294
+ }
2295
+
2296
+ const box = column.getVisibleItemList();
2297
+
2298
+ const containerNode =
2299
  box.find('.ws_container')
2300
  .filter(function() {
2301
+ const itemUrl = AmeEditorApi.getItemDisplayUrl($(this).data('menu_item'));
2302
  return (itemUrl === url);
2303
  })
2304
  .first();
2307
  AmeEditorApi.selectItem(containerNode);
2308
 
2309
  if (expandProperties !== null) {
2310
+ const expandLink = containerNode.find('.ws_edit_link').first();
2311
  if (expandLink.hasClass('ws_edit_link_expanded') !== expandProperties) {
2312
  expandLink.trigger('click');
2313
  }
2392
 
2393
  /**
2394
  * Losslessly compress the admin menu configuration.
2395
+ *
2396
  * This is a JS port of the ameMenu::compress() function defined in /includes/menu.php.
2397
+ *
2398
  * @param {Object} adminMenu
2399
  * @returns {Object}
2400
  */
2700
  //By default, any actor that has the required cap has access to the menu.
2701
  //Users can override this on a per-menu basis.
2702
  var requiredCap = getFieldValue(menuItem, 'access_level', '< Error: access_level is missing! >');
2703
+ var actorHasAccess;
2704
  if (menuItem.grant_access.hasOwnProperty(actor)) {
2705
  actorHasAccess = menuItem.grant_access[actor];
2706
  } else {
2789
  var isDomReadyDone = false;
2790
 
2791
  function ameOnDomReady() {
2792
+ if (isDomReadyDone) {
2793
+ return;
2794
+ }
2795
  isDomReadyDone = true;
2796
 
2797
  //Some editor elements are only available in the Pro version.
2821
  /***************************************************************************
2822
  Event handlers for editor widgets
2823
  ***************************************************************************/
2824
+ const menuEditorNode = $('#ws_menu_editor');
 
 
2825
 
2826
+ menuPresenter = new AmeMenuPresenter(menuEditorNode, wsEditorData.deepNestingEnabled);
2827
 
2828
  /**
2829
  * Select a menu item and show its submenu.
2831
  * @param {JQuery|HTMLElement} container Menu container node.
2832
  */
2833
  function selectItem(container) {
2834
+ menuPresenter.selectItem(container);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2835
  }
2836
  AmeEditorApi.selectItem = selectItem;
2837
 
2841
  }));
2842
 
2843
  function updateSubmenuBoxHeight(selectedMenu) {
2844
+ const myColumn = menuPresenter.getColumnImmediate(selectedMenu.closest('.ws_main_container').data('ame-menu-level') || 1);
2845
+ const nextColumn = menuPresenter.getColumnImmediate(myColumn.level + 1);
2846
+ if (!nextColumn || (nextColumn === myColumn)) {
2847
+ return;
2848
+ }
2849
+ let mainMenuBox = myColumn.menuBox,
2850
+ submenuBox = nextColumn.menuBox,
2851
+ submenuDropZone = nextColumn.container.find('.ws_dropzone').first();
2852
+
2853
  //Make the submenu box tall enough to reach the selected item.
2854
  //This prevents the menu tip (if any) from floating in empty space.
2855
  if (selectedMenu.hasClass('ws_menu_separator')) {
2975
  field.removeClass('ws_input_default');
2976
  }
2977
 
2978
+ // noinspection EqualityComparisonWithCoercionJS It's been like this so long that I'm afraid to change it.
2979
+ if (field.hasClass('ws_input_default') && (value == defaultValue)) {
2980
  value = null; //null = use default.
2981
  }
2982
 
3047
  * @param containerNode
3048
  * @param {String|Object.<String, Boolean>} actor
3049
  * @param {Boolean} [allowAccess]
3050
+ * @param {Boolean} [skipParentUiRefresh] Whether to skip updating the parent access UI. Defaults to false.
3051
  */
3052
+ function setActorAccessForTreeAndUpdateUi(containerNode, actor, allowAccess, skipParentUiRefresh) {
3053
  setActorAccess(containerNode, actor, allowAccess);
3054
 
3055
  //Apply the same permissions to sub-menus.
3056
+ const subMenuId = containerNode.data('submenu_id');
3057
+ if (subMenuId) {
3058
  $('.ws_item', '#' + subMenuId).each(function() {
3059
+ const node = $(this);
3060
+ setActorAccessForTreeAndUpdateUi(node, actor, allowAccess, true);
 
3061
  });
3062
  }
3063
 
3064
  updateItemEditor(containerNode);
3065
+ updateActorAccessUi(containerNode);
3066
+
3067
+ if ( !skipParentUiRefresh ) {
3068
+ updateParentAccessUi(containerNode);
3069
+ }
3070
  }
3071
 
3072
  /**
3229
 
3230
  var isDropdownBeingHidden = false, isSuggestionClick = false;
3231
 
3232
+ const $extraCapInAccessEditor = $('#ws_extra_capability');
3233
+
3234
  //Show/hide the capability drop-down list when the trigger button is clicked
3235
  $('#ws_trigger_capability_dropdown').on('mousedown click', onDropdownTriggerClicked);
3236
  menuEditorNode.on('mousedown click', '.ws_cap_selector_trigger', onDropdownTriggerClicked);
3237
 
3238
  function onDropdownTriggerClicked(event){
3239
  /* jshint validthis:true */
3240
+ var inputBox;
3241
  var button = $(this);
3242
 
3243
  var isInAccessEditor = false;
3245
 
3246
  //Find the input associated with the button that was clicked.
3247
  if ( button.attr('id') === 'ws_trigger_capability_dropdown' ) {
3248
+ inputBox = $extraCapInAccessEditor;
3249
  isInAccessEditor = true;
3250
  } else {
3251
  inputBox = button.closest('.ws_edit_field').find('.ws_field_value').first();
3296
  } else {
3297
  currentDropdownOwnerMenu = currentDropdownOwner.closest('.ws_container').data('menu_item');
3298
  }
3299
+
3300
  capSelectorDropdown.focus();
3301
 
3302
  capSuggestionFeature.show();
3303
  }
3304
 
3305
  //Also show it when the user presses the down arrow in the input field (doesn't work in Opera).
3306
+ $extraCapInAccessEditor.bind('keyup', function(event){
3307
  if ( event.which === 40 ){
3308
  $('#ws_trigger_capability_dropdown').trigger('click');
3309
  }
4240
  return false;
4241
  });
4242
 
4243
+ //region Toolbar buttons
4244
+
4245
  /*************************************************************************
4246
  Menu toolbar buttons
4247
  *************************************************************************/
4248
  function getSelectedMenu() {
4249
+ return menuPresenter.getColumnImmediate(1).getSelectedItem();
4250
  }
4251
  AmeEditorApi.getSelectedMenu = getSelectedMenu;
4252
 
4253
  //Show/Hide menu
4254
+ menuEditorNode.on(
4255
+ 'adminMenuEditor:action-hide',
4256
+ /**
4257
+ * @param event
4258
+ * @param {JQuery|null} selectedItem
4259
+ * @param {AmeEditorColumn} column
4260
+ */
4261
+ function(event, selectedItem, column) {
4262
+ const selection = column.getSelectedItem();
4263
+ if (selection.length < 1) {
4264
+ return;
4265
+ }
4266
 
4267
+ toggleItemHiddenFlag(selection);
 
 
 
4268
  }
4269
+ );
 
 
4270
 
4271
  //Hide a menu and deny access.
4272
+ menuEditorNode.on(
4273
+ 'adminMenuEditor:action-deny',
4274
+ /**
4275
+ * @param event
4276
+ * @param {JQuery|null} selectedItem
4277
+ * @param {AmeEditorColumn} column
4278
+ */
4279
+ function(event, selectedItem, column) {
4280
+ const selection = column.getSelectedItem();
4281
+ if (selection.length < 1) {
4282
+ return;
4283
+ }
4284
 
4285
+ function objectFillKeys(keys, value) {
4286
+ let result = {};
4287
+ _.forEach(keys, function(key) {
4288
+ result[key] = value;
4289
+ });
4290
+ return result;
4291
+ }
4292
 
4293
+ if (actorSelectorWidget.selectedActor === null) {
4294
+ //Hide from everyone except Super Admin and the current user.
4295
+ let menuItem = selection.data('menu_item'),
4296
+ validActors = _.keys(wsEditorData.actors),
4297
+ alwaysAllowedActors = _.intersection(
4298
+ ['special:super_admin', 'user:' + wsEditorData.currentUserLogin],
4299
+ validActors
4300
+ ),
4301
+ victims = _.difference(validActors, alwaysAllowedActors),
4302
+ shouldHide;
4303
+
4304
+ //First, lets check who has access. Maybe this item is already hidden from the victims.
4305
+ shouldHide = _.some(victims, _.curry(actorCanAccessMenu, 2)(menuItem));
4306
+
4307
+ let keepEnabled = objectFillKeys(alwaysAllowedActors, true),
4308
+ hideAllExceptAllowed = _.assign(objectFillKeys(victims, false), keepEnabled);
4309
+
4310
+ walkMenuTree(selection, function(container, item) {
4311
+ let newAccess;
4312
+ if (shouldHide) {
4313
+ //Yay, hide it now!
4314
+ newAccess = hideAllExceptAllowed;
4315
+ //Only update had_access_before_hiding if this item isn't hidden yet or the field is missing.
4316
+ //We don't want to double-hide an item.
4317
+ let actorsWithAccess = _.filter(victims, function(actor) {
4318
+ return actorCanAccessMenu(item, actor);
4319
+ });
4320
+ if ((actorsWithAccess.length) > 0 || _.isEmpty(_.get(item, 'had_access_before_hiding', null))) {
4321
+ item.had_access_before_hiding = actorsWithAccess;
4322
+ }
4323
+ } else {
4324
+ //Give back access to the roles and users who previously had access.
4325
+ //Careful, don't give access to roles that no longer exist.
4326
+ let actorsWhoHadAccess = _.get(item, 'had_access_before_hiding', []) || [];
4327
+ actorsWhoHadAccess = _.intersection(actorsWhoHadAccess, validActors);
4328
+
4329
+ newAccess = _.assign(objectFillKeys(actorsWhoHadAccess, true), keepEnabled);
4330
+ delete item.had_access_before_hiding;
4331
  }
 
 
 
 
 
 
 
 
 
4332
 
4333
+ setActorAccess(container, newAccess);
4334
+ updateItemEditor(container);
4335
+ });
4336
 
4337
+ } else {
4338
+ //Just toggle the checkbox.
4339
+ selection.find('input.ws_actor_access_checkbox').trigger('click');
4340
+ }
4341
  }
4342
+ );
4343
 
4344
  //Delete error dialog. It shows up when the user tries to delete one of the default menus.
4345
  var menuDeletionDialog = $('#ws-ame-menu-deletion-error').dialog({
4418
  $('#ws_hide_menu_from_everyone').on('click', function() {
4419
  menuDeletionCallback('all');
4420
  });
4421
+ const $hideExceptCurrentUser = $('#ws_hide_menu_except_current_user').on('click', function() {
4422
  menuDeletionCallback('except_current_user');
4423
  });
4424
+ const $hideExceptAdmin = $('#ws_hide_menu_except_administrator').on('click', function() {
4425
  menuDeletionCallback('except_administrator');
4426
  });
4427
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4428
  /**
4429
  * Attempt to delete a menu item. Will check if the item can actually be deleted and ask the user for confirmation.
4430
  * UI callback.
4447
 
4448
  //Different versions get slightly different options because only the Pro version has
4449
  //role-specific permissions.
4450
+ $hideExceptCurrentUser.toggleClass('hidden', !wsEditorData.wsMenuEditorPro);
4451
+ $hideExceptAdmin.toggleClass('hidden', wsEditorData.wsMenuEditorPro);
4452
 
4453
  menuDeletionDialog.dialog('open');
4454
 
4457
  }
4458
 
4459
  if (shouldDelete) {
4460
+ const parentSubmenu = selection.closest('.ws_submenu');
 
 
 
 
 
4461
 
4462
  //Delete the menu.
4463
+ menuPresenter.destroyItem(selection);
4464
 
4465
+ if (parentSubmenu && (parentSubmenu.length > 0)) {
4466
  //Refresh permissions UI for this menu's parent (if any).
4467
  updateParentAccessUi(parentSubmenu);
4468
  }
4470
  }
4471
 
4472
  //Delete menu
4473
+ menuEditorNode.on(
4474
+ 'adminMenuEditor:action-delete',
4475
+ /**
4476
+ * @param event
4477
+ * @param {JQuery|null} selectedItem
4478
+ * @param {AmeEditorColumn} column
4479
+ */
4480
+ function(event, selectedItem, column) {
4481
+ const selection = column.getSelectedItem();
4482
+ if (selection.length < 1) {
4483
+ return;
4484
+ }
4485
 
4486
+ tryDeleteItem(selection);
 
 
 
4487
  }
4488
+ );
 
 
4489
 
4490
  //Copy menu
4491
+ menuEditorNode.on(
4492
+ 'adminMenuEditor:action-copy',
4493
 
4494
+ /**
4495
+ * @param event
4496
+ * @param {JQuery|null} selectedItem
4497
+ */
4498
+ function (event, selectedItem) {
4499
+ //Get the selected menu
4500
+ if (!selectedItem || (selectedItem.lengt < 1)) {
4501
+ return;
4502
+ }
4503
 
4504
+ //Store a copy of the current menu state in clipboard
4505
+ menu_in_clipboard = readItemState(selectedItem);
4506
+ }
4507
+ );
4508
 
4509
  //Cut menu
4510
+ menuEditorNode.on(
4511
+ 'adminMenuEditor:action-cut',
4512
 
4513
+ /**
4514
+ * @param event
4515
+ * @param {JQuery|null} selectedItem
4516
+ * @param {AmeEditorColumn} column
4517
+ */
4518
+ function (event, selectedItem, column) {
4519
+ if (selectedItem === null) {
4520
+ alert('Please select a menu item first.');
4521
+ return;
4522
+ }
4523
+ const submenu = selectedItem.closest('.ws_submenu');
4524
 
4525
+ //Store a copy of the current menu state in clipboard
4526
+ menu_in_clipboard = readItemState(selectedItem);
4527
 
4528
+ //Remove the original menu and submenu
4529
+ column.destroyItem(selectedItem);
 
 
4530
 
4531
+ //If this submenu had mixed permissions, that might have changed now that the item is gone.
4532
+ updateParentAccessUi(submenu);
 
 
 
4533
  }
4534
+ );
4535
 
4536
+ menuEditorNode.on(
4537
+ 'adminMenuEditor:action-paste',
4538
+ /**
4539
+ * @param event
4540
+ * @param {JQuery|null} selectedItem
4541
+ * @param {AmeEditorColumn} column
4542
+ */
4543
+ function(event, selectedItem, column) {
4544
+ //Check if anything has been copied/cut
4545
+ if (!menu_in_clipboard) {
4546
+ return;
4547
+ }
4548
 
4549
+ //You can only add separators to submenus in the Pro version.
4550
+ if ( menu_in_clipboard.separator && !wsEditorData.wsMenuEditorPro ) {
4551
+ return;
4552
+ }
 
 
 
4553
 
4554
+ const copyOfItem = $.extend(true, {}, menu_in_clipboard);
 
4555
 
4556
+ //Paste the menu after the selection.
4557
+ column.pasteItem(copyOfItem, selectedItem);
 
4558
  }
4559
+ );
 
 
 
 
 
 
 
4560
 
4561
  //New menu
4562
+ menuEditorNode.on(
4563
+ 'adminMenuEditor:action-new-menu',
4564
+ /**
4565
+ * @param event
4566
+ * @param {JQuery|null} selectedItem
4567
+ * @param {AmeEditorColumn} column
4568
+ */
4569
+ function(event, selectedItem, column) {
4570
+ const visibleList = column.getVisibleItemList();
4571
+ if (!visibleList || (visibleList.length < 1)) {
4572
+ //Abort if there's no item list in this column. This can happen if nothing is selected
4573
+ //in the previous column.
4574
+ return;
4575
+ }
4576
 
4577
+ ws_paste_count++;
4578
 
4579
+ //The new menu starts out rather bare.
4580
+ let item = $.extend(true, {}, wsEditorData.blankMenuItem, {
4581
+ custom: true, //Important : flag the new menu as custom, or it won't show up after saving.
4582
+ template_id: '',
4583
+ menu_title: 'Custom Menu ' + ws_paste_count,
4584
+ file: randomMenuId(),
4585
+ items: []
4586
+ });
4587
+ item.defaults = $.extend(true, {}, itemTemplates.getDefaults(''));
 
4588
 
4589
+ //Make it accessible only to the current actor if one is selected.
4590
+ if (actorSelectorWidget.selectedActor !== null) {
4591
+ denyAccessForAllExcept(item, actorSelectorWidget.selectedActor);
4592
+ }
4593
 
4594
+ //Insert the new menu item.
4595
+ let selection = column.getSelectedItem();
4596
+ if (!selection || (selection.length < 1)) {
4597
+ selection = null;
4598
+ }
4599
+ let result = column.outputItem(item, selection);
4600
 
4601
+ if (result && result.menu) {
4602
+ //The menu's editbox is always open
4603
+ result.menu.find('.ws_edit_link').trigger('click');
4604
 
4605
+ updateParentAccessUi(result.menu);
4606
+ }
4607
+ }
4608
+ );
4609
 
4610
+ //New separator
4611
+ menuEditorNode.on(
4612
+ 'adminMenuEditor:action-new-separator',
4613
+ /**
4614
+ * @param event
4615
+ * @param {JQuery|null} selectedItem
4616
+ * @param {AmeEditorColumn} column
4617
+ */
4618
+ function(event, selectedItem, column) {
4619
+ const visibleList = column.getVisibleItemList();
4620
+ if (!visibleList || (visibleList.length < 1)) {
4621
+ //Abort if there's no item list in this column. This can happen if nothing is selected
4622
+ //in the previous column.
4623
+ return;
4624
  }
 
4625
 
4626
+ ws_paste_count++;
4627
+
4628
+ const randomId = randomMenuId('separator_');
4629
+ let item = $.extend(true, {}, wsEditorData.blankMenuItem, {
4630
+ separator: true, //Flag as a separator
4631
+ custom: false, //Separators don't need to flagged as custom to be retained.
4632
+ items: [],
4633
+ defaults: {
4634
+ separator: true,
4635
+ css_class : 'wp-menu-separator',
4636
+ access_level : 'read',
4637
+ file : randomId,
4638
+ hookname : randomId
4639
+ }
4640
+ });
4641
+
4642
+ const selection = column.getSelectedItem();
4643
+ column.outputItem(item, (selection.length > 0) ? selection : null);
4644
  }
4645
+ );
4646
 
4647
  //Toggle all menus for the currently selected actor
4648
+ menuEditorNode.on(
4649
+ 'adminMenuEditor:action-toggle-all',
4650
+ /**
4651
+ * @param event
4652
+ */
4653
+ function(event) {
4654
+ if ( actorSelectorWidget.selectedActor === null ) {
4655
+ alert("This button enables/disables all menus for the selected role. To use it, click a role and then click this button again.");
4656
+ return;
4657
+ }
4658
 
4659
+ //Look at the first menu's permissions and set everything to the opposite.
4660
+ const firstColumn = menuPresenter.getColumnImmediate(1);
4661
+ const topMenuNodes = $('.ws_menu', firstColumn.getVisibleItemList());
 
4662
 
4663
+ const allow = ! actorCanAccessMenu(topMenuNodes.eq(0).data('menu_item'), actorSelectorWidget.selectedActor);
 
 
4664
 
4665
+ topMenuNodes.each(function() {
4666
+ let containerNode = $(this);
4667
+ setActorAccessForTreeAndUpdateUi(containerNode, actorSelectorWidget.selectedActor, allow);
4668
+ });
4669
+ }
4670
+ );
4671
 
4672
  //Copy all menu permissions from one role to another.
4673
  var copyPermissionsDialog = $('#ws-ame-copy-permissions-dialog').dialog({
4680
  var sourceActorList = $('#ame-copy-source-actor'), destinationActorList = $('#ame-copy-destination-actor');
4681
 
4682
  //The "Copy permissions" toolbar button.
4683
+ menuEditorNode.on(
4684
+ 'adminMenuEditor:action-copy-permissions',
4685
+ /**
4686
+ * @param event
4687
+ * @param {JQuery|null} selectedItem
4688
+ * @param {AmeEditorColumn} column
4689
+ */
4690
+ function(event, selectedItem, column) {
4691
+ const previousSource = sourceActorList.val();
4692
+
4693
+ //Populate source/destination lists.
4694
+ sourceActorList.find('option').not('[disabled]').remove();
4695
+ destinationActorList.find('option').not('[disabled]').remove();
4696
+ $.each(actorSelectorWidget.getVisibleActors(), function(index, actor) {
4697
+ let option = $('<option>', {
4698
+ val: actor.id,
4699
+ text: actorSelectorWidget.getNiceName(actor)
4700
+ });
4701
+ sourceActorList.append(option);
4702
+ destinationActorList.append(option.clone());
4703
  });
 
 
 
4704
 
4705
+ //Pre-select the current actor as the destination.
4706
+ if (actorSelectorWidget.selectedActor !== null) {
4707
+ destinationActorList.val(actorSelectorWidget.selectedActor);
4708
+ }
4709
 
4710
+ //Restore the previous source selection.
4711
+ if (previousSource) {
4712
+ sourceActorList.val(previousSource);
4713
+ }
4714
+ if (!sourceActorList.val()) {
4715
+ sourceActorList.find('option').first().prop('selected', true); //Fallback.
4716
+ }
4717
 
4718
+ copyPermissionsDialog.dialog('open');
4719
+ }
4720
+ );
4721
 
4722
  //Actually copy the permissions when the user click the confirmation button.
4723
  var copyConfirmationButton = $('#ws-ame-confirm-copy-permissions');
4731
  }
4732
 
4733
  //Iterate over all menu items and copy the permissions from one actor to the other.
4734
+ AmeEditorApi.forEachMenuItem(function (menuItem, node) {
 
 
 
 
4735
  //Only change permissions when they don't match. This ensures we won't unnecessarily overwrite default
4736
  //permissions and bloat the configuration with extra grant_access entries.
4737
+ const sourceAccess = actorCanAccessMenu(menuItem, sourceActor);
4738
+ const destinationAccess = actorCanAccessMenu(menuItem, destinationActor);
4739
  if (sourceAccess !== destinationAccess) {
4740
  setActorAccess(node, destinationActor, sourceAccess);
4741
  //Note: In theory, we could also look at the default permissions for destinationActor and
4768
  });
4769
 
4770
  //Sort menus in ascending or descending order.
4771
+ menuEditorNode.on(
4772
+ 'adminMenuEditor:action-sort',
4773
+ /**
4774
+ * @param event
4775
+ * @param {JQuery|null} selectedItem
4776
+ * @param {AmeEditorColumn} column
4777
+ * @param {JQuery} button
4778
+ */
4779
+ function(event, selectedItem, column, button) {
4780
+ let direction = button.data('sort-direction') || 'asc',
4781
+ menuBox = column.getVisibleItemList();
4782
 
4783
+ if (!menuBox || (menuBox.length < 1)) {
4784
+ return;
4785
+ }
4786
 
4787
+ function sortRecursively($box, currentColumn) {
4788
+ //When indirectly sorting the second menu level (regular submenus), leave the first item unmoved.
4789
+ //Moving the first item would change the parent menu URL (WP always links it to the first item),
4790
+ //which can be unexpected and confusing. The user can always move the first item manually.
4791
+ let leaveFirstItem = ((currentColumn !== column) && (currentColumn.level === 2));
4792
+ sortMenuItems($box, direction, leaveFirstItem);
4793
+
4794
+ //Also sort child items in the next columns.
4795
+ const nextColumn = menuPresenter.getColumnImmediate(currentColumn.level + 1);
4796
+ if (nextColumn) {
4797
+ $box.find('.ws_container').each(function () {
4798
+ const $submenu = getSubmenuOf($(this), null);
4799
+ if ($submenu) {
4800
+ sortRecursively($submenu, nextColumn);
4801
+ }
4802
+ });
4803
+ }
4804
+ }
4805
 
4806
+ sortRecursively(menuBox, column);
 
 
 
 
 
 
4807
  }
4808
+ );
4809
 
4810
  /**
4811
  * Sort menu items by title.
4857
  }
4858
 
4859
  //Toggle the second row of toolbar buttons.
4860
+ menuEditorNode.on(
4861
+ 'adminMenuEditor:action-toggle-toolbar',
4862
+ /**
4863
+ * @param event
4864
+ */
4865
+ function(event) {
4866
+ let visible = menuEditorNode.find('.ws_second_toolbar_row').toggle().is(':visible');
4867
+ if (typeof $['cookie'] !== 'undefined') {
4868
+ $.cookie('ame-show-second-toolbar', visible ? '1' : '0', {expires: 90});
4869
+ }
4870
  }
4871
+ );
4872
 
4873
 
4874
  /*************************************************************************
4875
  Item toolbar buttons
4876
  *************************************************************************/
4877
  function getSelectedSubmenuItem() {
4878
+ return menuPresenter.getColumnImmediate(2).getSelectedItem();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4879
  }
4880
 
4881
+ //endregion
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4882
 
4883
  //==============================================
4884
  // Main buttons
4944
  $('#ws_data_length').val(data.length);
4945
  $('#ws_selected_actor').val(actorSelectorWidget.selectedActor === null ? '' : actorSelectorWidget.selectedActor);
4946
 
4947
+ $('#ws_is_deep_nesting_enabled').val(JSON.stringify(menuPresenter.isDeepNestingEnabled));
4948
+
4949
  var selectedMenu = getSelectedMenu();
4950
  if (selectedMenu.length > 0) {
4951
  $('#ws_selected_menu_url').val(AmeEditorApi.getItemDisplayUrl(selectedMenu.data('menu_item')));
5073
  closeText: ' ',
5074
  modal: true
5075
  });
5076
+ const $importMenuForm = $('#import_menu_form');
5077
 
5078
  $('#ws_cancel_import').on('click', function(){
5079
  $('#import_dialog').dialog('close');
5082
  $('#ws_import_menu').on('click', function(){
5083
  $('#import_progress_notice, #import_progress_notice2, #import_complete_notice, #ws_import_error').hide();
5084
  $('#ws_import_panel').show();
5085
+ $importMenuForm.resetForm();
5086
  //The "Upload" button is disabled until the user selects a file
5087
  $('#ws_start_import').attr('disabled', 'disabled');
5088
 
5100
  function handleUnexpectedImportError(xhr, errorMessage) {
5101
  //The server-side code didn't catch this error, so it's probably something serious
5102
  //and retrying won't work.
5103
+ $importMenuForm.resetForm();
5104
  $('#ws_import_panel').hide();
5105
 
5106
  //Display error information.
5111
  }
5112
 
5113
  //AJAXify the upload form
5114
+ $importMenuForm.ajaxForm({
5115
  dataType : 'json',
5116
  beforeSubmit: function(formData) {
5117
 
5149
  if ( typeof data.error !== 'undefined' ){
5150
  alert(data.error);
5151
  //Let the user try again
5152
+ $importMenuForm.resetForm();
5153
  importDialog.find('.hide-when-uploading').show();
5154
  }
5155
 
5188
  }),
5189
 
5190
  'drop' : (function(event, ui){
5191
+ const firstColumn = menuPresenter.getColumnImmediate(1);
5192
+ if (!firstColumn) {
5193
+ return;
5194
+ }
5195
+ const nextColumn = menuPresenter.getColumnImmediate(firstColumn.level + 1);
5196
+
5197
+ const droppedItemData = readItemState(ui.draggable);
5198
+ const newItemNodes = firstColumn.pasteItem(droppedItemData, null);
5199
 
5200
  //If the item was originally a top level menu, also move its original submenu items.
5201
+ if ((getFieldValue(droppedItemData, 'parent') === null) && (newItemNodes.submenu)) {
5202
+ const droppedItemFile = getFieldValue(droppedItemData, 'file');
5203
+ const nearbyItems = $(ui.draggable).siblings('.ws_item');
5204
  nearbyItems.each(function() {
5205
+ const containerNode = $(this),
5206
  submenuItem = containerNode.data('menu_item');
5207
 
5208
  //Was this item originally a child of the dragged menu?
5209
  if (getFieldValue(submenuItem, 'parent') === droppedItemFile) {
5210
+ nextColumn.pasteItem(submenuItem, null, newItemNodes.submenu);
5211
  if ( !event.ctrlKey ) {
5212
+ menuPresenter.destroyItem(containerNode);
5213
  }
5214
  }
5215
  });
5216
  }
5217
 
5218
  if ( !event.ctrlKey ) {
5219
+ menuPresenter.destroyItem(ui.draggable);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5220
  }
5221
  })
5222
  });
5690
  domCheckAttempts++;
5691
 
5692
  if ($ && $.isReady) {
5693
+ window.clearInterval(domCheckIntervalId);
5694
  ameOnDomReady();
5695
  }
5696
  }, 1000);
js/menu-highlight-fix.js CHANGED
@@ -1,4 +1,4 @@
1
- jQuery(function($) {
2
  // parseUri 1.2.2
3
  // (c) Steven Levithan <stevenlevithan.com>
4
  // MIT License
@@ -46,174 +46,243 @@ jQuery(function($) {
46
 
47
  // --- parseUri ends ---
48
 
49
- //Find the menu item whose URL best matches the currently open page.
50
- var currentUri = parseUri(location.href);
51
- var bestMatch = {
52
- uri : null,
53
- link : null,
54
- matchingParams : -1,
55
- differentParams : 10000,
56
- isAnchorMatch : false,
57
- isTopMenu : false,
58
- isHighlighted: false
59
- };
60
-
61
- //Special case: ".../wp-admin/" should match ".../wp-admin/index.php".
62
- if (currentUri.path.match(/\/wp-admin\/$/)) {
63
- currentUri.path = currentUri.path + 'index.php';
64
- }
 
 
 
 
 
 
 
 
 
65
 
66
- //Special case: if post_type is not specified for edit.php and post-new.php,
67
- //WordPress assumes it is "post". Here we make this explicit.
68
- if ( (currentUri.file === 'edit.php') || (currentUri.file === 'post-new.php') ) {
69
- if ( !currentUri.queryKey.hasOwnProperty('post_type') ) {
70
- currentUri.queryKey['post_type'] = 'post';
 
71
  }
72
- }
73
 
74
- var adminMenu = $('#adminmenu');
75
- adminMenu.find('li > a').each(function(index, link) {
76
- var $link = $(link);
77
 
78
- //Skip links that have no href or contain nothing but an "#anchor". Both AME and some
79
- //other plugins (e.g. S2Member 120703) use them as separators.
80
- if ( !$link.is('[href]') || ($link.attr('href').substring(0, 1) === '#') ) {
81
- return;
82
- }
83
 
84
- var uri = parseUri(link.href);
85
 
86
- //Same as above - use "post" as the default post type.
87
- if ( (uri.file === 'edit.php') || (uri.file === 'post-new.php') ) {
88
- if ( !uri.queryKey.hasOwnProperty('post_type') ) {
89
- uri.queryKey['post_type'] = 'post';
 
90
  }
91
- }
92
- //TODO: Consider using get_current_screen and the current_screen filter to get post types and taxonomies.
93
 
94
- //Check for a close match - everything but query and #anchor.
95
- var components = ['protocol', 'host', 'port', 'user', 'password', 'path'];
96
- var isCloseMatch = true;
97
- for (var i = 0; (i < components.length) && isCloseMatch; i++) {
98
- isCloseMatch = isCloseMatch && (uri[components[i]] === currentUri[components[i]]);
99
- }
100
 
101
- if (!isCloseMatch) {
102
- return; //Skip to the next link.
103
- }
104
 
105
- //Calculate the number of matching and different query parameters.
106
- var matchingParams = 0, differentParams = 0, param;
107
- for(param in uri.queryKey) {
108
- if (uri.queryKey.hasOwnProperty(param)) {
109
- if (currentUri.queryKey.hasOwnProperty(param)) {
110
- //All parameters that are present in *both* URLs must have the same exact values.
111
- if (uri.queryKey[param] === currentUri.queryKey[param]) {
112
- matchingParams++;
113
- } else {
114
- return; //Skip to the next link.
115
- }
116
- } else {
 
 
 
 
 
 
117
  differentParams++;
118
  }
119
  }
120
- }
121
- for(param in currentUri.queryKey) {
122
- if (currentUri.queryKey.hasOwnProperty(param) && !uri.queryKey.hasOwnProperty(param)) {
123
- differentParams++;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  }
125
- }
126
 
127
- var isAnchorMatch = uri.anchor === currentUri.anchor;
128
- var isTopMenu = $link.hasClass('menu-top');
129
- var isHighlighted = $link.is('.current, .wp-has-current-submenu');
130
-
131
- //Figure out if the current link is better than the best found so far.
132
- //To do that, we compare them by several criteria (in order of priority):
133
- var comparisons = [
134
- {
135
- better : (matchingParams > bestMatch.matchingParams),
136
- equal : (matchingParams === bestMatch.matchingParams)
137
- },
138
- {
139
- better : (differentParams < bestMatch.differentParams),
140
- equal : (differentParams === bestMatch.differentParams)
141
- },
142
- {
143
- better : (isAnchorMatch && (!bestMatch.isAnchorMatch)),
144
- equal : (isAnchorMatch === bestMatch.isAnchorMatch)
145
- },
146
-
147
- //When a menu has multiple submenus, the first submenu usually has the same URL
148
- //as the parent menu. We want to highlight this item and not just the parent.
149
- {
150
- better : (!isTopMenu && bestMatch.isTopMenu
151
- //Is this link a child of the current best match?
152
- && (!bestMatch.link || ($link.closest(bestMatch.link.closest('li')).length > 0))
153
- ),
154
- equal : (isTopMenu === bestMatch.isTopMenu)
155
- },
156
-
157
- //All else being equal, the item highlighted by WP is probably a better match.
158
- {
159
- better : (isHighlighted && !bestMatch.isHighlighted),
160
- equal : (isHighlighted === bestMatch.isHighlighted)
161
- }
162
- ];
163
-
164
- var isBetterMatch = false,
165
- isEquallyGood = true,
166
- j = 0;
167
-
168
- while (isEquallyGood && !isBetterMatch && (j < comparisons.length)) {
169
- isBetterMatch = comparisons[j].better;
170
- isEquallyGood = comparisons[j].equal;
171
- j++;
172
- }
173
 
174
- if (isBetterMatch) {
175
- bestMatch = {
176
- uri : uri,
177
- link : $link,
178
- matchingParams : matchingParams,
179
- differentParams : differentParams,
180
- isAnchorMatch : isAnchorMatch,
181
- isTopMenu : isTopMenu,
182
- isHighlighted: isHighlighted
183
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  }
185
- });
186
 
187
- //Highlight and/or expand the best matching menu.
188
- if (bestMatch.link !== null) {
189
- var bestMatchLink = bestMatch.link;
190
- var parentMenu = bestMatchLink.closest('li.menu-top');
191
- //console.log('Best match is: ', bestMatchLink);
192
-
193
- var otherHighlightedMenus = $('li.wp-has-current-submenu, li.menu-top.current', '#adminmenu')
194
- .not(parentMenu)
195
- .not('.ws-ame-has-always-open-submenu');
196
-
197
- var isWrongItemHighlighted = !bestMatchLink.hasClass('current');
198
- var isWrongMenuHighlighted = !parentMenu.is('.wp-has-current-submenu, .current') ||
199
- (otherHighlightedMenus.length > 0);
200
-
201
- if (isWrongMenuHighlighted) {
202
- //Account for users who use the Expanded Admin Menus plugin to keep all menus expanded.
203
- var shouldCloseOtherMenus = ! $('div.expand-arrow', '#adminmenu').get(0);
204
- if (shouldCloseOtherMenus) {
205
- otherHighlightedMenus
206
- .add('> a', otherHighlightedMenus)
207
- .removeClass('wp-menu-open wp-has-current-submenu current')
208
- .addClass('wp-not-current-submenu');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  }
210
 
211
- var parentMenuAndLink = parentMenu.add('> a.menu-top', parentMenu);
212
- parentMenuAndLink.removeClass('wp-not-current-submenu');
213
- if (parentMenu.hasClass('wp-has-submenu')) {
214
- parentMenuAndLink.addClass('wp-has-current-submenu wp-menu-open');
215
  }
216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  //Note: WordPress switches the admin menu between `position: fixed` and `position: relative` depending on
218
  //how tall it is compared to the browser window. Opening a different submenu can change the menu's height,
219
  //so we must trigger the position update to avoid bugs. If we don't, we can end up with a very tall menu
@@ -226,28 +295,15 @@ jQuery(function($) {
226
  //We'll resort to faking a resize event to make WP update the menu height and state.
227
  $(document).trigger('wp-window-resized');
228
  }
229
-
230
- //Workaround: Prevent the current submenu from "jumping around" when you click an item. This glitch is
231
- //caused by a `focusin` event handler in common.js. WP adds this handler to all top level menus that
232
- //are not the current menu. Since we're changing the current menu, we need to also remove this handler.
233
- if (typeof parentMenu['off'] === 'function') {
234
- parentMenu.off('focusin.adminmenu');
235
- }
236
- }
237
-
238
- if (isWrongItemHighlighted) {
239
- adminMenu.find('.current').removeClass('current');
240
- bestMatchLink.addClass('current').closest('li').addClass('current');
241
  }
242
  }
243
 
244
- //If a submenu is highlighted, so must be its parent.
245
- //In some cases, if we decide to stick with the WP-selected highlighted menu,
246
- //this might not be the case and we'll need to fix it.
247
- var parentOfHighlightedMenu = $('.wp-submenu a.current', '#adminmenu').closest('.menu-top').first();
248
- parentOfHighlightedMenu
249
- .add('> a.menu-top', parentOfHighlightedMenu)
250
- .removeClass('wp-not-current-submenu')
251
- .addClass('wp-has-current-submenu wp-menu-open');
252
 
253
- });
 
 
 
 
 
1
+ (function ($) {
2
  // parseUri 1.2.2
3
  // (c) Steven Levithan <stevenlevithan.com>
4
  // MIT License
46
 
47
  // --- parseUri ends ---
48
 
49
+ var hasRunAtLeastOnce = false;
50
+
51
+ function highlightCurrentMenu() {
52
+ hasRunAtLeastOnce = true;
53
+
54
+ //Find the menu item whose URL best matches the currently open page.
55
+ var currentUri = parseUri(location.href);
56
+ var bestMatch = {
57
+ uri: null,
58
+ /**
59
+ * @type {JQuery|null}
60
+ */
61
+ link: null,
62
+ matchingParams: -1,
63
+ differentParams: 10000,
64
+ isAnchorMatch: false,
65
+ isTopMenu: false,
66
+ level: 0,
67
+ isHighlighted: false
68
+ };
69
+
70
+ //Special case: ".../wp-admin/" should match ".../wp-admin/index.php".
71
+ if (currentUri.path.match(/\/wp-admin\/$/)) {
72
+ currentUri.path = currentUri.path + 'index.php';
73
+ }
74
 
75
+ //Special case: if post_type is not specified for edit.php and post-new.php,
76
+ //WordPress assumes it is "post". Here we make this explicit.
77
+ if ((currentUri.file === 'edit.php') || (currentUri.file === 'post-new.php')) {
78
+ if (!currentUri.queryKey.hasOwnProperty('post_type')) {
79
+ currentUri.queryKey['post_type'] = 'post';
80
+ }
81
  }
 
82
 
83
+ var adminMenu = $('#adminmenu');
84
+ adminMenu.find('li > a').each(function (index, link) {
85
+ var $link = $(link);
86
 
87
+ //Skip links that have no href or contain nothing but an "#anchor". Both AME and some
88
+ //other plugins (e.g. S2Member 120703) use them as separators.
89
+ if (!$link.is('[href]') || ($link.attr('href').substring(0, 1) === '#')) {
90
+ return;
91
+ }
92
 
93
+ var uri = parseUri(link.href);
94
 
95
+ //Same as above - use "post" as the default post type.
96
+ if ((uri.file === 'edit.php') || (uri.file === 'post-new.php')) {
97
+ if (!uri.queryKey.hasOwnProperty('post_type')) {
98
+ uri.queryKey['post_type'] = 'post';
99
+ }
100
  }
101
+ //TODO: Consider using get_current_screen and the current_screen filter to get post types and taxonomies.
 
102
 
103
+ //Check for a close match - everything but query and #anchor.
104
+ var components = ['protocol', 'host', 'port', 'user', 'password', 'path'];
105
+ var isCloseMatch = true;
106
+ for (var i = 0; (i < components.length) && isCloseMatch; i++) {
107
+ isCloseMatch = isCloseMatch && (uri[components[i]] === currentUri[components[i]]);
108
+ }
109
 
110
+ if (!isCloseMatch) {
111
+ return; //Skip to the next link.
112
+ }
113
 
114
+ //Calculate the number of matching and different query parameters.
115
+ var matchingParams = 0, differentParams = 0, param;
116
+ for (param in uri.queryKey) {
117
+ if (uri.queryKey.hasOwnProperty(param)) {
118
+ if (currentUri.queryKey.hasOwnProperty(param)) {
119
+ //All parameters that are present in *both* URLs must have the same exact values.
120
+ if (uri.queryKey[param] === currentUri.queryKey[param]) {
121
+ matchingParams++;
122
+ } else {
123
+ return; //Skip to the next link.
124
+ }
125
+ } else {
126
+ differentParams++;
127
+ }
128
+ }
129
+ }
130
+ for (param in currentUri.queryKey) {
131
+ if (currentUri.queryKey.hasOwnProperty(param) && !uri.queryKey.hasOwnProperty(param)) {
132
  differentParams++;
133
  }
134
  }
135
+
136
+ var isAnchorMatch = uri.anchor === currentUri.anchor;
137
+ var level = $link.parentsUntil(adminMenu, 'li').length;
138
+ var isHighlighted = $link.is('.current, .wp-has-current-submenu');
139
+
140
+ //Figure out if the current link is better than the best found so far.
141
+ //To do that, we compare them by several criteria (in order of priority):
142
+ var comparisons = [
143
+ {
144
+ better: (matchingParams > bestMatch.matchingParams),
145
+ equal: (matchingParams === bestMatch.matchingParams)
146
+ },
147
+ {
148
+ better: (differentParams < bestMatch.differentParams),
149
+ equal: (differentParams === bestMatch.differentParams)
150
+ },
151
+ {
152
+ better: (isAnchorMatch && (!bestMatch.isAnchorMatch)),
153
+ equal: (isAnchorMatch === bestMatch.isAnchorMatch)
154
+ },
155
+
156
+ //When a menu has multiple submenus, the first submenu usually has the same URL
157
+ //as the parent menu. We want to highlight this item and not just the parent.
158
+ {
159
+ better: ((level > bestMatch.level)
160
+ //Is this link a child of the current best match?
161
+ && (!bestMatch.link || ($link.closest(bestMatch.link.closest('li')).length > 0))
162
+ ),
163
+ equal: (level === bestMatch.level)
164
+ },
165
+
166
+ //All else being equal, the item highlighted by WP is probably a better match.
167
+ {
168
+ better: (isHighlighted && !bestMatch.isHighlighted),
169
+ equal: (isHighlighted === bestMatch.isHighlighted)
170
+ }
171
+ ];
172
+
173
+ var isBetterMatch = false,
174
+ isEquallyGood = true,
175
+ j = 0;
176
+
177
+ while (isEquallyGood && !isBetterMatch && (j < comparisons.length)) {
178
+ isBetterMatch = comparisons[j].better;
179
+ isEquallyGood = comparisons[j].equal;
180
+ j++;
181
  }
 
182
 
183
+ if (isBetterMatch) {
184
+ bestMatch = {
185
+ uri: uri,
186
+ link: $link,
187
+ matchingParams: matchingParams,
188
+ differentParams: differentParams,
189
+ isAnchorMatch: isAnchorMatch,
190
+ level: level,
191
+ isHighlighted: isHighlighted
192
+ }
193
+ }
194
+ });
195
+
196
+ var shouldUpdateStickiness = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
 
198
+ function isInViewport($thing) {
199
+ if (!$thing || ($thing.length < 1)) {
200
+ return false; //A non-existent element is never visible.
 
 
 
 
 
 
201
  }
202
+
203
+ var element = $thing.get(0);
204
+ if (!element.getBoundingClientRect || !window.innerHeight || !window.innerWidth) {
205
+ return true; //The browser doesn't support the necessary APIs, default to true.
206
+ }
207
+
208
+ var rect = element.getBoundingClientRect();
209
+ return (
210
+ (rect.top < window.innerHeight)
211
+ && (rect.bottom > 0)
212
+ && (rect.left < window.innerWidth)
213
+ && (rect.right > 0)
214
+ );
215
  }
 
216
 
217
+ //Highlight and/or expand the best matching menu.
218
+ if (bestMatch.link !== null) {
219
+ var bestMatchLink = bestMatch.link;
220
+ var linkAndItem = bestMatchLink.add(bestMatchLink.closest('li'));
221
+ var allParentMenus = bestMatchLink.parentsUntil(adminMenu, 'li');
222
+ var topLevelParent = allParentMenus.filter('li.menu-top').last();
223
+ //console.log('Best match is: ', bestMatchLink);
224
+
225
+ shouldUpdateStickiness = !isInViewport(bestMatchLink);
226
+
227
+ var otherHighlightedMenus = $('li.wp-has-current-submenu, li.menu-top.current', '#adminmenu')
228
+ .not(allParentMenus)
229
+ .not('.ws-ame-has-always-open-submenu');
230
+
231
+ var otherHighlightedSubmenus = adminMenu.find('li.current,a.current').not(linkAndItem);
232
+
233
+ var isWrongItemHighlighted = !bestMatchLink.hasClass('current') || (otherHighlightedSubmenus.length > 0);
234
+ var isWrongMenuHighlighted = !topLevelParent.is('.wp-has-current-submenu, .current') ||
235
+ (otherHighlightedMenus.length > 0);
236
+
237
+ if (isWrongMenuHighlighted) {
238
+ //Account for users who use the Expanded Admin Menus plugin to keep all menus expanded.
239
+ var shouldCloseOtherMenus = !$('div.expand-arrow', '#adminmenu').get(0);
240
+ if (shouldCloseOtherMenus) {
241
+ otherHighlightedMenus
242
+ .add('> a', otherHighlightedMenus)
243
+ .removeClass('wp-menu-open wp-has-current-submenu current')
244
+ .addClass('wp-not-current-submenu');
245
+ }
246
+
247
+ var parentMenuAndLink = topLevelParent.add('> a.menu-top', topLevelParent.get(0));
248
+ parentMenuAndLink.removeClass('wp-not-current-submenu');
249
+ if (topLevelParent.hasClass('wp-has-submenu')) {
250
+ parentMenuAndLink.addClass('wp-has-current-submenu wp-menu-open');
251
+ }
252
+
253
+ shouldUpdateStickiness = true;
254
+
255
+ //Workaround: Prevent the current submenu from "jumping around" when you click an item. This glitch is
256
+ //caused by a `focusin` event handler in common.js. WP adds this handler to all top level menus that
257
+ //are not the current menu. Since we're changing the current menu, we need to also remove this handler.
258
+ if (typeof topLevelParent['off'] === 'function') {
259
+ topLevelParent.off('focusin.adminmenu');
260
+ }
261
  }
262
 
263
+ if (isWrongItemHighlighted) {
264
+ adminMenu.find('.current').removeClass('current');
265
+ linkAndItem.addClass('current');
266
+ shouldUpdateStickiness = true;
267
  }
268
 
269
+ //WordPress adds the "current" class only to the <li> that is the direct parent of the highlighted link.
270
+ //If the highlighted item is a submenu, its top-level parent shouldn't have this class.
271
+ bestMatchLink.closest('li').parentsUntil(adminMenu, 'li').children('a').addBack().removeClass('current');
272
+
273
+ bestMatchLink.closest('ul').parentsUntil(adminMenu, 'li').addClass('ame-has-highlighted-item');
274
+ }
275
+
276
+ //If a submenu is highlighted, so must be its parent.
277
+ //In some cases, if we decide to stick with the WP-selected highlighted menu,
278
+ //this might not be the case and we'll need to fix it.
279
+ var parentOfHighlightedMenu = $('.wp-submenu a.current', '#adminmenu').closest('.menu-top').first();
280
+ parentOfHighlightedMenu
281
+ .add('> a.menu-top', parentOfHighlightedMenu)
282
+ .removeClass('wp-not-current-submenu')
283
+ .addClass('wp-has-current-submenu wp-menu-open');
284
+
285
+ if (shouldUpdateStickiness) {
286
  //Note: WordPress switches the admin menu between `position: fixed` and `position: relative` depending on
287
  //how tall it is compared to the browser window. Opening a different submenu can change the menu's height,
288
  //so we must trigger the position update to avoid bugs. If we don't, we can end up with a very tall menu
295
  //We'll resort to faking a resize event to make WP update the menu height and state.
296
  $(document).trigger('wp-window-resized');
297
  }
 
 
 
 
 
 
 
 
 
 
 
 
298
  }
299
  }
300
 
301
+ //Other scripts can trigger this feature by using a custom event.
302
+ $(document).on('adminMenuEditor:highlightCurrentMenu', highlightCurrentMenu);
 
 
 
 
 
 
303
 
304
+ $(function () {
305
+ if (!hasRunAtLeastOnce) {
306
+ highlightCurrentMenu();
307
+ }
308
+ });
309
+ })(jQuery);
js/tab-utils.js ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ jQuery(function ($) {
2
+ var menuEditorHeading = $('#ws_ame_editor_heading').first();
3
+ var pageWrapper = menuEditorHeading.closest('.wrap');
4
+ var tabList = pageWrapper.find('.nav-tab-wrapper').first();
5
+
6
+ //On AME pages, move settings tabs after the heading. This is necessary to make them appear on the right side,
7
+ //and WordPress breaks that by moving notices like "Settings saved" after the first H1 (see common.js).
8
+ var menuEditorTabs = tabList.add(tabList.next('.clear'));
9
+ if ((menuEditorHeading.length > 0) && (menuEditorTabs.length > 0)) {
10
+ menuEditorTabs.insertAfter(menuEditorHeading);
11
+ }
12
+
13
+ //Switch tab styles when there are too many tabs and they don't fit on one row.
14
+ var $firstTab = null,
15
+ $lastTab = null,
16
+ knownTabWrapThreshold = -1;
17
+
18
+ function updateTabStyles() {
19
+ if (($firstTab === null) || ($lastTab === null)) {
20
+ var $tabItems = tabList.children('.nav-tab');
21
+ $firstTab = $tabItems.first();
22
+ $lastTab = $tabItems.last();
23
+ }
24
+
25
+ //To detect if any tabs are wrapped to the next row, check if the top of the last tab
26
+ //is below the bottom of the first tab.
27
+ var firstPosition = $firstTab.position();
28
+ var lastPosition = $lastTab.position();
29
+ var windowWidth = $(window).width();
30
+ //Sanity check.
31
+ if (
32
+ !firstPosition || !lastPosition || !windowWidth
33
+ || (typeof firstPosition['top'] === 'undefined')
34
+ || (typeof lastPosition['top'] === 'undefined')
35
+ ) {
36
+ return;
37
+ }
38
+ var firstTabBottom = firstPosition.top + $firstTab.outerHeight();
39
+ var areTabsWrapped = (lastPosition.top >= firstTabBottom);
40
+
41
+ //Tab positions may change when we apply different styles, which could lead to the tab bar
42
+ //rapidly cycling between one and two two rows when the browser width is just right.
43
+ //To prevent that, remember what the width was when we detected wrapping, and always apply
44
+ //the alternative styles if the width is lower than that.
45
+ var wouldWrapByDefault = (windowWidth <= knownTabWrapThreshold);
46
+
47
+ var tooManyTabs = areTabsWrapped || wouldWrapByDefault;
48
+ if (tooManyTabs && (windowWidth > knownTabWrapThreshold)) {
49
+ knownTabWrapThreshold = windowWidth;
50
+ }
51
+
52
+ pageWrapper.toggleClass('ws-ame-too-many-tabs', tooManyTabs);
53
+ }
54
+
55
+ updateTabStyles();
56
+
57
+ $(window).on('resize', wsAmeLodash.debounce(
58
+ function () {
59
+ updateTabStyles();
60
+ },
61
+ 300
62
+ ));
63
+ });
64
+
menu-editor.php CHANGED
@@ -3,21 +3,25 @@
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.9.10
7
  Author: Janis Elsts
8
  Author URI: http://w-shadow.com/blog/
 
 
 
 
 
 
 
 
 
9
  */
10
 
11
  if ( include(dirname(__FILE__) . '/includes/version-conflict-check.php') ) {
12
  return;
13
  }
14
 
 
15
  require_once dirname(__FILE__) . '/includes/basic-dependencies.php';
16
-
17
- //Are we running in the Dashboard?
18
- if ( is_admin() ) {
19
-
20
- //Load the plugin
21
- $wp_menu_editor = new WPMenuEditor(__FILE__, 'ws_menu_editor');
22
-
23
- }//is_admin()
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.10
7
  Author: Janis Elsts
8
  Author URI: http://w-shadow.com/blog/
9
+ License: GPLv3
10
+ License URI: https://www.gnu.org/licenses/gpl-3.0.html
11
+ */
12
+
13
+ /*
14
+ This plugin may include third-party libraries and other content that is licensed under various
15
+ GPL-compatible licenses. In such cases, the relevant license will usually be stated at the top
16
+ of the source code file or in "readme.txt", "license.txt" or a similar file located in the same
17
+ directory as the content.
18
  */
19
 
20
  if ( include(dirname(__FILE__) . '/includes/version-conflict-check.php') ) {
21
  return;
22
  }
23
 
24
+ //Load the plugin
25
  require_once dirname(__FILE__) . '/includes/basic-dependencies.php';
26
+ global $wp_menu_editor;
27
+ $wp_menu_editor = new WPMenuEditor(__FILE__, 'ws_menu_editor');
 
 
 
 
 
 
modules/actor-selector/actor-selector.js CHANGED
@@ -43,7 +43,12 @@ var AmeActorSelector = /** @class */ (function () {
43
  }
44
  //Select an actor on click.
45
  this.selectorNode.on('click', 'li a.ws_actor_option', function (event) {
46
- var actor = jQuery(event.target).attr('href').substring(1);
 
 
 
 
 
47
  if (actor === '') {
48
  actor = null;
49
  }
43
  }
44
  //Select an actor on click.
45
  this.selectorNode.on('click', 'li a.ws_actor_option', function (event) {
46
+ var href = jQuery(event.target).attr('href');
47
+ var fragmentStart = href.indexOf('#');
48
+ var actor = null;
49
+ if (fragmentStart >= 0) {
50
+ actor = href.substring(fragmentStart + 1);
51
+ }
52
  if (actor === '') {
53
  actor = null;
54
  }
modules/actor-selector/actor-selector.ts CHANGED
@@ -85,7 +85,13 @@ class AmeActorSelector {
85
 
86
  //Select an actor on click.
87
  this.selectorNode.on('click', 'li a.ws_actor_option', (event) => {
88
- let actor = jQuery(event.target).attr('href').substring(1);
 
 
 
 
 
 
89
  if (actor === '') {
90
  actor = null;
91
  }
85
 
86
  //Select an actor on click.
87
  this.selectorNode.on('click', 'li a.ws_actor_option', (event) => {
88
+ const href = jQuery(event.target).attr('href');
89
+ const fragmentStart = href.indexOf('#');
90
+
91
+ let actor = null;
92
+ if (fragmentStart >= 0) {
93
+ actor = href.substring(fragmentStart + 1);
94
+ }
95
  if (actor === '') {
96
  actor = null;
97
  }
modules/redirector/drag-indicator.svg ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ height="24px"
4
+ viewBox="0 0 24 24"
5
+ width="24px"
6
+ fill="#000000"
7
+ version="1.1"
8
+ id="svg23"
9
+ sodipodi:docname="drag-indicator.svg"
10
+ inkscape:version="1.1 (c68e22c387, 2021-05-23)"
11
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
13
+ xmlns:xlink="http://www.w3.org/1999/xlink"
14
+ xmlns="http://www.w3.org/2000/svg"
15
+ >
16
+ <defs
17
+ id="defs27" />
18
+ <sodipodi:namedview
19
+ id="namedview25"
20
+ pagecolor="#ffffff"
21
+ bordercolor="#666666"
22
+ borderopacity="1.0"
23
+ inkscape:pageshadow="2"
24
+ inkscape:pageopacity="0.0"
25
+ inkscape:pagecheckerboard="0"
26
+ showgrid="true"
27
+ inkscape:zoom="33.708333"
28
+ inkscape:cx="7.0160692"
29
+ inkscape:cy="12"
30
+ inkscape:window-width="1920"
31
+ inkscape:window-height="1017"
32
+ inkscape:window-x="-8"
33
+ inkscape:window-y="-8"
34
+ inkscape:window-maximized="1"
35
+ inkscape:current-layer="symbol1321"
36
+ inkscape:snap-grids="true"
37
+ inkscape:snap-page="false"
38
+ showguides="true"
39
+ inkscape:guide-bbox="true">
40
+ <inkscape:grid
41
+ type="xygrid"
42
+ id="grid2690" />
43
+ <sodipodi:guide
44
+ position="9,18"
45
+ orientation="1,0"
46
+ id="guide7197" />
47
+ <sodipodi:guide
48
+ position="9,18"
49
+ orientation="0,-1"
50
+ id="guide7199" />
51
+ <sodipodi:guide
52
+ position="9,12"
53
+ orientation="0,-1"
54
+ id="guide7201" />
55
+ <sodipodi:guide
56
+ position="9,6"
57
+ orientation="0,-1"
58
+ id="guide7203" />
59
+ <sodipodi:guide
60
+ position="15,18"
61
+ orientation="1,0"
62
+ id="guide7205" />
63
+ </sodipodi:namedview>
64
+ <path
65
+ d="M0 0h24v24H0V0z"
66
+ fill="none"
67
+ id="box_border"
68
+ inkscape:label="box_border" />
69
+ <g
70
+ inkscape:groupmode="layer"
71
+ id="layer2"
72
+ inkscape:label="Dots"
73
+ style="display:inline">
74
+ <use
75
+ xlink:href="#symbol1321"
76
+ id="use1346"
77
+ x="0"
78
+ y="0"
79
+ width="100%"
80
+ height="100%"
81
+ transform="translate(6)" />
82
+ <use
83
+ xlink:href="#symbol1321"
84
+ id="use2560"
85
+ x="0"
86
+ y="0"
87
+ width="100%"
88
+ height="100%"
89
+ transform="translate(6,5.9999999)" />
90
+ <use
91
+ xlink:href="#symbol1321"
92
+ id="use2585"
93
+ x="0"
94
+ y="0"
95
+ width="100%"
96
+ height="100%"
97
+ transform="translate(6,12)" />
98
+ <use
99
+ xlink:href="#symbol1321"
100
+ id="use2610"
101
+ x="0"
102
+ y="0"
103
+ width="100%"
104
+ height="100%"
105
+ transform="translate(-1.5014649e-8,5.9999999)" />
106
+ <use
107
+ xlink:href="#symbol1321"
108
+ id="use2635"
109
+ x="0"
110
+ y="0"
111
+ width="100%"
112
+ height="100%"
113
+ transform="translate(-1.5014649e-8,12)" />
114
+ <g
115
+ id="symbol1321"
116
+ transform="translate(0.41718102,3.7731771)"
117
+ inkscape:label="BaseDot">
118
+ <circle
119
+ style="fill:#737373;fill-opacity:1;stroke:none;stroke-width:7.03132;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:125.448;stroke-opacity:1"
120
+ id="path269"
121
+ cy="2.2268229"
122
+ cx="8.582819"
123
+ r="1.5" />
124
+ </g>
125
+ </g>
126
+ </svg>
modules/redirector/knockout-sortable.js ADDED
@@ -0,0 +1,494 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // knockout-sortable 1.2.1 | (c) 2021 Ryan Niemeyer | http://www.opensource.org/licenses/mit-license
2
+ ;(function(factory) {
3
+ if (typeof define === "function" && define.amd) {
4
+ // AMD anonymous module
5
+ define(["knockout", "jquery", "jquery-ui/ui/widgets/sortable", "jquery-ui/ui/widgets/draggable", "jquery-ui/ui/widgets/droppable"], factory);
6
+ } else if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
7
+ // CommonJS module
8
+ var ko = require("knockout"),
9
+ jQuery = require("jquery");
10
+ require("jquery-ui/ui/widgets/sortable");
11
+ require("jquery-ui/ui/widgets/draggable");
12
+ require("jquery-ui/ui/widgets/droppable");
13
+ factory(ko, jQuery);
14
+ } else {
15
+ // No module loader (plain <script> tag) - put directly in global namespace
16
+ factory(window.ko, window.jQuery);
17
+ }
18
+ })(function(ko, $) {
19
+ var ITEMKEY = "ko_sortItem",
20
+ INDEXKEY = "ko_sourceIndex",
21
+ LISTKEY = "ko_sortList",
22
+ PARENTKEY = "ko_parentList",
23
+ DRAGKEY = "ko_dragItem",
24
+ unwrap = ko.utils.unwrapObservable,
25
+ dataGet = ko.utils.domData.get,
26
+ dataSet = ko.utils.domData.set,
27
+ version = $.ui && $.ui.version,
28
+ //1.8.24 included a fix for how events were triggered in nested sortables. indexOf checks will fail if version starts with that value (0 vs. -1)
29
+ hasNestedSortableFix = version && version.indexOf("1.6.") && version.indexOf("1.7.") && (version.indexOf("1.8.") || version === "1.8.24");
30
+
31
+ //internal afterRender that adds meta-data to children
32
+ var addMetaDataAfterRender = function(elements, data) {
33
+ ko.utils.arrayForEach(elements, function(element) {
34
+ if (element.nodeType === 1) {
35
+ dataSet(element, ITEMKEY, data);
36
+ dataSet(element, PARENTKEY, dataGet(element.parentNode, LISTKEY));
37
+ }
38
+ });
39
+ };
40
+
41
+ //prepare the proper options for the template binding
42
+ var prepareTemplateOptions = function(valueAccessor, dataName) {
43
+ var result = {},
44
+ options = {},
45
+ actualAfterRender;
46
+
47
+ //build our options to pass to the template engine
48
+ if (ko.utils.peekObservable(valueAccessor()).data) {
49
+ options = unwrap(valueAccessor() || {});
50
+ result[dataName] = options.data;
51
+ if (options.hasOwnProperty("template")) {
52
+ result.name = options.template;
53
+ }
54
+ } else {
55
+ result[dataName] = valueAccessor();
56
+ }
57
+
58
+ ko.utils.arrayForEach(["afterAdd", "afterRender", "as", "beforeRemove", "includeDestroyed", "templateEngine", "templateOptions", "nodes"], function (option) {
59
+ if (options.hasOwnProperty(option)) {
60
+ result[option] = options[option];
61
+ } else if (ko.bindingHandlers.sortable.hasOwnProperty(option)) {
62
+ result[option] = ko.bindingHandlers.sortable[option];
63
+ }
64
+ });
65
+
66
+ //use an afterRender function to add meta-data
67
+ if (dataName === "foreach") {
68
+ if (result.afterRender) {
69
+ //wrap the existing function, if it was passed
70
+ actualAfterRender = result.afterRender;
71
+ result.afterRender = function(element, data) {
72
+ addMetaDataAfterRender.call(data, element, data);
73
+ actualAfterRender.call(data, element, data);
74
+ };
75
+ } else {
76
+ result.afterRender = addMetaDataAfterRender;
77
+ }
78
+ }
79
+
80
+ //return options to pass to the template binding
81
+ return result;
82
+ };
83
+
84
+ var updateIndexFromDestroyedItems = function(index, items) {
85
+ var unwrapped = unwrap(items);
86
+
87
+ if (unwrapped) {
88
+ for (var i = 0; i <= index; i++) {
89
+ //add one for every destroyed item we find before the targetIndex in the target array
90
+ if (unwrapped[i] && unwrap(unwrapped[i]._destroy)) {
91
+ index++;
92
+ }
93
+ }
94
+ }
95
+
96
+ return index;
97
+ };
98
+
99
+ //remove problematic leading/trailing whitespace from templates
100
+ var stripTemplateWhitespace = function(element, name) {
101
+ var templateSource,
102
+ templateElement;
103
+
104
+ //process named templates
105
+ if (name) {
106
+ templateElement = document.getElementById(name);
107
+ if (templateElement) {
108
+ templateSource = new ko.templateSources.domElement(templateElement);
109
+ templateSource.text($.trim(templateSource.text()));
110
+ }
111
+ }
112
+ else {
113
+ //remove leading/trailing non-elements from anonymous templates
114
+ $(element).contents().each(function() {
115
+ if (this && this.nodeType !== 1) {
116
+ element.removeChild(this);
117
+ }
118
+ });
119
+ }
120
+ };
121
+
122
+ //connect items with observableArrays
123
+ ko.bindingHandlers.sortable = {
124
+ init: function(element, valueAccessor, allBindingsAccessor, data, context) {
125
+ var $element = $(element),
126
+ value = unwrap(valueAccessor()) || {},
127
+ templateOptions = prepareTemplateOptions(valueAccessor, "foreach"),
128
+ sortable = {},
129
+ startActual, updateActual;
130
+
131
+ stripTemplateWhitespace(element, templateOptions.name);
132
+
133
+ //build a new object that has the global options with overrides from the binding
134
+ $.extend(true, sortable, ko.bindingHandlers.sortable);
135
+ if (value.options && sortable.options) {
136
+ ko.utils.extend(sortable.options, value.options);
137
+ delete value.options;
138
+ }
139
+ ko.utils.extend(sortable, value);
140
+
141
+ //if allowDrop is an observable or a function, then execute it in a computed observable
142
+ if (sortable.connectClass && (ko.isObservable(sortable.allowDrop) || typeof sortable.allowDrop == "function")) {
143
+ ko.computed({
144
+ read: function() {
145
+ var value = unwrap(sortable.allowDrop),
146
+ shouldAdd = typeof value == "function" ? value.call(this, templateOptions.foreach) : value;
147
+ ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, shouldAdd);
148
+ },
149
+ disposeWhenNodeIsRemoved: element
150
+ }, this);
151
+ } else {
152
+ ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, sortable.allowDrop);
153
+ }
154
+
155
+ //wrap the template binding
156
+ ko.bindingHandlers.template.init(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
157
+
158
+ //keep a reference to start/update functions that might have been passed in
159
+ startActual = sortable.options.start;
160
+ updateActual = sortable.options.update;
161
+
162
+ //ensure draggable table row cells maintain their width while dragging (unless a helper is provided)
163
+ if ( !sortable.options.helper ) {
164
+ sortable.options.helper = function(e, ui) {
165
+ if (ui.is("tr")) {
166
+ ui.children().each(function() {
167
+ $(this).width($(this).width());
168
+ });
169
+ }
170
+ return ui;
171
+ };
172
+ }
173
+
174
+ //initialize sortable binding after template binding has rendered in update function
175
+ var createTimeout = setTimeout(function() {
176
+ var dragItem;
177
+ var originalReceive = sortable.options.receive;
178
+
179
+ $element.sortable(ko.utils.extend(sortable.options, {
180
+ start: function(event, ui) {
181
+ //track original index
182
+ var el = ui.item[0];
183
+ dataSet(el, INDEXKEY, ko.utils.arrayIndexOf(ui.item.parent().children(), el));
184
+
185
+ //make sure that fields have a chance to update model
186
+ ui.item.find("input:focus").change();
187
+ if (startActual) {
188
+ startActual.apply(this, arguments);
189
+ }
190
+ },
191
+ receive: function(event, ui) {
192
+ //optionally apply an existing receive handler
193
+ if (typeof originalReceive === "function") {
194
+ originalReceive.call(this, event, ui);
195
+ }
196
+
197
+ dragItem = dataGet(ui.item[0], DRAGKEY);
198
+ if (dragItem) {
199
+ //copy the model item, if a clone option is provided
200
+ if (dragItem.clone) {
201
+ dragItem = dragItem.clone();
202
+ }
203
+
204
+ //configure a handler to potentially manipulate item before drop
205
+ if (sortable.dragged) {
206
+ dragItem = sortable.dragged.call(this, dragItem, event, ui) || dragItem;
207
+ }
208
+ }
209
+ },
210
+ update: function(event, ui) {
211
+ var sourceParent, targetParent, sourceIndex, targetIndex, arg,
212
+ el = ui.item[0],
213
+ parentEl = ui.item.parent()[0],
214
+ item = dataGet(el, ITEMKEY) || dragItem;
215
+
216
+ if (!item) {
217
+ $(el).remove();
218
+ }
219
+ dragItem = null;
220
+
221
+ //make sure that moves only run once, as update fires on multiple containers
222
+ if (item && (this === parentEl) || (!hasNestedSortableFix && $.contains(this, parentEl))) {
223
+ //identify parents
224
+ sourceParent = dataGet(el, PARENTKEY);
225
+ sourceIndex = dataGet(el, INDEXKEY);
226
+ targetParent = dataGet(el.parentNode, LISTKEY);
227
+ targetIndex = ko.utils.arrayIndexOf(ui.item.parent().children(), el);
228
+
229
+ //take destroyed items into consideration
230
+ if (!templateOptions.includeDestroyed) {
231
+ sourceIndex = updateIndexFromDestroyedItems(sourceIndex, sourceParent);
232
+ targetIndex = updateIndexFromDestroyedItems(targetIndex, targetParent);
233
+ }
234
+
235
+ //build up args for the callbacks
236
+ if (sortable.beforeMove || sortable.afterMove) {
237
+ arg = {
238
+ item: item,
239
+ sourceParent: sourceParent,
240
+ sourceParentNode: sourceParent && ui.sender || el.parentNode,
241
+ sourceIndex: sourceIndex,
242
+ targetParent: targetParent,
243
+ targetIndex: targetIndex,
244
+ cancelDrop: false
245
+ };
246
+
247
+ //execute the configured callback prior to actually moving items
248
+ if (sortable.beforeMove) {
249
+ sortable.beforeMove.call(this, arg, event, ui);
250
+ }
251
+ }
252
+
253
+ //call cancel on the correct list, so KO can take care of DOM manipulation
254
+ if (sourceParent) {
255
+ $(sourceParent === targetParent ? this : ui.sender || this).sortable("cancel");
256
+ }
257
+ //for a draggable item just remove the element
258
+ else {
259
+ $(el).remove();
260
+ }
261
+
262
+ //if beforeMove told us to cancel, then we are done
263
+ if (arg && arg.cancelDrop) {
264
+ return;
265
+ }
266
+
267
+ //if the strategy option is unset or false, employ the order strategy involving removal and insertion of items
268
+ if (!sortable.hasOwnProperty("strategyMove") || sortable.strategyMove === false) {
269
+ //do the actual move
270
+ if (targetIndex >= 0) {
271
+ if (sourceParent) {
272
+ sourceParent.splice(sourceIndex, 1);
273
+
274
+ //if using deferred updates plugin, force updates
275
+ if (ko.processAllDeferredBindingUpdates) {
276
+ ko.processAllDeferredBindingUpdates();
277
+ }
278
+
279
+ //if using deferred updates on knockout 3.4, force updates
280
+ if (ko.options && ko.options.deferUpdates) {
281
+ ko.tasks.runEarly();
282
+ }
283
+ }
284
+
285
+ targetParent.splice(targetIndex, 0, item);
286
+ }
287
+
288
+ //rendering is handled by manipulating the observableArray; ignore dropped element
289
+ dataSet(el, ITEMKEY, null);
290
+ }
291
+ else { //employ the strategy of moving items
292
+ if (targetIndex >= 0) {
293
+ if (sourceParent) {
294
+ if (sourceParent !== targetParent) {
295
+ // moving from one list to another
296
+
297
+ sourceParent.splice(sourceIndex, 1);
298
+ targetParent.splice(targetIndex, 0, item);
299
+
300
+ //rendering is handled by manipulating the observableArray; ignore dropped element
301
+ dataSet(el, ITEMKEY, null);
302
+ ui.item.remove();
303
+ }
304
+ else {
305
+ // moving within same list
306
+ var underlyingList = unwrap(sourceParent);
307
+
308
+ // notify 'beforeChange' subscribers
309
+ if (sourceParent.valueWillMutate) {
310
+ sourceParent.valueWillMutate();
311
+ }
312
+
313
+ // move from source index ...
314
+ underlyingList.splice(sourceIndex, 1);
315
+ // ... to target index
316
+ underlyingList.splice(targetIndex, 0, item);
317
+
318
+ // notify subscribers
319
+ if (sourceParent.valueHasMutated) {
320
+ sourceParent.valueHasMutated();
321
+ }
322
+ }
323
+ }
324
+ else {
325
+ // drop new element from outside
326
+ targetParent.splice(targetIndex, 0, item);
327
+
328
+ //rendering is handled by manipulating the observableArray; ignore dropped element
329
+ dataSet(el, ITEMKEY, null);
330
+ ui.item.remove();
331
+ }
332
+ }
333
+ }
334
+
335
+ //if using deferred updates plugin, force updates
336
+ if (ko.processAllDeferredBindingUpdates) {
337
+ ko.processAllDeferredBindingUpdates();
338
+ }
339
+
340
+ //allow binding to accept a function to execute after moving the item
341
+ if (sortable.afterMove) {
342
+ sortable.afterMove.call(this, arg, event, ui);
343
+ }
344
+ }
345
+
346
+ if (updateActual) {
347
+ updateActual.apply(this, arguments);
348
+ }
349
+ },
350
+ connectWith: sortable.connectClass ? "." + sortable.connectClass : false
351
+ }));
352
+
353
+ //handle enabling/disabling sorting
354
+ if (sortable.isEnabled !== undefined) {
355
+ ko.computed({
356
+ read: function() {
357
+ $element.sortable(unwrap(sortable.isEnabled) ? "enable" : "disable");
358
+ },
359
+ disposeWhenNodeIsRemoved: element
360
+ });
361
+ }
362
+ }, 0);
363
+
364
+ //handle disposal
365
+ ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
366
+ //only call destroy if sortable has been created
367
+ if ($element.data("ui-sortable") || $element.data("sortable")) {
368
+ $element.sortable("destroy");
369
+ }
370
+
371
+ ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, false);
372
+
373
+ //do not create the sortable if the element has been removed from DOM
374
+ clearTimeout(createTimeout);
375
+ });
376
+
377
+ return { 'controlsDescendantBindings': true };
378
+ },
379
+ update: function(element, valueAccessor, allBindingsAccessor, data, context) {
380
+ var templateOptions = prepareTemplateOptions(valueAccessor, "foreach");
381
+
382
+ //attach meta-data
383
+ dataSet(element, LISTKEY, templateOptions.foreach);
384
+
385
+ //call template binding's update with correct options
386
+ ko.bindingHandlers.template.update(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
387
+ },
388
+ connectClass: 'ko_container',
389
+ allowDrop: true,
390
+ afterMove: null,
391
+ beforeMove: null,
392
+ options: {}
393
+ };
394
+
395
+ //create a draggable that is appropriate for dropping into a sortable
396
+ ko.bindingHandlers.draggable = {
397
+ init: function(element, valueAccessor, allBindingsAccessor, data, context) {
398
+ var value = unwrap(valueAccessor()) || {},
399
+ options = value.options || {},
400
+ draggableOptions = ko.utils.extend({}, ko.bindingHandlers.draggable.options),
401
+ templateOptions = prepareTemplateOptions(valueAccessor, "data"),
402
+ connectClass = value.connectClass || ko.bindingHandlers.draggable.connectClass,
403
+ isEnabled = value.isEnabled !== undefined ? value.isEnabled : ko.bindingHandlers.draggable.isEnabled;
404
+
405
+ value = "data" in value ? value.data : value;
406
+
407
+ //set meta-data
408
+ dataSet(element, DRAGKEY, value);
409
+
410
+ //override global options with override options passed in
411
+ ko.utils.extend(draggableOptions, options);
412
+
413
+ //setup connection to a sortable
414
+ draggableOptions.connectToSortable = connectClass ? "." + connectClass : false;
415
+
416
+ //initialize draggable
417
+ $(element).draggable(draggableOptions);
418
+
419
+ //handle enabling/disabling sorting
420
+ if (isEnabled !== undefined) {
421
+ ko.computed({
422
+ read: function() {
423
+ $(element).draggable(unwrap(isEnabled) ? "enable" : "disable");
424
+ },
425
+ disposeWhenNodeIsRemoved: element
426
+ });
427
+ }
428
+
429
+ //handle disposal
430
+ ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
431
+ if ($element.data("ui-draggable") || $element.data("draggable")) {
432
+ $element.draggable("destroy");
433
+ }
434
+ });
435
+
436
+ return ko.bindingHandlers.template.init(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
437
+ },
438
+ update: function(element, valueAccessor, allBindingsAccessor, data, context) {
439
+ var templateOptions = prepareTemplateOptions(valueAccessor, "data");
440
+
441
+ return ko.bindingHandlers.template.update(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
442
+ },
443
+ connectClass: ko.bindingHandlers.sortable.connectClass,
444
+ options: {
445
+ helper: "clone"
446
+ }
447
+ };
448
+
449
+ // Simple Droppable Implementation
450
+ // binding that updates (function or observable)
451
+ ko.bindingHandlers.droppable = {
452
+ init: function(element, valueAccessor, allBindingsAccessor, data, context) {
453
+ var value = unwrap(valueAccessor()) || {},
454
+ options = value.options || {},
455
+ droppableOptions = ko.utils.extend({}, ko.bindingHandlers.droppable.options),
456
+ isEnabled = value.isEnabled !== undefined ? value.isEnabled : ko.bindingHandlers.droppable.isEnabled;
457
+
458
+ //override global options with override options passed in
459
+ ko.utils.extend(droppableOptions, options);
460
+
461
+ //get reference to drop method
462
+ value = "data" in value ? value.data : valueAccessor();
463
+
464
+ //set drop method
465
+ droppableOptions.drop = function(event, ui) {
466
+ var droppedItem = dataGet(ui.draggable[0], DRAGKEY) || dataGet(ui.draggable[0], ITEMKEY);
467
+ value(droppedItem);
468
+ };
469
+
470
+ //initialize droppable
471
+ $(element).droppable(droppableOptions);
472
+
473
+ //handle enabling/disabling droppable
474
+ if (isEnabled !== undefined) {
475
+ ko.computed({
476
+ read: function() {
477
+ $(element).droppable(unwrap(isEnabled) ? "enable": "disable");
478
+ },
479
+ disposeWhenNodeIsRemoved: element
480
+ });
481
+ }
482
+
483
+ //handle disposal
484
+ ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
485
+ if ($element.data("ui-droppable") || $element.data("droppable")) {
486
+ $element.droppable("destroy");
487
+ }
488
+ });
489
+ },
490
+ options: {
491
+ accept: "*"
492
+ }
493
+ };
494
+ });
modules/redirector/redirector-template.php ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @var string $moduleTabUrl
4
+ */
5
+
6
+ $dragIconUrl = plugins_url('drag-indicator.svg', __FILE__);
7
+
8
+ if ( defined('AME_DISABLE_REDIRECTS') && constant('AME_DISABLE_REDIRECTS') ) {
9
+ ?>
10
+ <div class="notice notice-warning">
11
+ <p>
12
+ Custom redirects are currently disabled because <code>AME_DISABLE_REDIRECTS</code> is set to
13
+ <code>true</code>.
14
+ </p>
15
+ </div>
16
+ <?php
17
+ }
18
+ ?>
19
+ <div id="ame-redirector-ui-root" data-bind="visible: isLoaded" style="display: none">
20
+ <!-- A second level of tabs, uh oh! -->
21
+ <ul data-bind="foreach: availableTriggers" role="tablist" class="ame-rui-trigger-selector ame-rui-sub-tabs">
22
+ <li class="ame-rui-tab" data-bind="css: { 'ame-rui-active-tab': ($data.trigger === $root.selectedTrigger()) }">
23
+ <a data-bind="
24
+ text: label,
25
+ click: $root.selectedTrigger.bind($root.selectedTrigger, $data.trigger),
26
+ attr: {'data-text': label}"
27
+ class="ame-rui-tab-label"
28
+ role="tab"></a>
29
+ </li>
30
+ </ul>
31
+
32
+ <div id="ame-rui-column-container">
33
+ <div id="ame-rui-main-section">
34
+
35
+ <!-- ko if: (selectedTrigger() === 'registration') -->
36
+ <p>
37
+ The registration redirect happens immediately after someone registers a new account
38
+ but before they log in for the first time. By default, the user is redirected to
39
+ a "check your email" page.
40
+ </p>
41
+ <!-- /ko -->
42
+
43
+ <!-- ko if: currentTriggerView().supportsUserSettings -->
44
+ <h3>Users</h3>
45
+ <div data-bind="
46
+ template: {name: 'ame-redirect-list-template', data: {items: currentTriggerView().users}},
47
+ visible: currentTriggerView().users().length > 0"></div>
48
+
49
+ <!-- ko if: (userSelectionUi === 'dropdown') -->
50
+
51
+ <p>
52
+ <!-- ko if: (addableUsers().length > 0) -->
53
+ <label for="ame-rui-add-user" class="screen-reader-text">Add a user</label>
54
+ <select id="ame-rui-add-user" class="ame-rui-add-actor-dropdown"
55
+ data-bind="options: addableUsers,
56
+ optionsText: 'userLogin',
57
+ value: selectedUserToAdd,
58
+ optionsCaption: 'Add a user',
59
+ hasFocus: userSelectorHasFocus"></select>
60
+ <!-- /ko -->
61
+ <!-- ko if: (addableUsers().length <= 0) -->
62
+ <span class="description">This list includes all users.</span>
63
+ <!-- /ko -->
64
+ </p>
65
+ <!-- /ko -->
66
+ <!-- ko if: (userSelectionUi === 'search') -->
67
+ <form method="post" data-bind="submit: addEnteredUserLogin.bind($root)">
68
+ <p>
69
+ <label for="ame-rui-user-search-query" class="screen-reader-text">Search users</label>
70
+ <input type="text" id="ame-rui-user-search-query" placeholder="Enter a username"
71
+ data-bind="
72
+ textInput: userLoginQuery,
73
+ ameRuiUserAutocomplete: { filter: filterUserAutocompleteResults.bind($root) }">
74
+ <input type="button" class="button" id="ame-rui-add-user" value="Add user"
75
+ data-bind="enable: addUserButtonEnabled, click: addEnteredUserLogin.bind($root)">
76
+ </p>
77
+ </form>
78
+ <!-- /ko -->
79
+ <!-- /ko -->
80
+
81
+ <!-- ko if: currentTriggerView().supportsRoleSettings -->
82
+ <h3>Roles</h3>
83
+ <div data-bind="
84
+ template: {name: 'ame-redirect-list-template', data: {items: currentTriggerView().roles}},
85
+ visible: currentTriggerView().roles().length > 0"></div>
86
+
87
+ <p>
88
+ <!-- ko if: (addableRoles().length > 0) -->
89
+ <label for="ame-rui-add-role" style="display: none;">Add a role</label>
90
+ <select id="ame-rui-add-role" class="ame-rui-add-actor-dropdown"
91
+ data-bind="options: addableRoles,
92
+ optionsText: 'displayName',
93
+ value: selectedRoleToAdd,
94
+ optionsCaption: 'Add a role',
95
+ hasFocus: roleSelectorHasFocus"></select>
96
+ <!-- /ko -->
97
+ <!-- ko if: (addableRoles().length <= 0) -->
98
+ <span class="description">This list includes all roles.</span>
99
+ <!-- /ko -->
100
+ </p>
101
+ <!-- /ko -->
102
+
103
+ <!-- ko if: currentTriggerView().supportsActorSettings -->
104
+ <h3>Default</h3>
105
+ <p>
106
+ The default setting will be applied to users who don't match any of the above rules. Leave the field
107
+ empty to let WordPress or other plugins choose the redirect.
108
+ </p>
109
+ <!-- /ko -->
110
+ <!-- ko ifnot: currentTriggerView().supportsActorSettings -->
111
+ <h3>All Users</h3>
112
+ <!-- /ko -->
113
+ <div data-bind="using: currentTriggerView().defaultRedirect" class="ame-rui-default-redirect-container">
114
+ <div class="ame-rui-redirect">
115
+ <div class="ame-rui-redirect-content">
116
+ <ame-redirect-url-input params="redirect: $data, menuItems: $root.menuItems"
117
+ class="ame-rui-url-template"></ame-redirect-url-input>
118
+
119
+ <label class="ame-rui-shortcodes-enabled" title="Process shortcodes in the redirect URL">
120
+ <input type="checkbox" data-bind="checked: shortcodesEnabled, enable: canToggleShortcodes">
121
+ <span class="dashicons dashicons-shortcode"></span>
122
+ <span class="ame-rui-button-label">Enable shortcodes</span>
123
+ </label>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ </div>
129
+ <div id="ame-rui-sidebar">
130
+ <div id="ame-rui-main-actions">
131
+
132
+ </div>
133
+ </div>
134
+ </div>
135
+
136
+ <form class="ame-rui-save-form" method="post" data-bind="submit: saveChanges" action="<?php
137
+ echo esc_attr(add_query_arg(array('noheader' => '1'), $moduleTabUrl));
138
+ ?>">
139
+ <?php
140
+ submit_button(
141
+ null, 'primary', 'submit', true,
142
+ [
143
+ 'data-bind' => 'disable: isSaving',
144
+ 'disabled' => 'disabled',
145
+ ]
146
+ );
147
+ ?>
148
+ <input type="hidden" name="settings" value="" data-bind="value: settingsData">
149
+ <input type="hidden" name="action" value="ame-save-redirect-settings">
150
+ <?php wp_nonce_field('ame-save-redirect-settings'); ?>
151
+ <input type="hidden" name="selectedTrigger" data-bind="value: selectedTrigger">
152
+ </form>
153
+
154
+ <label for="ame-rui-menu-items" style="display: none">Admin menu items</label>
155
+ <select name="ame-rui-menu-items" id="ame-rui-menu-items" size="10" style="display: none;"
156
+ data-bind="options: menuDropdownOptions, optionsText: 'title', value: selectedMenuDropdownItem">
157
+ </select>
158
+ </div>
159
+
160
+ <div style="display: none;">
161
+ <template id="ame-redirect-list-template">
162
+ <div data-bind="sortable: {
163
+ data: $data.items,
164
+ allowDrop: false,
165
+ options: {
166
+ handle: '.ame-rui-drag-handle'
167
+ }}"
168
+ class="ame-rui-redirect-list">
169
+ <div class="ame-rui-redirect">
170
+ <div class="ame-rui-drag-handle">
171
+ <img src="<?php echo esc_attr($dragIconUrl); ?>" alt="Drag indicator" width="24">
172
+ </div>
173
+ <div class="ame-rui-redirect-content">
174
+ <div class="ame-rui-actor">
175
+ <label data-bind="text: displayName(), attr: {'for': inputElementId}"></label>
176
+ <span class="ame-rui-missing-actor-indicator"
177
+ data-bind="
178
+ if: $root.isMissingActor(actor),
179
+ attr:{title: 'This ' + actorTypeNoun() + ' does not exist on the current site.'}">
180
+ (missing)
181
+ </span>
182
+ </div>
183
+ <ame-redirect-url-input params="redirect: $data, menuItems: $root.menuItems"
184
+ class="ame-rui-url-template"></ame-redirect-url-input>
185
+ <label class="ame-rui-shortcodes-enabled" title="Process shortcodes in the redirect URL">
186
+ <input type="checkbox" data-bind="checked: shortcodesEnabled, enable: canToggleShortcodes">
187
+ <span class="dashicons dashicons-shortcode"></span>
188
+ <span class="ame-rui-button-label">Enable shortcodes</span>
189
+ </label>
190
+ <div class="ame-rui-actions">
191
+ <button class="button ame-rui-delete" title="Remove redirect"
192
+ data-bind="click: $parent.items.remove.bind($parent.items, $data)">
193
+ <span class="dashicons dashicons-trash"></span>
194
+ <span class="ame-rui-button-label">Remove</span>
195
+ </button>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ </template>
201
+
202
+ <template id="ame-redirect-url-component">
203
+ <!--suppress HtmlFormInputWithoutLabel -->
204
+ <input type="text"
205
+ data-bind="
206
+ value: displayValue,
207
+ attr: {'id' : redirect.inputElementId, 'readonly': isUrlReadonly},
208
+ hasFocus: redirect.inputHasFocus,
209
+ css: {'ame-rui-has-url-dropdown': redirect.urlDropdownEnabled}"
210
+ class="regular-text">
211
+ <!-- ko if: redirect.urlDropdownEnabled -->
212
+ <div class="ame-rui-url-dropdown-trigger">
213
+ <span class="am-rui-trigger-icon"></span>
214
+ </div>
215
+ <!-- /ko -->
216
+ </template>
217
+ </div>
modules/redirector/redirector-ui.js ADDED
@@ -0,0 +1,797 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference path="../../js/knockout.d.ts" />
2
+ /// <reference path="../../js/jquery.d.ts" />
3
+ /// <reference path="../../js/jqueryui.d.ts" />
4
+ /// <reference path="../../js/actor-manager.ts" />
5
+ /// <reference path="../actor-selector/actor-selector.ts" />
6
+ /// <reference path="../../js/common.d.ts" />
7
+ /// <reference path="../../ajax-wrapper/ajax-action-wrapper.d.ts" />
8
+ var __extends = (this && this.__extends) || (function () {
9
+ var extendStatics = function (d, b) {
10
+ extendStatics = Object.setPrototypeOf ||
11
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
12
+ function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
13
+ return extendStatics(d, b);
14
+ };
15
+ return function (d, b) {
16
+ if (typeof b !== "function" && b !== null)
17
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
18
+ extendStatics(d, b);
19
+ function __() { this.constructor = d; }
20
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
21
+ };
22
+ })();
23
+ var AmeRedirectorUi;
24
+ (function (AmeRedirectorUi) {
25
+ var AllKnownTriggers = {
26
+ login: null,
27
+ logout: null,
28
+ registration: null,
29
+ firstLogin: null
30
+ };
31
+ var _ = wsAmeLodash;
32
+ var AbstractTriggerDictionary = /** @class */ (function () {
33
+ function AbstractTriggerDictionary() {
34
+ }
35
+ return AbstractTriggerDictionary;
36
+ }());
37
+ var DefaultActorId = 'special:default';
38
+ var defaultActor = {
39
+ getDisplayName: function () {
40
+ return 'Default';
41
+ },
42
+ getId: function () {
43
+ return DefaultActorId;
44
+ }
45
+ };
46
+ var Redirect = /** @class */ (function () {
47
+ function Redirect(properties, actorProvider) {
48
+ var _this = this;
49
+ if (actorProvider === void 0) { actorProvider = null; }
50
+ this.actorId = properties.actorId;
51
+ this.trigger = properties.trigger;
52
+ this.urlTemplate = ko.observable(properties.urlTemplate);
53
+ this.menuTemplateId = ko.observable(properties.hasOwnProperty('menuTemplateId') ? properties.menuTemplateId : '');
54
+ this.canToggleShortcodes = ko.pureComputed(function () {
55
+ return (_this.menuTemplateId().trim() === '');
56
+ });
57
+ this.inputHasFocus = ko.observable(false);
58
+ var internalShortcodesEnabled = ko.observable(properties.shortcodesEnabled);
59
+ this.shortcodesEnabled = ko.computed({
60
+ read: function () {
61
+ //All of the menu items use shortcodes to generate the admin page URL,
62
+ //so shortcodes must be enabled when a menu item is selected.
63
+ var menu = _this.menuTemplateId().trim();
64
+ if (menu !== '') {
65
+ return true;
66
+ }
67
+ return internalShortcodesEnabled();
68
+ },
69
+ write: function (value) {
70
+ if (!_this.canToggleShortcodes()) {
71
+ return;
72
+ }
73
+ internalShortcodesEnabled(value);
74
+ },
75
+ deferEvaluation: true
76
+ });
77
+ if (this.actorId === DefaultActorId) {
78
+ this.actor = defaultActor;
79
+ }
80
+ else {
81
+ var provider = actorProvider ? actorProvider : AmeActors;
82
+ this.actor = provider.getActor(this.actorId);
83
+ }
84
+ this.actorTypeNoun = ko.pureComputed(function () {
85
+ var prefix = _this.actorId.substring(0, _this.actorId.indexOf(':'));
86
+ if (prefix === 'user') {
87
+ return 'user';
88
+ }
89
+ else if (prefix === 'role') {
90
+ return 'role';
91
+ }
92
+ return 'item';
93
+ });
94
+ this.urlDropdownEnabled = ko.pureComputed(function () {
95
+ //If a menu item is already selected in the dropdown, the dropdown has to be enabled
96
+ //to give the user the ability to select something else.
97
+ var menu = _this.menuTemplateId().trim();
98
+ if (menu !== '') {
99
+ return true;
100
+ }
101
+ //The dropdown only contains admin menu items, so it's only useful if the user
102
+ //can access the admin dashboard after the trigger happens.
103
+ //Note: This may need to change if we add other options to the dropdown.
104
+ return (_this.trigger === 'login') || (_this.trigger === 'firstLogin');
105
+ });
106
+ Redirect.inputCounter++;
107
+ this.inputElementId = 'ame-rui-unique-input-' + Redirect.inputCounter;
108
+ }
109
+ Redirect.prototype.toJs = function () {
110
+ var result = {
111
+ actorId: this.actorId,
112
+ urlTemplate: this.urlTemplate().trim(),
113
+ shortcodesEnabled: this.shortcodesEnabled(),
114
+ trigger: this.trigger
115
+ };
116
+ var menu = this.menuTemplateId().trim();
117
+ if (menu !== '') {
118
+ result.menuTemplateId = menu;
119
+ }
120
+ return result;
121
+ };
122
+ Redirect.prototype.displayName = function () {
123
+ if (this.actor.hasOwnProperty('userLogin')) {
124
+ var user = this.actor;
125
+ return user.userLogin;
126
+ }
127
+ else {
128
+ return this.actor.getDisplayName();
129
+ }
130
+ };
131
+ Redirect.inputCounter = 0;
132
+ return Redirect;
133
+ }());
134
+ AmeRedirectorUi.Redirect = Redirect;
135
+ var TriggerView = /** @class */ (function () {
136
+ function TriggerView(trigger, supportsUserSettings, supportsRoleSettings) {
137
+ var _this = this;
138
+ if (supportsUserSettings === void 0) { supportsUserSettings = null; }
139
+ if (supportsRoleSettings === void 0) { supportsRoleSettings = null; }
140
+ this.users = ko.observableArray([]);
141
+ this.roles = ko.observableArray([]);
142
+ this.supportsUserSettings = true;
143
+ this.supportsRoleSettings = true;
144
+ if (supportsUserSettings !== null) {
145
+ this.supportsUserSettings = supportsUserSettings;
146
+ }
147
+ if (supportsRoleSettings !== null) {
148
+ this.supportsRoleSettings = supportsRoleSettings;
149
+ }
150
+ this.supportsActorSettings = ko.pureComputed(function () {
151
+ return _this.supportsUserSettings || _this.supportsRoleSettings;
152
+ });
153
+ this.defaultRedirect = ko.observable(new Redirect({
154
+ actorId: 'special:default',
155
+ trigger: trigger,
156
+ shortcodesEnabled: true,
157
+ urlTemplate: ''
158
+ }));
159
+ }
160
+ TriggerView.prototype.add = function (item) {
161
+ var actorId = item.actorId;
162
+ if (actorId === DefaultActorId) {
163
+ this.defaultRedirect(item);
164
+ }
165
+ else if (actorId === 'special:super_admin') {
166
+ this.roles.push(item);
167
+ }
168
+ else {
169
+ var actorType = actorId.substring(0, actorId.indexOf(':'));
170
+ switch (actorType) {
171
+ case 'user':
172
+ this.users.push(item);
173
+ break;
174
+ case 'role':
175
+ this.roles.push(item);
176
+ break;
177
+ default:
178
+ console.log('Unknown actor type for a trigger view: ' + actorType);
179
+ }
180
+ }
181
+ };
182
+ TriggerView.prototype.toArray = function () {
183
+ var results = [];
184
+ results.push.apply(results, this.users());
185
+ results.push.apply(results, this.roles());
186
+ //Include the default redirect only if it's not empty.
187
+ var defaultRedirect = this.defaultRedirect();
188
+ var url = defaultRedirect.urlTemplate().trim();
189
+ if (url !== '') {
190
+ results.push(defaultRedirect);
191
+ }
192
+ return results;
193
+ };
194
+ return TriggerView;
195
+ }());
196
+ var MenuCollection = /** @class */ (function () {
197
+ function MenuCollection(usableMenuItems) {
198
+ this.menusByTemplate = {};
199
+ this.menusByTemplate = {};
200
+ for (var i = 0; i < usableMenuItems.length; i++) {
201
+ this.menusByTemplate[usableMenuItems[i].templateId] = usableMenuItems[i];
202
+ }
203
+ }
204
+ MenuCollection.prototype.findSelectedMenu = function (redirect) {
205
+ var templateId = redirect.menuTemplateId();
206
+ if (templateId === '') {
207
+ return null;
208
+ }
209
+ if (!this.menusByTemplate.hasOwnProperty(templateId)) {
210
+ return null;
211
+ }
212
+ var menu = this.menusByTemplate[templateId];
213
+ var url = redirect.urlTemplate();
214
+ if (menu.url === url) {
215
+ return menu;
216
+ }
217
+ return null;
218
+ };
219
+ return MenuCollection;
220
+ }());
221
+ var RedirectsByTrigger = /** @class */ (function (_super) {
222
+ __extends(RedirectsByTrigger, _super);
223
+ function RedirectsByTrigger() {
224
+ var _this = _super.call(this) || this;
225
+ _this.login = new TriggerView('login');
226
+ _this.logout = new TriggerView('logout');
227
+ _this.registration = new TriggerView('registration', false, false);
228
+ _this.firstLogin = new TriggerView('firstLogin', false, true);
229
+ return _this;
230
+ }
231
+ RedirectsByTrigger.fromArray = function (redirects) {
232
+ var instance = new RedirectsByTrigger();
233
+ var length = redirects.length;
234
+ for (var i = 0; i < length; i++) {
235
+ var item = redirects[i];
236
+ if (instance.hasOwnProperty(item.trigger)) {
237
+ var view = instance[item.trigger];
238
+ view.add(item);
239
+ }
240
+ }
241
+ return instance;
242
+ };
243
+ RedirectsByTrigger.prototype.toArray = function () {
244
+ var results = [];
245
+ for (var key in AllKnownTriggers) {
246
+ if (this.hasOwnProperty(key)) {
247
+ var view = this[key];
248
+ results.push.apply(results, view.toArray());
249
+ }
250
+ }
251
+ //Remove redirects that don't have a URL.
252
+ results = results.filter(function (redirect) {
253
+ var url = redirect.urlTemplate().trim();
254
+ return ((typeof url) === 'string') && (url !== '');
255
+ });
256
+ return results;
257
+ };
258
+ return RedirectsByTrigger;
259
+ }(AbstractTriggerDictionary));
260
+ var RedirectUrlInputComponent = /** @class */ (function () {
261
+ function RedirectUrlInputComponent(params) {
262
+ var _this = this;
263
+ this.redirect = ko.unwrap(params.redirect);
264
+ this.menuItems = params.menuItems;
265
+ this.displayValue = ko.computed({
266
+ read: function () {
267
+ var menu = _this.menuItems.findSelectedMenu(_this.redirect);
268
+ if (menu) {
269
+ return menu.title;
270
+ }
271
+ else {
272
+ return _this.redirect.urlTemplate();
273
+ }
274
+ },
275
+ write: function (value) {
276
+ var menu = _this.menuItems.findSelectedMenu(_this.redirect);
277
+ if (menu !== null) {
278
+ //Can't manually edit the URL because a menu item is selected.
279
+ return;
280
+ }
281
+ _this.redirect.urlTemplate(value);
282
+ }
283
+ });
284
+ this.isUrlReadonly = ko.pureComputed(function () {
285
+ if (_this.menuItems.findSelectedMenu(_this.redirect) !== null) {
286
+ return true;
287
+ }
288
+ return null;
289
+ });
290
+ }
291
+ return RedirectUrlInputComponent;
292
+ }());
293
+ AmeRedirectorUi.RedirectUrlInputComponent = RedirectUrlInputComponent;
294
+ /**
295
+ * Proxy class that automatically creates placeholders for missing actors.
296
+ */
297
+ var ActorProviderProxy = /** @class */ (function () {
298
+ function ActorProviderProxy(realProvider) {
299
+ this.provider = realProvider;
300
+ this.placeholders = {};
301
+ }
302
+ ActorProviderProxy.prototype.getActor = function (actorId) {
303
+ if (actorId === DefaultActorId) {
304
+ return defaultActor;
305
+ }
306
+ var existingActor = this.provider.getActor(actorId);
307
+ if (existingActor) {
308
+ return existingActor;
309
+ }
310
+ else if (this.placeholders.hasOwnProperty(actorId)) {
311
+ return this.placeholders[actorId];
312
+ }
313
+ //If the actor hasn't been loaded or created by now, that means it has been deleted
314
+ //or it was invalid to begin with. Let's use a placeholder object to represent it.
315
+ var missingActor;
316
+ if (_.startsWith(actorId, 'user:')) {
317
+ missingActor = new MissingUserPlaceholder(actorId);
318
+ }
319
+ else if (_.startsWith(actorId, 'role:')) {
320
+ missingActor = new MissingRolePlaceholder(actorId);
321
+ }
322
+ else {
323
+ missingActor = new MissingActorPlaceholder(actorId);
324
+ }
325
+ this.placeholders[actorId] = missingActor;
326
+ return missingActor;
327
+ };
328
+ return ActorProviderProxy;
329
+ }());
330
+ var MinimalUser = /** @class */ (function (_super) {
331
+ __extends(MinimalUser, _super);
332
+ function MinimalUser() {
333
+ return _super !== null && _super.apply(this, arguments) || this;
334
+ }
335
+ MinimalUser.createFromProperties = function (properties) {
336
+ return new MinimalUser(properties.user_login, properties.display_name, {}, [], false);
337
+ };
338
+ return MinimalUser;
339
+ }(AmeUser));
340
+ AmeRedirectorUi.MinimalUser = MinimalUser;
341
+ var MissingActorPlaceholder = /** @class */ (function () {
342
+ function MissingActorPlaceholder(id, displayName) {
343
+ if (displayName === void 0) { displayName = null; }
344
+ this.actorId = id;
345
+ if (displayName !== null) {
346
+ this.displayName = displayName;
347
+ }
348
+ else {
349
+ this.displayName = this.idWithoutPrefix(id);
350
+ }
351
+ }
352
+ MissingActorPlaceholder.prototype.getDisplayName = function () {
353
+ return this.displayName;
354
+ };
355
+ MissingActorPlaceholder.prototype.getId = function () {
356
+ return this.actorId;
357
+ };
358
+ MissingActorPlaceholder.prototype.idWithoutPrefix = function (actorId) {
359
+ var delimiterPos = actorId.indexOf(':');
360
+ if (delimiterPos < 0) {
361
+ return actorId;
362
+ }
363
+ return actorId.substring(delimiterPos + 1);
364
+ };
365
+ return MissingActorPlaceholder;
366
+ }());
367
+ var MissingRolePlaceholder = /** @class */ (function (_super) {
368
+ __extends(MissingRolePlaceholder, _super);
369
+ function MissingRolePlaceholder() {
370
+ return _super !== null && _super.apply(this, arguments) || this;
371
+ }
372
+ return MissingRolePlaceholder;
373
+ }(MissingActorPlaceholder));
374
+ var MissingUserPlaceholder = /** @class */ (function (_super) {
375
+ __extends(MissingUserPlaceholder, _super);
376
+ function MissingUserPlaceholder(actorId) {
377
+ var _this = _super.call(this, actorId) || this;
378
+ _this.isSuperAdmin = false;
379
+ _this.userLogin = _this.idWithoutPrefix(actorId);
380
+ return _this;
381
+ }
382
+ return MissingUserPlaceholder;
383
+ }(MissingActorPlaceholder));
384
+ var App = /** @class */ (function () {
385
+ function App(settings) {
386
+ var _this = this;
387
+ this.isLoaded = ko.observable(false);
388
+ this.availableTriggers = [
389
+ { trigger: 'login', label: 'Login Redirect' },
390
+ { trigger: 'logout', label: 'Logout Redirect' },
391
+ { trigger: 'registration', label: 'Registration Redirect' },
392
+ { trigger: 'firstLogin', label: 'First Login Redirect' }
393
+ ];
394
+ this.customUrlOption = {
395
+ templateId: '',
396
+ url: '',
397
+ title: '[ Custom URL ]'
398
+ };
399
+ this.ignoreNextDropdownClick = null;
400
+ this.userSelectionUi = 'dropdown';
401
+ var self = this;
402
+ this.actorProvider = new ActorProviderProxy(AmeActors);
403
+ //Users need to be loaded before redirects because redirects use actor objects.
404
+ var loadedUsers = settings.users.map(function (props) {
405
+ var existingInstance = AmeActors.getUser(props.user_login);
406
+ if (existingInstance) {
407
+ return existingInstance;
408
+ }
409
+ else {
410
+ var newUser = MinimalUser.createFromProperties(props);
411
+ AmeActors.addUsers([newUser]);
412
+ return newUser;
413
+ }
414
+ });
415
+ loadedUsers.sort(function (a, b) {
416
+ return a.userLogin.localeCompare(b.userLogin);
417
+ });
418
+ this.redirects = ko.observableArray(settings.redirects.map(function (props) { return new Redirect(props, _this.actorProvider); }));
419
+ this.menuItems = new MenuCollection(settings.usableMenuItems);
420
+ this.menuDropdownOptions = [this.customUrlOption].concat(settings.usableMenuItems);
421
+ this.menuDropdownParent = ko.observable(null);
422
+ this.selectedMenuDropdownItem = ko.computed({
423
+ read: function () {
424
+ var currentRedirect = _this.menuDropdownParent();
425
+ if (currentRedirect === null) {
426
+ return _this.customUrlOption;
427
+ }
428
+ else {
429
+ //Find the option that matches this template ID and URL.
430
+ var foundMenu = _this.menuItems.findSelectedMenu(currentRedirect);
431
+ if (foundMenu === null) {
432
+ foundMenu = _this.customUrlOption;
433
+ }
434
+ return foundMenu;
435
+ }
436
+ },
437
+ write: function (newValue) {
438
+ var currentRedirect = _this.menuDropdownParent();
439
+ if (!currentRedirect) {
440
+ return; //Nothing to do!
441
+ }
442
+ if (!newValue) {
443
+ newValue = _this.customUrlOption;
444
+ }
445
+ currentRedirect.menuTemplateId(newValue.templateId);
446
+ if (newValue.templateId !== '') {
447
+ currentRedirect.urlTemplate(newValue.url);
448
+ }
449
+ },
450
+ owner: self,
451
+ deferEvaluation: true
452
+ });
453
+ this.menuDropdown = jQuery('#ame-rui-menu-items');
454
+ //Hide the dropdown when it loses focus.
455
+ this.menuDropdown.on('blur', function () {
456
+ _this.closeMenuDropdown();
457
+ });
458
+ this.menuDropdown.on('keydown', function (event) {
459
+ //Also hide the dropdown if the user presses Esc.
460
+ if (event.which === 27) {
461
+ _this.closeMenuDropdown(true);
462
+ }
463
+ else if (event.which === 13) {
464
+ //Close the dropdown when the user presses Enter.
465
+ //Since we currently update the redirect on every change, there's no difference between
466
+ //this and pressing Esc.
467
+ _this.closeMenuDropdown(true);
468
+ }
469
+ });
470
+ //Close the dropdown when the user selects an option by clicking it.
471
+ this.menuDropdown.on('click', 'option', function () {
472
+ _this.closeMenuDropdown();
473
+ });
474
+ //this.addTestData();
475
+ this.byTrigger = ko.observable(RedirectsByTrigger.fromArray(this.redirects()));
476
+ //Reselect the previous trigger, or just the first trigger.
477
+ this.selectedTrigger = ko.observable(settings.selectedTrigger ? settings.selectedTrigger : this.availableTriggers[0].trigger);
478
+ this.currentTriggerView = ko.pureComputed(function () {
479
+ var trigger = _this.selectedTrigger();
480
+ var mapping = _this.byTrigger();
481
+ if (mapping.hasOwnProperty(trigger) && (mapping[trigger] instanceof TriggerView)) {
482
+ return mapping[trigger];
483
+ }
484
+ else {
485
+ return mapping.login;
486
+ }
487
+ });
488
+ this.addableRoles = ko.pureComputed(function () {
489
+ var allRoles = _.values(AmeActors.getRoles());
490
+ var usedRoles = _.map(_this.currentTriggerView().roles(), function (redirect) {
491
+ return redirect.actor;
492
+ });
493
+ return _.difference(allRoles, usedRoles);
494
+ });
495
+ this.selectedRoleToAdd = ko.observable(void 0);
496
+ this.roleSelectorHasFocus = ko.observable(false);
497
+ this.addableUsers = ko.pureComputed(function () {
498
+ var usedUsers = _.map(_this.currentTriggerView().users(), function (redirect) {
499
+ return redirect.actor;
500
+ });
501
+ return _.difference(loadedUsers, usedUsers);
502
+ });
503
+ this.selectedUserToAdd = ko.observable(void 0);
504
+ this.userSelectorHasFocus = ko.observable(false);
505
+ this.selectedRoleToAdd.subscribe(function (newSelection) {
506
+ _this.addSelectedActorTo(newSelection, _this.currentTriggerView().roles);
507
+ _this.roleSelectorHasFocus(false);
508
+ _this.selectedRoleToAdd(void 0);
509
+ });
510
+ this.selectedUserToAdd.subscribe(function (newSelection) {
511
+ _this.addSelectedActorTo(newSelection, _this.currentTriggerView().users);
512
+ _this.userSelectorHasFocus(false);
513
+ _this.selectedUserToAdd(void 0);
514
+ });
515
+ this.userLoginQuery = ko.observable('');
516
+ this.addUserButtonEnabled = ko.pureComputed(function () {
517
+ return (_this.userLoginQuery().trim() !== '');
518
+ });
519
+ if (settings.hasMoreUsers) {
520
+ this.userSelectionUi = 'search';
521
+ }
522
+ this.isSaving = ko.observable(false);
523
+ this.settingsData = ko.observable('');
524
+ this.isLoaded(true);
525
+ }
526
+ App.prototype.getSettings = function () {
527
+ return {
528
+ redirects: this.byTrigger().toArray().map(function (redirect) { return redirect.toJs(); })
529
+ };
530
+ };
531
+ App.prototype.onDropdownTrigger = function (event) {
532
+ //Note: There probably is some jQuery feature or library that makes dropdowns easier,
533
+ //but I already did this the hard way.
534
+ var $input = jQuery(event.target).closest('.ame-rui-url-template,ame-redirect-url-input').find('input').first();
535
+ var $node = $input.closest('.ame-rui-redirect');
536
+ if ($node.length < 1) {
537
+ return;
538
+ }
539
+ var redirect = ko.dataFor($node.get(0));
540
+ if (!(redirect instanceof AmeRedirectorUi.Redirect)) {
541
+ return;
542
+ }
543
+ //Clicking the same trigger a second time closes the dropdown.
544
+ if (event.type === 'mousedown') {
545
+ var isSameTrigger = this.menuDropdown.is(':visible') && (this.menuDropdownParent() === redirect);
546
+ if (isSameTrigger) {
547
+ //The dropdown will be automatically closed by its "blur" event handler,
548
+ //but we need to ignore the next click event on this element.
549
+ this.ignoreNextDropdownClick = event.target;
550
+ }
551
+ else {
552
+ this.ignoreNextDropdownClick = null;
553
+ }
554
+ return;
555
+ }
556
+ if ((event.type === 'click') && (event.target === this.ignoreNextDropdownClick)) {
557
+ return;
558
+ }
559
+ //Move the drop-down near the input box.
560
+ this.menuDropdown
561
+ .css({
562
+ position: 'absolute',
563
+ zIndex: 100 //The dropdown should be displayed above other elements. This may not be required.
564
+ })
565
+ .show()
566
+ .outerWidth(Math.max($input.outerWidth(), 100))
567
+ .position({
568
+ my: 'right top',
569
+ at: 'right bottom',
570
+ of: $input
571
+ });
572
+ //Move focus to the dropdown.
573
+ var $select = this.menuDropdown;
574
+ if (!this.menuDropdown.is('select, input')) {
575
+ $select = this.menuDropdown.find('select, input').first();
576
+ }
577
+ $select.trigger('focus');
578
+ //Select the current option and scroll it into view. It looks like the browser will automatically
579
+ //scroll to the selected option, but only if the select element is already visible, so we need to
580
+ //do this *after* we show the dropdown.
581
+ this.menuDropdownParent(redirect);
582
+ };
583
+ App.prototype.closeMenuDropdown = function (moveFocusToInput) {
584
+ if (moveFocusToInput === void 0) { moveFocusToInput = false; }
585
+ var currentRedirect = this.menuDropdownParent();
586
+ this.menuDropdown.hide();
587
+ this.menuDropdownParent(null);
588
+ //Refocus on the URL input after closing the dropdown.
589
+ if (moveFocusToInput && currentRedirect) {
590
+ currentRedirect.inputHasFocus(true);
591
+ }
592
+ };
593
+ App.prototype.addSelectedActorTo = function (actor, list) {
594
+ //The list includes a caption item that is displayed when nothing is selected.
595
+ //The value of that option is supposed to be undefined.
596
+ if ((typeof actor === 'undefined') || (actor === null) || !this.currentTriggerView()) {
597
+ return;
598
+ }
599
+ //Add a redirect for the selected role.
600
+ var newRedirect = new Redirect({
601
+ actorId: actor.getId(),
602
+ shortcodesEnabled: true,
603
+ urlTemplate: '',
604
+ trigger: this.selectedTrigger()
605
+ }, this.actorProvider);
606
+ list.push(newRedirect);
607
+ newRedirect.inputHasFocus(true);
608
+ };
609
+ App.prototype.addEnteredUserLogin = function () {
610
+ var userLogin = this.userLoginQuery().trim();
611
+ if (userLogin === '') {
612
+ return;
613
+ }
614
+ var actorId = 'user:' + userLogin;
615
+ if (!AmeActors.actorExists(actorId)) {
616
+ if (console && console.warn) {
617
+ console.warn('User "' + userLogin + '" has not been initialized. Creating a minimal actor now.');
618
+ }
619
+ AmeActors.addUsers([
620
+ MinimalUser.createFromProperties({
621
+ user_login: userLogin,
622
+ display_name: userLogin
623
+ })
624
+ ]);
625
+ }
626
+ //Only add each user once.
627
+ var alreadyAdded = _.some(this.currentTriggerView().users(), function (redirect) {
628
+ return redirect.actorId === actorId;
629
+ });
630
+ if (alreadyAdded) {
631
+ alert('Error: Duplicate entry. User "' + userLogin + '" has already been added.');
632
+ return;
633
+ }
634
+ var newRedirect = new Redirect({
635
+ actorId: actorId,
636
+ shortcodesEnabled: true,
637
+ urlTemplate: '',
638
+ trigger: this.selectedTrigger()
639
+ }, this.actorProvider);
640
+ this.currentTriggerView().users.push(newRedirect);
641
+ this.userLoginQuery('');
642
+ };
643
+ App.prototype.filterUserAutocompleteResults = function (results) {
644
+ //Filter out users that are already in the current list.
645
+ var usedLogins = _.indexBy(this.currentTriggerView().users(), function (redirect) {
646
+ return redirect.actor.userLogin;
647
+ });
648
+ return _.filter(results, function (props) {
649
+ return !(usedLogins.hasOwnProperty(props.user_login));
650
+ });
651
+ };
652
+ App.prototype.isMissingActor = function (actor) {
653
+ return (actor instanceof MissingActorPlaceholder);
654
+ };
655
+ App.prototype.saveChanges = function () {
656
+ this.isSaving(true);
657
+ this.settingsData(ko.toJSON(this.getSettings()));
658
+ return true;
659
+ };
660
+ App.prototype.addTestData = function () {
661
+ //Add some test data.
662
+ this.redirects.push(new Redirect({
663
+ actorId: 'role:editor',
664
+ urlTemplate: '[wp-admin]edit.php',
665
+ trigger: 'login',
666
+ shortcodesEnabled: true
667
+ }, this.actorProvider));
668
+ this.redirects.push(new Redirect({
669
+ actorId: 'role:author',
670
+ urlTemplate: '[wp-admin]profile.php',
671
+ trigger: 'login',
672
+ shortcodesEnabled: true
673
+ }, this.actorProvider));
674
+ this.redirects.push(new Redirect({
675
+ actorId: 'user:admin',
676
+ urlTemplate: '[wp-admin]index.php',
677
+ trigger: 'login',
678
+ shortcodesEnabled: true
679
+ }, this.actorProvider));
680
+ this.redirects.push(new Redirect({
681
+ actorId: 'role:contributor',
682
+ urlTemplate: '[wp-admin]index.php',
683
+ trigger: 'login',
684
+ shortcodesEnabled: true
685
+ }, this.actorProvider));
686
+ this.redirects.push(new Redirect({
687
+ actorId: 'role:nonexistent',
688
+ urlTemplate: '[wp-admin]options-general.php',
689
+ trigger: 'login',
690
+ shortcodesEnabled: true
691
+ }, this.actorProvider));
692
+ this.redirects.push(new Redirect({
693
+ actorId: 'user:notarealuser',
694
+ urlTemplate: '[wp-admin]index.php',
695
+ trigger: 'login',
696
+ shortcodesEnabled: true
697
+ }, this.actorProvider));
698
+ this.redirects.push(new Redirect({
699
+ actorId: DefaultActorId,
700
+ urlTemplate: '[wp-admin]index.php?this-is-the-default=yep',
701
+ trigger: 'login',
702
+ shortcodesEnabled: true
703
+ }, this.actorProvider));
704
+ this.redirects.push(new Redirect({
705
+ actorId: 'role:administrator',
706
+ urlTemplate: '[wp-admin]options-general.php',
707
+ trigger: 'login',
708
+ shortcodesEnabled: true
709
+ }, this.actorProvider));
710
+ };
711
+ return App;
712
+ }());
713
+ AmeRedirectorUi.App = App;
714
+ })(AmeRedirectorUi || (AmeRedirectorUi = {}));
715
+ jQuery(function ($) {
716
+ ko.components.register('ame-redirect-url-input', {
717
+ viewModel: AmeRedirectorUi.RedirectUrlInputComponent,
718
+ template: { element: 'ame-redirect-url-component' }
719
+ });
720
+ //The user autocomplete feature is implemented as a custom binding only because that makes it easier
721
+ //to correctly initialise it when Knockout changes the DOM. The binding is not intended to be reusable.
722
+ ko.bindingHandlers.ameRuiUserAutocomplete = {
723
+ init: function (element, valueAccessor) {
724
+ var options = ko.unwrap(valueAccessor());
725
+ options = wsAmeLodash.defaults(options, {
726
+ filter: function (suggestions) {
727
+ return suggestions;
728
+ }
729
+ });
730
+ jQuery(element).autocomplete({
731
+ minLength: 2,
732
+ source: function (request, response) {
733
+ var action = AjawV1.getAction('ws-ame-rui-search-users');
734
+ action.get({ term: request.term }, function (results) {
735
+ //Filter received users.
736
+ if (options.filter) {
737
+ results = options.filter(results);
738
+ }
739
+ response(results);
740
+ }, function (error) {
741
+ response([]);
742
+ if (console && console.error) {
743
+ console.error(error);
744
+ }
745
+ });
746
+ },
747
+ select: function (unusedEvent, ui) {
748
+ var props = ui.item;
749
+ var existingUser = AmeActors.getUser(props.user_login);
750
+ if (existingUser === null) {
751
+ AmeActors.addUsers([AmeRedirectorUi.MinimalUser.createFromProperties(props)]);
752
+ }
753
+ },
754
+ classes: {
755
+ 'ui-autocomplete': 'ame-rui-found-users'
756
+ }
757
+ });
758
+ ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
759
+ jQuery(element).autocomplete('destroy');
760
+ });
761
+ }
762
+ };
763
+ var $container = $('#ame-redirector-ui-root');
764
+ var ameRedirectorApp = new AmeRedirectorUi.App(wsAmeRedirectorSettings);
765
+ ko.applyBindings(ameRedirectorApp, $container.get(0));
766
+ //Open the menu dropdown when the user clicks the trigger icon or presses
767
+ //the down arrow key in the redirect input field.
768
+ $container.on('mousedown click', '.ame-rui-url-dropdown-trigger', function (event) {
769
+ ameRedirectorApp.onDropdownTrigger(event);
770
+ });
771
+ /*
772
+ Releasing the "down" key only opens the dropdown if the key was pressed in the same input.
773
+ This is to avoid a confusing situation where the user selects a role from the "add a role"
774
+ dropdown using arrow keys and then the menu dropdown immediately shows up because the focus
775
+ moved to the redirect input before the user could release the key.
776
+ */
777
+ var redirectInputSelector = '.ame-rui-url-template input[type=text].ame-rui-has-url-dropdown';
778
+ var lastDownArrowTarget = null;
779
+ $container.on('focus', redirectInputSelector, function () {
780
+ lastDownArrowTarget = null;
781
+ });
782
+ $container.on('keydown', redirectInputSelector, function (event) {
783
+ //Ignore repeated "keydown" events. These will happen even if the key was originally
784
+ //pressed in a different element.
785
+ if ((typeof event.originalEvent['repeat'] !== 'undefined') && (event.originalEvent['repeat'] === true)) {
786
+ return;
787
+ }
788
+ if (event.which === 40) {
789
+ lastDownArrowTarget = event.target;
790
+ }
791
+ });
792
+ $container.on('keyup', redirectInputSelector, function (event) {
793
+ if ((event.which === 40) && (event.target === lastDownArrowTarget)) {
794
+ ameRedirectorApp.onDropdownTrigger(event);
795
+ }
796
+ });
797
+ });
modules/redirector/redirector-ui.ts ADDED
@@ -0,0 +1,994 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference path="../../js/knockout.d.ts" />
2
+ /// <reference path="../../js/jquery.d.ts" />
3
+ /// <reference path="../../js/jqueryui.d.ts" />
4
+ /// <reference path="../../js/actor-manager.ts" />
5
+ /// <reference path="../actor-selector/actor-selector.ts" />
6
+ /// <reference path="../../js/common.d.ts" />
7
+ /// <reference path="../../ajax-wrapper/ajax-action-wrapper.d.ts" />
8
+
9
+ declare var wsAmeRedirectorSettings: AmeRedirectorUi.ScriptData;
10
+ declare var wsAmeLodash: _.LoDashStatic;
11
+
12
+ namespace AmeRedirectorUi {
13
+ const AllKnownTriggers = {
14
+ login: null,
15
+ logout: null,
16
+ registration: null,
17
+ firstLogin: null
18
+ }
19
+
20
+ const _ = wsAmeLodash;
21
+
22
+ type RedirectTrigger = keyof typeof AllKnownTriggers;
23
+
24
+ type TriggerDictionary<ValueType> = {
25
+ [property in RedirectTrigger]: ValueType;
26
+ }
27
+
28
+ abstract class AbstractTriggerDictionary<ValueType> implements TriggerDictionary<ValueType> {
29
+ login: ValueType;
30
+ logout: ValueType;
31
+ registration: ValueType;
32
+ firstLogin: ValueType;
33
+ }
34
+
35
+ interface RedirectProperties {
36
+ actorId: string;
37
+ urlTemplate: string;
38
+ menuTemplateId?: string;
39
+
40
+ shortcodesEnabled: boolean;
41
+ trigger: RedirectTrigger;
42
+ }
43
+
44
+ interface MenuItemProperties {
45
+ templateId: string;
46
+ url: string;
47
+ title: string;
48
+ }
49
+
50
+ interface StorableSettings {
51
+ redirects: RedirectProperties[];
52
+ }
53
+
54
+ export interface ScriptData extends StorableSettings {
55
+ usableMenuItems: MenuItemProperties[];
56
+ users: MinimalUserProperties[];
57
+ hasMoreUsers: boolean;
58
+ selectedTrigger?: RedirectTrigger;
59
+ }
60
+
61
+ const DefaultActorId = 'special:default';
62
+ const defaultActor: IAmeActor = {
63
+ getDisplayName(): string {
64
+ return 'Default';
65
+ },
66
+ getId(): string {
67
+ return DefaultActorId;
68
+ }
69
+ }
70
+
71
+ export class Redirect {
72
+ protected static inputCounter: number = 0
73
+
74
+ actorId: RedirectProperties['actorId'];
75
+ actor: IAmeActor;
76
+ urlTemplate: KnockoutObservable<string>;
77
+ menuTemplateId: KnockoutObservable<string>;
78
+ shortcodesEnabled: KnockoutObservable<boolean>;
79
+ trigger: RedirectProperties['trigger'];
80
+
81
+ canToggleShortcodes: KnockoutObservable<boolean>;
82
+
83
+ inputElementId: string;
84
+ inputHasFocus: KnockoutObservable<boolean>;
85
+
86
+ actorTypeNoun: KnockoutObservable<string>;
87
+
88
+ urlDropdownEnabled: KnockoutComputed<boolean>;
89
+
90
+ constructor(properties: RedirectProperties, actorProvider: ActorProvider = null) {
91
+ this.actorId = properties.actorId;
92
+ this.trigger = properties.trigger;
93
+ this.urlTemplate = ko.observable(properties.urlTemplate);
94
+
95
+ this.menuTemplateId = ko.observable(
96
+ properties.hasOwnProperty('menuTemplateId') ? properties.menuTemplateId : ''
97
+ );
98
+
99
+ this.canToggleShortcodes = ko.pureComputed(() => {
100
+ return (this.menuTemplateId().trim() === '');
101
+ });
102
+ this.inputHasFocus = ko.observable(false);
103
+
104
+ const internalShortcodesEnabled = ko.observable(properties.shortcodesEnabled);
105
+ this.shortcodesEnabled = ko.computed<boolean>({
106
+ read: () => {
107
+ //All of the menu items use shortcodes to generate the admin page URL,
108
+ //so shortcodes must be enabled when a menu item is selected.
109
+ const menu = this.menuTemplateId().trim();
110
+ if (menu !== '') {
111
+ return true;
112
+ }
113
+ return internalShortcodesEnabled();
114
+ },
115
+ write: (value: boolean) => {
116
+ if (!this.canToggleShortcodes()) {
117
+ return;
118
+ }
119
+ internalShortcodesEnabled(value);
120
+ },
121
+ deferEvaluation: true
122
+ });
123
+
124
+ if (this.actorId === DefaultActorId) {
125
+ this.actor = defaultActor;
126
+ } else {
127
+ const provider: ActorProvider = actorProvider ? actorProvider : AmeActors;
128
+ this.actor = provider.getActor(this.actorId);
129
+ }
130
+
131
+ this.actorTypeNoun = ko.pureComputed(() => {
132
+ const prefix = this.actorId.substring(0, this.actorId.indexOf(':'));
133
+ if (prefix === 'user') {
134
+ return 'user';
135
+ } else if (prefix === 'role') {
136
+ return 'role'
137
+ }
138
+ return 'item';
139
+ });
140
+
141
+ this.urlDropdownEnabled = ko.pureComputed(() => {
142
+ //If a menu item is already selected in the dropdown, the dropdown has to be enabled
143
+ //to give the user the ability to select something else.
144
+ const menu = this.menuTemplateId().trim();
145
+ if (menu !== '') {
146
+ return true;
147
+ }
148
+
149
+ //The dropdown only contains admin menu items, so it's only useful if the user
150
+ //can access the admin dashboard after the trigger happens.
151
+ //Note: This may need to change if we add other options to the dropdown.
152
+ return (this.trigger === 'login') || (this.trigger === 'firstLogin');
153
+ });
154
+
155
+ Redirect.inputCounter++;
156
+ this.inputElementId = 'ame-rui-unique-input-' + Redirect.inputCounter;
157
+ }
158
+
159
+ toJs(): RedirectProperties {
160
+ let result: RedirectProperties = {
161
+ actorId: this.actorId,
162
+ urlTemplate: this.urlTemplate().trim(),
163
+ shortcodesEnabled: this.shortcodesEnabled(),
164
+ trigger: this.trigger
165
+ };
166
+
167
+ const menu = this.menuTemplateId().trim();
168
+ if (menu !== '') {
169
+ result.menuTemplateId = menu;
170
+ }
171
+
172
+ return result;
173
+ }
174
+
175
+ displayName(): string {
176
+ if (this.actor.hasOwnProperty('userLogin')) {
177
+ const user = this.actor as IAmeUser;
178
+ return user.userLogin;
179
+ } else {
180
+ return this.actor.getDisplayName();
181
+ }
182
+ }
183
+ }
184
+
185
+ class TriggerView {
186
+ users: KnockoutObservableArray<Redirect> = ko.observableArray([]);
187
+ roles: KnockoutObservableArray<Redirect> = ko.observableArray([]);
188
+ defaultRedirect: KnockoutObservable<Redirect>;
189
+
190
+ supportsUserSettings: boolean = true;
191
+ supportsRoleSettings: boolean = true;
192
+ supportsActorSettings: KnockoutComputed<boolean>;
193
+
194
+ constructor(
195
+ trigger: RedirectTrigger,
196
+ supportsUserSettings: boolean = null,
197
+ supportsRoleSettings: boolean = null
198
+ ) {
199
+ if (supportsUserSettings !== null) {
200
+ this.supportsUserSettings = supportsUserSettings;
201
+ }
202
+ if (supportsRoleSettings !== null) {
203
+ this.supportsRoleSettings = supportsRoleSettings;
204
+ }
205
+ this.supportsActorSettings = ko.pureComputed(() => {
206
+ return this.supportsUserSettings || this.supportsRoleSettings;
207
+ });
208
+
209
+ this.defaultRedirect = ko.observable(new Redirect({
210
+ actorId: 'special:default',
211
+ trigger: trigger,
212
+ shortcodesEnabled: true,
213
+ urlTemplate: ''
214
+ }));
215
+ }
216
+
217
+ add(item: Redirect) {
218
+ const actorId = item.actorId;
219
+ if (actorId === DefaultActorId) {
220
+ this.defaultRedirect(item);
221
+ } else if (actorId === 'special:super_admin') {
222
+ this.roles.push(item);
223
+ } else {
224
+ const actorType = actorId.substring(0, actorId.indexOf(':'));
225
+ switch (actorType) {
226
+ case 'user':
227
+ this.users.push(item);
228
+ break;
229
+ case 'role':
230
+ this.roles.push(item);
231
+ break;
232
+ default:
233
+ console.log('Unknown actor type for a trigger view: ' + actorType);
234
+ }
235
+ }
236
+ }
237
+
238
+ toArray(): Redirect[] {
239
+ let results = [];
240
+ results.push(...this.users());
241
+ results.push(...this.roles());
242
+
243
+ //Include the default redirect only if it's not empty.
244
+ const defaultRedirect = this.defaultRedirect();
245
+ const url = defaultRedirect.urlTemplate().trim();
246
+ if (url !== '') {
247
+ results.push(defaultRedirect);
248
+ }
249
+
250
+ return results;
251
+ }
252
+ }
253
+
254
+ class MenuCollection {
255
+ menusByTemplate: AmeDictionary<MenuItemProperties> = {};
256
+
257
+ constructor(usableMenuItems: MenuItemProperties[]) {
258
+ this.menusByTemplate = {};
259
+ for (let i = 0; i < usableMenuItems.length; i++) {
260
+ this.menusByTemplate[usableMenuItems[i].templateId] = usableMenuItems[i];
261
+ }
262
+ }
263
+
264
+ findSelectedMenu(redirect: Redirect): MenuItemProperties | null {
265
+ const templateId = redirect.menuTemplateId();
266
+ if (templateId === '') {
267
+ return null;
268
+ }
269
+ if (!this.menusByTemplate.hasOwnProperty(templateId)) {
270
+ return null;
271
+ }
272
+
273
+ const menu = this.menusByTemplate[templateId];
274
+ const url = redirect.urlTemplate();
275
+ if (menu.url === url) {
276
+ return menu;
277
+ }
278
+ return null;
279
+ }
280
+ }
281
+
282
+ class RedirectsByTrigger extends AbstractTriggerDictionary<TriggerView> {
283
+ constructor() {
284
+ super();
285
+ this.login = new TriggerView('login');
286
+ this.logout = new TriggerView('logout');
287
+ this.registration = new TriggerView('registration', false, false);
288
+ this.firstLogin = new TriggerView('firstLogin', false, true);
289
+ }
290
+
291
+ public static fromArray(redirects: Redirect[]): RedirectsByTrigger {
292
+ const instance = new RedirectsByTrigger();
293
+
294
+ const length = redirects.length;
295
+ for (let i = 0; i < length; i++) {
296
+ const item = redirects[i];
297
+ if (instance.hasOwnProperty(item.trigger)) {
298
+ const view = instance[item.trigger] as TriggerView;
299
+ view.add(item);
300
+ }
301
+ }
302
+
303
+ return instance;
304
+ }
305
+
306
+ toArray(): Redirect[] {
307
+ let results: Redirect[] = [];
308
+
309
+ for (let key in AllKnownTriggers) {
310
+ if (this.hasOwnProperty(key)) {
311
+ const view = this[key] as TriggerView;
312
+ results.push(...view.toArray());
313
+ }
314
+ }
315
+
316
+ //Remove redirects that don't have a URL.
317
+ results = results.filter(function (redirect) {
318
+ const url = redirect.urlTemplate().trim();
319
+ return ((typeof url) === 'string') && (url !== '');
320
+ });
321
+
322
+ return results;
323
+ }
324
+ }
325
+
326
+ export class RedirectUrlInputComponent {
327
+ redirect: Redirect;
328
+ displayValue: KnockoutComputed<string>;
329
+ isUrlReadonly: KnockoutComputed<boolean | null>;
330
+ menuItems: MenuCollection;
331
+
332
+ constructor(params: AmeDictionary<any>) {
333
+ this.redirect = ko.unwrap(params.redirect) as Redirect;
334
+ this.menuItems = params.menuItems as MenuCollection;
335
+
336
+ this.displayValue = ko.computed({
337
+ read: (): string => {
338
+ const menu = this.menuItems.findSelectedMenu(this.redirect);
339
+ if (menu) {
340
+ return menu.title;
341
+ } else {
342
+ return this.redirect.urlTemplate();
343
+ }
344
+ },
345
+ write: (value: string) => {
346
+ const menu = this.menuItems.findSelectedMenu(this.redirect);
347
+ if (menu !== null) {
348
+ //Can't manually edit the URL because a menu item is selected.
349
+ return;
350
+ }
351
+ this.redirect.urlTemplate(value);
352
+ }
353
+ });
354
+
355
+ this.isUrlReadonly = ko.pureComputed(() => {
356
+ if (this.menuItems.findSelectedMenu(this.redirect) !== null) {
357
+ return true;
358
+ }
359
+ return null;
360
+ });
361
+ }
362
+ }
363
+
364
+ interface ActorProvider {
365
+ getActor(actorId): IAmeActor;
366
+ }
367
+
368
+ /**
369
+ * Proxy class that automatically creates placeholders for missing actors.
370
+ */
371
+ class ActorProviderProxy implements ActorProvider {
372
+ private provider: ActorProvider;
373
+ private readonly placeholders: AmeDictionary<IAmeActor>;
374
+
375
+ constructor(realProvider: ActorProvider) {
376
+ this.provider = realProvider;
377
+ this.placeholders = {};
378
+ }
379
+
380
+ getActor(actorId): IAmeActor {
381
+ if (actorId === DefaultActorId) {
382
+ return defaultActor;
383
+ }
384
+
385
+ const existingActor = this.provider.getActor(actorId);
386
+ if (existingActor) {
387
+ return existingActor;
388
+ } else if (this.placeholders.hasOwnProperty(actorId)) {
389
+ return this.placeholders[actorId];
390
+ }
391
+
392
+ //If the actor hasn't been loaded or created by now, that means it has been deleted
393
+ //or it was invalid to begin with. Let's use a placeholder object to represent it.
394
+ let missingActor;
395
+ if (_.startsWith(actorId, 'user:')) {
396
+ missingActor = new MissingUserPlaceholder(actorId);
397
+ } else if (_.startsWith(actorId, 'role:')) {
398
+ missingActor = new MissingRolePlaceholder(actorId);
399
+ } else {
400
+ missingActor = new MissingActorPlaceholder(actorId);
401
+ }
402
+ this.placeholders[actorId] = missingActor;
403
+
404
+ return missingActor;
405
+ }
406
+ }
407
+
408
+ //For this feature we only need enough information to display and identify a user.
409
+ export interface MinimalUserProperties {
410
+ user_login: string;
411
+ display_name: string;
412
+ }
413
+
414
+ export class MinimalUser extends AmeUser {
415
+ static createFromProperties(properties: MinimalUserProperties): MinimalUser {
416
+ return new MinimalUser(
417
+ properties.user_login,
418
+ properties.display_name,
419
+ {},
420
+ [],
421
+ false
422
+ );
423
+ }
424
+ }
425
+
426
+ class MissingActorPlaceholder implements IAmeActor {
427
+ protected actorId: string;
428
+ protected displayName: string;
429
+
430
+ constructor(id: string, displayName: string = null) {
431
+ this.actorId = id;
432
+ if (displayName !== null) {
433
+ this.displayName = displayName;
434
+ } else {
435
+ this.displayName = this.idWithoutPrefix(id);
436
+ }
437
+ }
438
+
439
+ getDisplayName(): string {
440
+ return this.displayName;
441
+ }
442
+
443
+ getId(): string {
444
+ return this.actorId;
445
+ }
446
+
447
+ protected idWithoutPrefix(actorId: string): string {
448
+ const delimiterPos = actorId.indexOf(':');
449
+ if (delimiterPos < 0) {
450
+ return actorId;
451
+ }
452
+ return actorId.substring(delimiterPos + 1);
453
+ }
454
+ }
455
+
456
+ class MissingRolePlaceholder extends MissingActorPlaceholder {
457
+ }
458
+
459
+ class MissingUserPlaceholder extends MissingActorPlaceholder implements IAmeUser {
460
+ isSuperAdmin: boolean = false;
461
+ userLogin: string;
462
+
463
+ constructor(actorId: string) {
464
+ super(actorId);
465
+ this.userLogin = this.idWithoutPrefix(actorId);
466
+ }
467
+ }
468
+
469
+ export class App {
470
+ isLoaded: KnockoutObservable<boolean> = ko.observable(false);
471
+
472
+ redirects: KnockoutObservableArray<Redirect>;
473
+
474
+ selectedTrigger: KnockoutObservable<RedirectTrigger>;
475
+ byTrigger: KnockoutObservable<RedirectsByTrigger>;
476
+ currentTriggerView: KnockoutComputed<TriggerView>;
477
+
478
+ availableTriggers: { trigger: RedirectTrigger, label: string }[] = [
479
+ {trigger: 'login', label: 'Login Redirect'},
480
+ {trigger: 'logout', label: 'Logout Redirect'},
481
+ {trigger: 'registration', label: 'Registration Redirect'},
482
+ {trigger: 'firstLogin', label: 'First Login Redirect'}
483
+ ];
484
+
485
+ menuItems: MenuCollection;
486
+
487
+ menuDropdownParent: KnockoutObservable<Redirect | null>;
488
+ menuDropdownOptions: MenuItemProperties[];
489
+ selectedMenuDropdownItem: KnockoutObservable<MenuItemProperties>;
490
+ readonly customUrlOption: MenuItemProperties = {
491
+ templateId: '',
492
+ url: '',
493
+ title: '[ Custom URL ]'
494
+ };
495
+
496
+ private readonly menuDropdown: JQuery;
497
+ private ignoreNextDropdownClick: JQueryEventObject['target'] = null;
498
+
499
+ addableRoles: KnockoutComputed<IAmeActor[]>;
500
+ selectedRoleToAdd: KnockoutObservable<IAmeActor | undefined>;
501
+ roleSelectorHasFocus: KnockoutObservable<boolean>;
502
+
503
+ addableUsers: KnockoutComputed<IAmeUser[]>;
504
+ selectedUserToAdd: KnockoutObservable<IAmeUser | undefined>;
505
+ userSelectorHasFocus: KnockoutObservable<boolean>;
506
+ userSelectionUi: 'dropdown' | 'search' = 'dropdown';
507
+
508
+ userLoginQuery: KnockoutObservable<string>;
509
+ addUserButtonEnabled: KnockoutObservable<boolean>;
510
+
511
+ actorProvider: ActorProviderProxy;
512
+
513
+ isSaving: KnockoutObservable<boolean>;
514
+ settingsData: KnockoutObservable<string>;
515
+
516
+ constructor(settings: ScriptData) {
517
+ const self = this;
518
+
519
+ this.actorProvider = new ActorProviderProxy(AmeActors);
520
+
521
+ //Users need to be loaded before redirects because redirects use actor objects.
522
+ let loadedUsers = settings.users.map(
523
+ (props) => {
524
+ const existingInstance = AmeActors.getUser(props.user_login);
525
+ if (existingInstance) {
526
+ return existingInstance;
527
+ } else {
528
+ const newUser = MinimalUser.createFromProperties(props);
529
+ AmeActors.addUsers([newUser]);
530
+ return newUser;
531
+ }
532
+ }
533
+ );
534
+ loadedUsers.sort(function (a, b) {
535
+ return a.userLogin.localeCompare(b.userLogin);
536
+ });
537
+
538
+ this.redirects = ko.observableArray(settings.redirects.map(
539
+ props => new Redirect(props, this.actorProvider))
540
+ );
541
+ this.menuItems = new MenuCollection(settings.usableMenuItems);
542
+
543
+ this.menuDropdownOptions = [this.customUrlOption].concat(settings.usableMenuItems);
544
+ this.menuDropdownParent = ko.observable(null);
545
+
546
+ this.selectedMenuDropdownItem = ko.computed<MenuItemProperties>({
547
+ read: () => {
548
+ const currentRedirect = this.menuDropdownParent();
549
+ if (currentRedirect === null) {
550
+ return this.customUrlOption;
551
+ } else {
552
+ //Find the option that matches this template ID and URL.
553
+ let foundMenu = this.menuItems.findSelectedMenu(currentRedirect);
554
+ if (foundMenu === null) {
555
+ foundMenu = this.customUrlOption;
556
+ }
557
+ return foundMenu;
558
+ }
559
+ },
560
+ write: (newValue) => {
561
+ const currentRedirect = this.menuDropdownParent();
562
+ if (!currentRedirect) {
563
+ return; //Nothing to do!
564
+ }
565
+
566
+ if (!newValue) {
567
+ newValue = this.customUrlOption;
568
+ }
569
+
570
+ currentRedirect.menuTemplateId(newValue.templateId);
571
+ if (newValue.templateId !== '') {
572
+ currentRedirect.urlTemplate(newValue.url);
573
+ }
574
+ },
575
+ owner: self,
576
+ deferEvaluation: true
577
+ });
578
+
579
+ this.menuDropdown = jQuery('#ame-rui-menu-items');
580
+
581
+ //Hide the dropdown when it loses focus.
582
+ this.menuDropdown.on('blur', () => {
583
+ this.closeMenuDropdown();
584
+ });
585
+
586
+ this.menuDropdown.on('keydown', (event) => {
587
+ //Also hide the dropdown if the user presses Esc.
588
+ if (event.which === 27) {
589
+ this.closeMenuDropdown(true);
590
+ } else if (event.which === 13) {
591
+ //Close the dropdown when the user presses Enter.
592
+ //Since we currently update the redirect on every change, there's no difference between
593
+ //this and pressing Esc.
594
+ this.closeMenuDropdown(true);
595
+ }
596
+ });
597
+
598
+ //Close the dropdown when the user selects an option by clicking it.
599
+ this.menuDropdown.on('click', 'option', () => {
600
+ this.closeMenuDropdown();
601
+ });
602
+
603
+ //this.addTestData();
604
+
605
+ this.byTrigger = ko.observable(RedirectsByTrigger.fromArray(this.redirects()));
606
+
607
+ //Reselect the previous trigger, or just the first trigger.
608
+ this.selectedTrigger = ko.observable(
609
+ settings.selectedTrigger ? settings.selectedTrigger : this.availableTriggers[0].trigger
610
+ );
611
+
612
+ this.currentTriggerView = ko.pureComputed(() => {
613
+ const trigger = this.selectedTrigger();
614
+ const mapping = this.byTrigger();
615
+
616
+ if (mapping.hasOwnProperty(trigger) && (mapping[trigger] instanceof TriggerView)) {
617
+ return mapping[trigger];
618
+ } else {
619
+ return mapping.login;
620
+ }
621
+ });
622
+
623
+ this.addableRoles = ko.pureComputed(() => {
624
+ const allRoles: IAmeActor[] = _.values(AmeActors.getRoles());
625
+ const usedRoles = _.map(
626
+ this.currentTriggerView().roles(),
627
+ (redirect) => {
628
+ return redirect.actor;
629
+ }
630
+ );
631
+ return _.difference(allRoles, usedRoles);
632
+ });
633
+
634
+ this.selectedRoleToAdd = ko.observable(void 0);
635
+ this.roleSelectorHasFocus = ko.observable(false);
636
+
637
+ this.addableUsers = ko.pureComputed(() => {
638
+ const usedUsers = _.map(
639
+ this.currentTriggerView().users(),
640
+ (redirect) => {
641
+ return redirect.actor as IAmeUser;
642
+ }
643
+ );
644
+ return _.difference(loadedUsers, usedUsers);
645
+ });
646
+
647
+ this.selectedUserToAdd = ko.observable(void 0);
648
+ this.userSelectorHasFocus = ko.observable(false);
649
+
650
+ this.selectedRoleToAdd.subscribe((newSelection) => {
651
+ this.addSelectedActorTo(newSelection, this.currentTriggerView().roles);
652
+ this.roleSelectorHasFocus(false);
653
+ this.selectedRoleToAdd(void 0);
654
+ });
655
+
656
+ this.selectedUserToAdd.subscribe((newSelection) => {
657
+ this.addSelectedActorTo(newSelection, this.currentTriggerView().users);
658
+ this.userSelectorHasFocus(false);
659
+ this.selectedUserToAdd(void 0);
660
+ });
661
+
662
+ this.userLoginQuery = ko.observable('');
663
+ this.addUserButtonEnabled = ko.pureComputed(() => {
664
+ return (this.userLoginQuery().trim() !== '');
665
+ });
666
+
667
+ if (settings.hasMoreUsers) {
668
+ this.userSelectionUi = 'search';
669
+ }
670
+
671
+ this.isSaving = ko.observable(false);
672
+ this.settingsData = ko.observable('');
673
+
674
+ this.isLoaded(true);
675
+ }
676
+
677
+ getSettings(): StorableSettings {
678
+ return {
679
+ redirects: this.byTrigger().toArray().map(redirect => redirect.toJs())
680
+ }
681
+ }
682
+
683
+ onDropdownTrigger(event: JQueryEventObject) {
684
+ //Note: There probably is some jQuery feature or library that makes dropdowns easier,
685
+ //but I already did this the hard way.
686
+
687
+ const $input = jQuery(event.target).closest('.ame-rui-url-template,ame-redirect-url-input').find('input').first();
688
+ const $node = $input.closest('.ame-rui-redirect');
689
+ if ($node.length < 1) {
690
+ return;
691
+ }
692
+
693
+ const redirect = ko.dataFor($node.get(0));
694
+ if (!(redirect instanceof AmeRedirectorUi.Redirect)) {
695
+ return;
696
+ }
697
+
698
+ //Clicking the same trigger a second time closes the dropdown.
699
+ if (event.type === 'mousedown') {
700
+ const isSameTrigger = this.menuDropdown.is(':visible') && (this.menuDropdownParent() === redirect);
701
+ if (isSameTrigger) {
702
+ //The dropdown will be automatically closed by its "blur" event handler,
703
+ //but we need to ignore the next click event on this element.
704
+ this.ignoreNextDropdownClick = event.target;
705
+ } else {
706
+ this.ignoreNextDropdownClick = null;
707
+ }
708
+ return;
709
+ }
710
+ if ((event.type === 'click') && (event.target === this.ignoreNextDropdownClick)) {
711
+ return;
712
+ }
713
+
714
+ //Move the drop-down near the input box.
715
+ this.menuDropdown
716
+ .css({
717
+ position: 'absolute',
718
+ zIndex: 100 //The dropdown should be displayed above other elements. This may not be required.
719
+ })
720
+ .show()
721
+ .outerWidth(Math.max($input.outerWidth(), 100))
722
+ .position({
723
+ my: 'right top',
724
+ at: 'right bottom',
725
+ of: $input
726
+ });
727
+
728
+ //Move focus to the dropdown.
729
+ let $select = this.menuDropdown;
730
+ if (!this.menuDropdown.is('select, input')) {
731
+ $select = this.menuDropdown.find('select, input').first();
732
+ }
733
+ $select.trigger('focus');
734
+
735
+ //Select the current option and scroll it into view. It looks like the browser will automatically
736
+ //scroll to the selected option, but only if the select element is already visible, so we need to
737
+ //do this *after* we show the dropdown.
738
+ this.menuDropdownParent(redirect);
739
+ }
740
+
741
+ closeMenuDropdown(moveFocusToInput: boolean = false) {
742
+ const currentRedirect = this.menuDropdownParent();
743
+
744
+ this.menuDropdown.hide();
745
+ this.menuDropdownParent(null);
746
+
747
+ //Refocus on the URL input after closing the dropdown.
748
+ if (moveFocusToInput && currentRedirect) {
749
+ currentRedirect.inputHasFocus(true);
750
+ }
751
+ }
752
+
753
+ protected addSelectedActorTo(actor: IAmeActor | undefined, list: KnockoutObservableArray<Redirect>) {
754
+ //The list includes a caption item that is displayed when nothing is selected.
755
+ //The value of that option is supposed to be undefined.
756
+ if ((typeof actor === 'undefined') || (actor === null) || !this.currentTriggerView()) {
757
+ return;
758
+ }
759
+
760
+ //Add a redirect for the selected role.
761
+ let newRedirect = new Redirect({
762
+ actorId: actor.getId(),
763
+ shortcodesEnabled: true,
764
+ urlTemplate: '',
765
+ trigger: this.selectedTrigger()
766
+ }, this.actorProvider);
767
+
768
+ list.push(newRedirect);
769
+
770
+ newRedirect.inputHasFocus(true);
771
+ }
772
+
773
+ addEnteredUserLogin() {
774
+ const userLogin = this.userLoginQuery().trim();
775
+ if (userLogin === '') {
776
+ return;
777
+ }
778
+
779
+ const actorId = 'user:' + userLogin;
780
+ if (!AmeActors.actorExists(actorId)) {
781
+ if (console && console.warn) {
782
+ console.warn('User "' + userLogin + '" has not been initialized. Creating a minimal actor now.');
783
+ }
784
+ AmeActors.addUsers([
785
+ MinimalUser.createFromProperties({
786
+ user_login: userLogin,
787
+ display_name: userLogin
788
+ })
789
+ ]);
790
+ }
791
+
792
+ //Only add each user once.
793
+ const alreadyAdded = _.some(this.currentTriggerView().users(), function (redirect) {
794
+ return redirect.actorId === actorId;
795
+ });
796
+ if (alreadyAdded) {
797
+ alert('Error: Duplicate entry. User "' + userLogin + '" has already been added.');
798
+ return;
799
+ }
800
+
801
+ let newRedirect = new Redirect({
802
+ actorId: actorId,
803
+ shortcodesEnabled: true,
804
+ urlTemplate: '',
805
+ trigger: this.selectedTrigger()
806
+ }, this.actorProvider);
807
+
808
+ this.currentTriggerView().users.push(newRedirect);
809
+
810
+ this.userLoginQuery('');
811
+ }
812
+
813
+ filterUserAutocompleteResults(results: MinimalUserProperties[]): MinimalUserProperties[] {
814
+ //Filter out users that are already in the current list.
815
+ const usedLogins = _.indexBy(
816
+ this.currentTriggerView().users(),
817
+ (redirect) => {
818
+ return (redirect.actor as IAmeUser).userLogin;
819
+ }
820
+ );
821
+ return _.filter(results, function (props) {
822
+ return !(usedLogins.hasOwnProperty(props.user_login));
823
+ });
824
+ }
825
+
826
+ isMissingActor(actor: IAmeActor): boolean {
827
+ return (actor instanceof MissingActorPlaceholder);
828
+ }
829
+
830
+ saveChanges() {
831
+ this.isSaving(true);
832
+ this.settingsData(ko.toJSON(this.getSettings()));
833
+ return true;
834
+ }
835
+
836
+ private addTestData() {
837
+ //Add some test data.
838
+ this.redirects.push(new Redirect({
839
+ actorId: 'role:editor',
840
+ urlTemplate: '[wp-admin]edit.php',
841
+ trigger: 'login',
842
+ shortcodesEnabled: true
843
+ }, this.actorProvider));
844
+
845
+ this.redirects.push(new Redirect({
846
+ actorId: 'role:author',
847
+ urlTemplate: '[wp-admin]profile.php',
848
+ trigger: 'login',
849
+ shortcodesEnabled: true
850
+ }, this.actorProvider));
851
+
852
+ this.redirects.push(new Redirect({
853
+ actorId: 'user:admin',
854
+ urlTemplate: '[wp-admin]index.php',
855
+ trigger: 'login',
856
+ shortcodesEnabled: true
857
+ }, this.actorProvider));
858
+
859
+ this.redirects.push(new Redirect({
860
+ actorId: 'role:contributor',
861
+ urlTemplate: '[wp-admin]index.php',
862
+ trigger: 'login',
863
+ shortcodesEnabled: true
864
+ }, this.actorProvider));
865
+
866
+ this.redirects.push(new Redirect({
867
+ actorId: 'role:nonexistent',
868
+ urlTemplate: '[wp-admin]options-general.php',
869
+ trigger: 'login',
870
+ shortcodesEnabled: true
871
+ }, this.actorProvider));
872
+
873
+ this.redirects.push(new Redirect({
874
+ actorId: 'user:notarealuser',
875
+ urlTemplate: '[wp-admin]index.php',
876
+ trigger: 'login',
877
+ shortcodesEnabled: true
878
+ }, this.actorProvider));
879
+
880
+ this.redirects.push(new Redirect({
881
+ actorId: DefaultActorId,
882
+ urlTemplate: '[wp-admin]index.php?this-is-the-default=yep',
883
+ trigger: 'login',
884
+ shortcodesEnabled: true
885
+ }, this.actorProvider));
886
+
887
+ this.redirects.push(new Redirect({
888
+ actorId: 'role:administrator',
889
+ urlTemplate: '[wp-admin]options-general.php',
890
+ trigger: 'login',
891
+ shortcodesEnabled: true
892
+ }, this.actorProvider));
893
+ }
894
+ }
895
+ }
896
+
897
+ jQuery(function ($) {
898
+ ko.components.register(
899
+ 'ame-redirect-url-input',
900
+ {
901
+ viewModel: AmeRedirectorUi.RedirectUrlInputComponent,
902
+ template: {element: 'ame-redirect-url-component'}
903
+ }
904
+ );
905
+
906
+ //The user autocomplete feature is implemented as a custom binding only because that makes it easier
907
+ //to correctly initialise it when Knockout changes the DOM. The binding is not intended to be reusable.
908
+ ko.bindingHandlers.ameRuiUserAutocomplete = {
909
+ init: function (element, valueAccessor) {
910
+ let options = ko.unwrap(valueAccessor());
911
+
912
+ options = wsAmeLodash.defaults(options, {
913
+ filter: function (suggestions) {
914
+ return suggestions;
915
+ }
916
+ });
917
+
918
+ jQuery(element).autocomplete({
919
+ minLength: 2,
920
+ source: function (request, response) {
921
+ const action = AjawV1.getAction('ws-ame-rui-search-users');
922
+ action.get(
923
+ {term: request.term},
924
+ function (results) {
925
+ //Filter received users.
926
+ if (options.filter) {
927
+ results = options.filter(results);
928
+ }
929
+ response(results)
930
+ },
931
+ function (error) {
932
+ response([]);
933
+ if (console && console.error) {
934
+ console.error(error);
935
+ }
936
+ }
937
+ );
938
+ },
939
+ select: function (unusedEvent, ui) {
940
+ const props = ui.item as AmeRedirectorUi.MinimalUserProperties;
941
+ const existingUser = AmeActors.getUser(props.user_login);
942
+ if (existingUser === null) {
943
+ AmeActors.addUsers([AmeRedirectorUi.MinimalUser.createFromProperties(props)]);
944
+ }
945
+ },
946
+ classes: {
947
+ 'ui-autocomplete': 'ame-rui-found-users'
948
+ }
949
+ });
950
+
951
+ ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
952
+ jQuery(element).autocomplete('destroy');
953
+ });
954
+ }
955
+ };
956
+
957
+ const $container = $('#ame-redirector-ui-root');
958
+
959
+ const ameRedirectorApp = new AmeRedirectorUi.App(wsAmeRedirectorSettings);
960
+ ko.applyBindings(ameRedirectorApp, $container.get(0));
961
+
962
+ //Open the menu dropdown when the user clicks the trigger icon or presses
963
+ //the down arrow key in the redirect input field.
964
+ $container.on('mousedown click', '.ame-rui-url-dropdown-trigger', function (event) {
965
+ ameRedirectorApp.onDropdownTrigger(event);
966
+ });
967
+
968
+ /*
969
+ Releasing the "down" key only opens the dropdown if the key was pressed in the same input.
970
+ This is to avoid a confusing situation where the user selects a role from the "add a role"
971
+ dropdown using arrow keys and then the menu dropdown immediately shows up because the focus
972
+ moved to the redirect input before the user could release the key.
973
+ */
974
+ const redirectInputSelector = '.ame-rui-url-template input[type=text].ame-rui-has-url-dropdown';
975
+ let lastDownArrowTarget = null;
976
+ $container.on('focus', redirectInputSelector, function () {
977
+ lastDownArrowTarget = null;
978
+ });
979
+ $container.on('keydown', redirectInputSelector, function (event) {
980
+ //Ignore repeated "keydown" events. These will happen even if the key was originally
981
+ //pressed in a different element.
982
+ if ((typeof event.originalEvent['repeat'] !== 'undefined') && (event.originalEvent['repeat'] === true)) {
983
+ return;
984
+ }
985
+ if (event.which === 40) {
986
+ lastDownArrowTarget = event.target;
987
+ }
988
+ });
989
+ $container.on('keyup', redirectInputSelector, function (event) {
990
+ if ((event.which === 40) && (event.target === lastDownArrowTarget)) {
991
+ ameRedirectorApp.onDropdownTrigger(event);
992
+ }
993
+ });
994
+ });
modules/redirector/redirector.css ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @charset "UTF-8";
2
+ .ame-rui-redirect-list {
3
+ display: flex;
4
+ flex-direction: column;
5
+ padding-top: 1px;
6
+ }
7
+
8
+ .ame-rui-redirect {
9
+ display: flex;
10
+ flex-direction: row;
11
+ background: #fff;
12
+ border: 1px solid #dcdcde;
13
+ margin-top: -1px;
14
+ }
15
+ .ame-rui-redirect.ui-sortable-helper {
16
+ border: 1px solid #8c8f94;
17
+ box-shadow: 1px 1px 5px 0 rgba(0, 0, 0, 0.3);
18
+ }
19
+
20
+ .ame-rui-redirect-content {
21
+ flex-grow: 1;
22
+ display: flex;
23
+ flex-direction: row;
24
+ padding: 10px 10px 10px 0;
25
+ }
26
+
27
+ .ame-rui-actor {
28
+ margin-right: 10px;
29
+ font-size: 14px;
30
+ line-height: 27px;
31
+ }
32
+
33
+ .ame-rui-url-template, ame-redirect-url-input {
34
+ flex-grow: 1;
35
+ display: block;
36
+ margin-right: 10px;
37
+ position: relative;
38
+ }
39
+ .ame-rui-url-template input[type=text], ame-redirect-url-input input[type=text] {
40
+ width: 100%;
41
+ }
42
+ .ame-rui-url-template input[type=text].ame-rui-has-url-dropdown, ame-redirect-url-input input[type=text].ame-rui-has-url-dropdown {
43
+ padding-right: 32px;
44
+ }
45
+ .ame-rui-url-template input[readonly], ame-redirect-url-input input[readonly] {
46
+ background-color: #dbeeff;
47
+ }
48
+
49
+ .ame-rui-url-dropdown-trigger {
50
+ display: block;
51
+ position: absolute;
52
+ width: 30px;
53
+ height: 28px;
54
+ top: 1px;
55
+ right: 1px;
56
+ border-top-right-radius: 4px;
57
+ border-bottom-right-radius: 4px;
58
+ cursor: pointer;
59
+ text-align: center;
60
+ color: #787c82;
61
+ }
62
+ .ame-rui-url-dropdown-trigger:hover {
63
+ color: #1d2327;
64
+ }
65
+ .ame-rui-url-dropdown-trigger .am-rui-trigger-icon:before {
66
+ content: "";
67
+ font: normal 20px dashicons;
68
+ line-height: 28px;
69
+ vertical-align: middle;
70
+ }
71
+
72
+ .ame-rui-shortcodes-enabled {
73
+ margin: 0 10px 0 0;
74
+ display: inline-block;
75
+ box-sizing: border-box;
76
+ min-height: 30px;
77
+ padding: 0 10px;
78
+ line-height: 28px;
79
+ border: 1px solid #dcdcde;
80
+ border-radius: 3px;
81
+ background: #f6f7f7;
82
+ }
83
+ .ame-rui-shortcodes-enabled .dashicons {
84
+ vertical-align: top;
85
+ line-height: 28px;
86
+ }
87
+ .ame-rui-shortcodes-enabled:hover, .ame-rui-shortcodes-enabled:active {
88
+ background: #f0f0f1;
89
+ border-color: #0a4b78;
90
+ }
91
+
92
+ .ame-rui-drag-handle {
93
+ cursor: grab;
94
+ min-width: 30px;
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ }
99
+ .ui-sortable-helper > .ame-rui-drag-handle {
100
+ cursor: grabbing;
101
+ }
102
+
103
+ .ame-rui-drag-icon {
104
+ fill: #1e1e1e;
105
+ }
106
+
107
+ #ame-rui-menu-items {
108
+ background-image: none;
109
+ max-width: 40rem;
110
+ padding-right: 8px;
111
+ box-shadow: 3px 3px 4px -2px rgba(0, 0, 0, 0.5);
112
+ }
113
+ #ame-rui-menu-items:hover, #ame-rui-menu-items:focus {
114
+ color: unset;
115
+ }
116
+ #ame-rui-menu-items option:hover {
117
+ background-color: #f0f0f5;
118
+ }
119
+
120
+ .ame-rui-default-redirect-container {
121
+ max-width: 768px;
122
+ }
123
+ .ame-rui-default-redirect-container .ame-rui-redirect-content {
124
+ padding-left: 10px;
125
+ }
126
+
127
+ .ame-rui-add-actor-dropdown, #ame-rui-user-search-query {
128
+ min-width: 15em;
129
+ }
130
+
131
+ .ame-rui-found-users li.ui-menu-item {
132
+ padding: 0;
133
+ }
134
+ .ame-rui-found-users .ui-menu-item-wrapper {
135
+ padding: 4px 10px 6px;
136
+ }
137
+ .ame-rui-found-users .ui-state-active {
138
+ background-color: #2271b1;
139
+ color: #fff;
140
+ }
141
+
142
+ .ame-rui-missing-actor-indicator {
143
+ vertical-align: middle;
144
+ font-style: italic;
145
+ }
146
+
147
+ .ame-rui-trigger-selector {
148
+ display: inline-block;
149
+ margin-bottom: 0;
150
+ list-style: none;
151
+ font-size: 14px;
152
+ }
153
+ .ame-rui-trigger-selector li {
154
+ display: inline-block;
155
+ margin: 0;
156
+ }
157
+ .ame-rui-trigger-selector li a {
158
+ text-decoration: none;
159
+ transition: none;
160
+ }
161
+
162
+ .ame-rui-small-tabs {
163
+ display: inline-block;
164
+ padding-left: 7px;
165
+ border-bottom: 1px solid #c3c4c7;
166
+ }
167
+ .ame-rui-small-tabs li {
168
+ display: inline-block;
169
+ margin: 0;
170
+ background: #e3e3e5;
171
+ border: 1px solid #c3c4c7;
172
+ border-bottom: none;
173
+ }
174
+ .ame-rui-small-tabs li a {
175
+ display: inline-block;
176
+ transition: none;
177
+ min-width: 7em;
178
+ font-size: 14px;
179
+ padding: 5px 8px 6px 8px;
180
+ cursor: pointer;
181
+ text-decoration: none;
182
+ color: #444;
183
+ }
184
+ .ame-rui-small-tabs li a:hover {
185
+ color: #2271b1;
186
+ }
187
+ .ame-rui-small-tabs .ame-rui-active-tab {
188
+ background: transparent;
189
+ border-bottom: 1px solid #f0f0f1;
190
+ margin-bottom: -1px;
191
+ }
192
+ .ame-rui-small-tabs .ame-rui-active-tab a {
193
+ color: #000;
194
+ }
195
+ .ame-rui-small-tabs .ame-rui-active-tab a:hover {
196
+ color: #000;
197
+ }
198
+
199
+ .ame-rui-filter-like-tabs {
200
+ display: inline-flex;
201
+ margin: 13px 0 0 0;
202
+ list-style: none;
203
+ border: 1px solid #c3c4c7;
204
+ background: #fff;
205
+ }
206
+ .ame-rui-filter-like-tabs li {
207
+ display: inline-block;
208
+ margin: 0;
209
+ padding: 0;
210
+ border-style: none;
211
+ }
212
+ .ame-rui-filter-like-tabs li a {
213
+ display: inline-block;
214
+ margin: 0;
215
+ padding: 10px 14px;
216
+ min-width: 7em;
217
+ border: none;
218
+ border-bottom: 4px solid transparent;
219
+ color: #646970;
220
+ text-decoration: none;
221
+ cursor: pointer;
222
+ }
223
+ .ame-rui-filter-like-tabs li a:hover {
224
+ color: #135e96;
225
+ border-bottom-color: transparent;
226
+ }
227
+ .ame-rui-filter-like-tabs .ame-rui-active-tab a {
228
+ border-bottom-color: #007cba;
229
+ }
230
+ .ame-rui-filter-like-tabs .ame-rui-active-tab a:hover {
231
+ color: #646970;
232
+ border-bottom-color: #007cba;
233
+ }
234
+
235
+ .ame-rui-sub-tabs {
236
+ margin-top: 6px;
237
+ font-size: 13px;
238
+ }
239
+ .ame-rui-sub-tabs li::after {
240
+ content: "|";
241
+ }
242
+ .ame-rui-sub-tabs li:last-child::after {
243
+ content: "";
244
+ }
245
+ .ame-rui-sub-tabs li a {
246
+ display: inline-block;
247
+ text-align: center;
248
+ padding: 0.2em;
249
+ line-height: 2;
250
+ cursor: pointer;
251
+ }
252
+ .ame-rui-sub-tabs li a::before {
253
+ display: block;
254
+ content: attr(data-text);
255
+ font-weight: bold;
256
+ height: 1px;
257
+ overflow: hidden;
258
+ visibility: hidden;
259
+ margin-bottom: -1px;
260
+ }
261
+ .ame-rui-sub-tabs li:first-child a {
262
+ padding-left: 0;
263
+ text-align: left;
264
+ }
265
+ .ame-rui-sub-tabs .ame-rui-active-tab a {
266
+ font-weight: 600;
267
+ color: #000;
268
+ }
269
+
270
+ .ame-rui-actions button .dashicons {
271
+ vertical-align: inherit;
272
+ line-height: 28px;
273
+ }
274
+
275
+ #ame-rui-column-container {
276
+ display: flex;
277
+ flex-direction: row;
278
+ }
279
+
280
+ #ame-rui-main-section {
281
+ display: block;
282
+ flex-grow: 1;
283
+ max-width: 1320px;
284
+ }
285
+
286
+ #ame-rui-sidebar {
287
+ display: none;
288
+ width: 300px;
289
+ flex-shrink: 0;
290
+ flex-grow: 0;
291
+ align-self: flex-start;
292
+ margin-left: 20px;
293
+ }
294
+
295
+ #ame-rui-main-actions {
296
+ position: relative;
297
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
298
+ background: #fff;
299
+ margin-bottom: 20px;
300
+ margin-bottom: 0;
301
+ padding: 10px 8px;
302
+ border: 1px solid #ccd0d4;
303
+ }
304
+ #ame-rui-main-actions .ws-ame-postbox-header {
305
+ position: relative;
306
+ font-size: 14px;
307
+ margin: 0;
308
+ line-height: 1.4;
309
+ border: 1px solid #ccd0d4;
310
+ }
311
+ #ame-rui-main-actions .ws-ame-postbox-header h3 {
312
+ padding: 10px 12px;
313
+ margin: 0;
314
+ font-size: 1em;
315
+ line-height: 1;
316
+ white-space: nowrap;
317
+ text-overflow: ellipsis;
318
+ overflow: hidden;
319
+ }
320
+ #ame-rui-main-actions .ws-ame-postbox-toggle {
321
+ color: #72777c;
322
+ background: #fff;
323
+ display: block;
324
+ font: normal 20px/1 dashicons;
325
+ text-align: center;
326
+ cursor: pointer;
327
+ border: none;
328
+ position: absolute;
329
+ top: 0;
330
+ right: 0;
331
+ bottom: 0;
332
+ width: 36px;
333
+ height: 100%;
334
+ padding: 0;
335
+ }
336
+ #ame-rui-main-actions .ws-ame-postbox-toggle:hover {
337
+ color: #23282d;
338
+ }
339
+ #ame-rui-main-actions .ws-ame-postbox-toggle:active, #ame-rui-main-actions .ws-ame-postbox-toggle:focus {
340
+ outline: none;
341
+ padding: 0;
342
+ }
343
+ #ame-rui-main-actions .ws-ame-postbox-toggle:before {
344
+ content: "";
345
+ display: inline-block;
346
+ vertical-align: middle;
347
+ }
348
+ #ame-rui-main-actions .ws-ame-postbox-toggle:after {
349
+ display: inline-block;
350
+ content: "";
351
+ vertical-align: middle;
352
+ height: 100%;
353
+ }
354
+ #ame-rui-main-actions .ws-ame-postbox-content {
355
+ border: 1px solid #ccd0d4;
356
+ border-top: none;
357
+ padding: 12px;
358
+ }
359
+ #ame-rui-main-actions.ws-ame-closed-postbox .ws-ame-postbox-content {
360
+ display: none;
361
+ }
362
+ #ame-rui-main-actions.ws-ame-closed-postbox .ws-ame-postbox-toggle:before {
363
+ content: "";
364
+ }
365
+ #ame-rui-main-actions input.button {
366
+ max-width: 150px;
367
+ width: 100%;
368
+ }
369
+
370
+ @media only screen and (min-width: 961px) {
371
+ .ame-rui-actor {
372
+ min-width: 12em;
373
+ }
374
+
375
+ .ame-rui-actions button .dashicons {
376
+ display: none;
377
+ }
378
+ }
379
+ @media only screen and (max-width: 960px) {
380
+ .ame-rui-actor {
381
+ min-width: 10em;
382
+ }
383
+
384
+ .ame-rui-shortcodes-enabled .ame-rui-button-label, .ame-rui-actions .ame-rui-button-label {
385
+ display: none;
386
+ }
387
+ }
388
+ @media only screen and (max-width: 782px) {
389
+ .ame-rui-actor {
390
+ line-height: 38px;
391
+ }
392
+
393
+ .ame-rui-redirect .ame-rui-actions button {
394
+ margin-bottom: 0;
395
+ }
396
+ .ame-rui-redirect .ame-rui-actions button .dashicons {
397
+ line-height: 38px;
398
+ vertical-align: top;
399
+ }
400
+
401
+ .ame-rui-shortcodes-enabled {
402
+ line-height: 38px;
403
+ }
404
+ .ame-rui-shortcodes-enabled .dashicons {
405
+ vertical-align: top;
406
+ line-height: 38px;
407
+ }
408
+ }
409
+
410
+ /*# sourceMappingURL=redirector.css.map */
modules/redirector/redirector.php ADDED
@@ -0,0 +1,1027 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace YahnisElsts\AdminMenuEditor\Redirects;
4
+
5
+ use ameMenuItem;
6
+ use amePersistentModule;
7
+ use ameRoleUtils;
8
+ use ameUtils;
9
+ use DateTime;
10
+ use DateTimeZone;
11
+ use Exception;
12
+ use RuntimeException;
13
+ use WP_Error;
14
+ use WP_User;
15
+
16
+ class Module extends amePersistentModule {
17
+ const FILTER_PRIORITY = 1000000;
18
+ const UI_SCRIPT_HANDLE = 'ame-redirector-ui';
19
+
20
+ const FIRST_LOGIN_AGE_LIMIT_IN_DAYS = 14;
21
+ const FIRST_LOGIN_META_KEY = 'ame_rui_first_login_done';
22
+
23
+ const SETTINGS_INIT_TIME_KEY = 'ws_ame_rui_first_change';
24
+
25
+ const PRELOADED_USER_LIMIT = 50;
26
+ const SEARCH_USER_LIMIT = 30;
27
+ protected static $desiredUserFields = array('ID', 'display_name', 'user_login');
28
+
29
+ protected $tabSlug = 'redirects';
30
+ protected $tabTitle = 'Redirects';
31
+ protected $optionName = 'ws_ame_redirects';
32
+
33
+ protected $settingsFormAction = 'ame-save-redirect-settings';
34
+
35
+ /**
36
+ * @var RedirectCollection|null
37
+ */
38
+ protected $redirects = null;
39
+
40
+ protected $searchUsersAction;
41
+
42
+ public function __construct($menuEditor) {
43
+ parent::__construct($menuEditor);
44
+
45
+ if ( !$this->isEnabledForRequest() ) {
46
+ return;
47
+ }
48
+
49
+ //Let the user disable all redirects in wp-config.php.
50
+ $allRedirectsDisabled = defined('AME_DISABLE_REDIRECTS') && constant('AME_DISABLE_REDIRECTS');
51
+ if ( !$allRedirectsDisabled ) {
52
+ //Login redirect.
53
+ add_filter('login_redirect', [$this, 'filterLoginRedirect'], self::FILTER_PRIORITY, 3);
54
+ //Logout redirect. We might need to also use the "wp_logout" action if something bypasses wp-login.php.
55
+ add_filter('logout_redirect', [$this, 'filterLogoutRedirect'], self::FILTER_PRIORITY, 3);
56
+ //Registration redirect. This happens after the user is created but before the user logs in.
57
+ add_filter('registration_redirect', [$this, 'filterRegistrationRedirect'], self::FILTER_PRIORITY, 1);
58
+ }
59
+
60
+ if ( is_admin() ) {
61
+ $this->searchUsersAction = ajaw_v1_CreateAction('ws-ame-rui-search-users')
62
+ ->requiredParam('term')
63
+ ->method('get')
64
+ ->permissionCallback(array($this, 'userCanSearchUsers'))
65
+ ->handler(array($this, 'ajaxSearchUsers'))
66
+ ->register();
67
+
68
+ add_action('admin_menu_editor-load_tab-' . $this->tabSlug, [$this, 'addContextualHelp']);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Get the redirect that best matches the given trigger and user.
74
+ *
75
+ * When there are multiple redirects that could apply in this context, only the redirect
76
+ * with the highest priority will be returned.
77
+ *
78
+ * @param string $trigger
79
+ * @param WP_User $user
80
+ * @return Option<Redirect>
81
+ */
82
+ protected function getBestRedirectFor($trigger, WP_User $user) {
83
+ $redirectsForTrigger = $this->getRedirects()->filterByTrigger($trigger);
84
+ if ( empty($redirectsForTrigger) ) {
85
+ return None::getInstance();
86
+ }
87
+ $actors = $this->getUserActors($user);
88
+
89
+ //Redirects should already be sorted by priority, so we can just return the first match.
90
+ foreach ($redirectsForTrigger as $redirect) {
91
+ if ( array_key_exists($redirect->getActorId(), $actors) ) {
92
+ return new Some($redirect);
93
+ }
94
+ }
95
+ return None::getInstance();
96
+ }
97
+
98
+ /**
99
+ * @param WP_User|null $user
100
+ * @return array<string,bool>
101
+ */
102
+ protected function getUserActors(WP_User $user) {
103
+ $actorIds = ['special:default' => true]; //The "default" setting applies to every user.
104
+
105
+ if ( !isset($user) ) {
106
+ return $actorIds;
107
+ }
108
+
109
+ if ( isset($user->user_login) ) {
110
+ $actorIds['user:' . $user->user_login] = true;
111
+ }
112
+
113
+ if ( isset($user->roles) && is_array($user->roles) ) {
114
+ foreach ($user->roles as $roleId) {
115
+ $actorIds['role:' . $roleId] = true;
116
+ }
117
+ }
118
+
119
+ if ( is_multisite() && is_super_admin($user) ) {
120
+ $actorIds['special:super_admin'] = true;
121
+ }
122
+
123
+ return $actorIds;
124
+ }
125
+
126
+ /**
127
+ * @return RedirectCollection
128
+ */
129
+ protected function getRedirects() {
130
+ if ( $this->redirects !== null ) {
131
+ return $this->redirects;
132
+ }
133
+
134
+ $settings = $this->loadSettings();
135
+ if ( isset($settings['redirects']) ) {
136
+ $this->redirects = RedirectCollection::fromDbFormat($settings['redirects']);
137
+ } else {
138
+ $this->redirects = new RedirectCollection();
139
+ }
140
+
141
+ return $this->redirects;
142
+ }
143
+
144
+ public function saveSettings() {
145
+ if ( isset($this->redirects) ) {
146
+ $this->loadSettings();
147
+ $this->settings['redirects'] = $this->redirects->toDbFormat();
148
+ }
149
+ parent::saveSettings();
150
+ }
151
+
152
+ /**
153
+ * @param string $redirectTo
154
+ * @param string $requestedRedirectTo
155
+ * @param WP_User|WP_Error $user
156
+ * @return string
157
+ * @noinspection PhpUnusedParameterInspection The parameters are defined by the hook and can't be changed.
158
+ */
159
+ public function filterLoginRedirect($redirectTo, $requestedRedirectTo, $user = null) {
160
+ if ( $this->checkFirstLogin($user) ) {
161
+ $trigger = Triggers::FIRST_LOGIN;
162
+ } else {
163
+ $trigger = Triggers::LOGIN;
164
+ }
165
+ return $this->filterRedirect($trigger, $redirectTo, $requestedRedirectTo, $user);
166
+ }
167
+
168
+ public function filterLogoutRedirect($redirectTo, $requestedRedirectTo, $user = null) {
169
+ return $this->filterRedirect(Triggers::LOGOUT, $redirectTo, $requestedRedirectTo, $user);
170
+ }
171
+
172
+ public function filterRegistrationRedirect($redirectTo) {
173
+ //Note that this does not depend on the user's role as the user isn't logged in yet.
174
+ return $this->filterRedirect(Triggers::REGISTRATION, $redirectTo, '');
175
+ }
176
+
177
+ /**
178
+ * @param string $trigger
179
+ * @param string $redirectTo
180
+ * @param string $requestedRedirectTo
181
+ * @param WP_User|WP_Error $user
182
+ * @return string
183
+ * @noinspection PhpUnusedParameterInspection The requested URL is unused right now, but might be useful in the future.
184
+ */
185
+ protected function filterRedirect($trigger, $redirectTo, $requestedRedirectTo, $user = null) {
186
+ if ( !($user instanceof WP_User) ) {
187
+ return $redirectTo;
188
+ }
189
+
190
+ $found = $this->getBestRedirectFor($trigger, $user);
191
+ if ( $found->nonEmpty() ) {
192
+ /** @var Redirect $customRedirect */
193
+ $customRedirect = $found->get();
194
+ $url = $customRedirect->getUrl();
195
+
196
+ //WordPress uses wp_safe_redirect() for login, logout, and registration redirects, which
197
+ //only allows local redirects by default. Let's temporarily add the domain name of the URL
198
+ //to the allowed host list to let the user set any redirect URL they want.
199
+ $redirectHost = parse_url($url, PHP_URL_HOST);
200
+ if ( !empty($redirectHost) ) {
201
+ add_filter('allowed_redirect_hosts', function ($allowedHosts) use ($redirectHost) {
202
+ $allowedHosts[] = $redirectHost;
203
+ return $allowedHosts;
204
+ });
205
+ }
206
+
207
+ return $url;
208
+ } else {
209
+ return $redirectTo;
210
+ }
211
+ }
212
+
213
+ /**
214
+ * @param WP_User|null $user
215
+ * @return bool
216
+ */
217
+ private function checkFirstLogin($user) {
218
+ if ( !($user instanceof WP_User) ) {
219
+ return false;
220
+ }
221
+
222
+ /* WordPress doesn't record logins by default, so we use a few checks to help ensure that
223
+ * this redirect will only happen when a new user logs in for the first time:
224
+ *
225
+ * - Account doesn't have the custom "first login done" flag.
226
+ * - Account is less than X days old.
227
+ * - Account was created after the admin changed redirect settings for the first time.
228
+ */
229
+
230
+ //Check the first login flag.
231
+ $isFirstLoginDone = !empty(get_user_meta($user->ID, self::FIRST_LOGIN_META_KEY, true));
232
+ if ( $isFirstLoginDone ) {
233
+ return false;
234
+ }
235
+ //This may or may not be the first login, but any future logins definitely won't be first.
236
+ update_user_meta($user->ID, self::FIRST_LOGIN_META_KEY, 1);
237
+
238
+ //Account age.
239
+ //Handle invalid timestamps by acting as if the user was registered just now.
240
+ $registrationTime = $this->getRegistrationTimestamp($user, time());
241
+ $accountAgeInDays = (time() - $registrationTime) / (24 * 3600);
242
+ if ( $accountAgeInDays > self::FIRST_LOGIN_AGE_LIMIT_IN_DAYS ) {
243
+ return false;
244
+ }
245
+
246
+ //Account created after using the "redirects" feature, not before.
247
+ if ( $registrationTime <= $this->getFirstSettingsActivityTime() ) {
248
+ return false;
249
+ }
250
+ return true;
251
+ }
252
+
253
+ /**
254
+ * @param WP_User $user
255
+ * @param int $default
256
+ */
257
+ private function getRegistrationTimestamp($user, $default) {
258
+ if ( !isset($user, $user->user_registered) ) {
259
+ return $default;
260
+ }
261
+
262
+ try {
263
+ $dateTime = new DateTime($user->user_registered, new DateTimeZone('UTC'));
264
+ return $dateTime->getTimestamp();
265
+ } catch (Exception $e) {
266
+ return $default;
267
+ }
268
+ }
269
+
270
+ /**
271
+ * @return int
272
+ */
273
+ private function getFirstSettingsActivityTime() {
274
+ $activityTimestamp = get_site_option(self::SETTINGS_INIT_TIME_KEY, 0);
275
+ if ( is_numeric($activityTimestamp) ) {
276
+ return intval($activityTimestamp);
277
+ } else {
278
+ return 0;
279
+ }
280
+ }
281
+
282
+ public function registerScripts() {
283
+ parent::registerScripts();
284
+
285
+ wp_register_auto_versioned_script(
286
+ 'ame-knockout-sortable',
287
+ plugins_url('knockout-sortable.js', __FILE__),
288
+ ['knockout', 'jquery', 'jquery-ui-sortable', 'jquery-ui-draggable', 'jquery-ui-droppable']
289
+ );
290
+ }
291
+
292
+ public function enqueueTabScripts() {
293
+ parent::enqueueTabScripts();
294
+
295
+ wp_enqueue_auto_versioned_script(
296
+ self::UI_SCRIPT_HANDLE,
297
+ plugins_url('redirector-ui.js', __FILE__),
298
+ [
299
+ 'jquery',
300
+ 'jquery-ui-position',
301
+ 'jquery-ui-autocomplete',
302
+ 'knockout',
303
+ 'ame-actor-selector',
304
+ 'ame-actor-manager',
305
+ 'ame-knockout-sortable',
306
+ 'ame-lodash',
307
+ $this->searchUsersAction->getScriptHandle(),
308
+ ]
309
+ );
310
+
311
+ $flattenedRedirects = $this->getRedirects()->flatten();
312
+
313
+ $usableMenuItems = [];
314
+ $adminMenu = $this->menuEditor->get_active_admin_menu();
315
+ if ( !empty($adminMenu['tree']) ) {
316
+ $extractor = new MenuExtractor($adminMenu['tree']);
317
+ $usableMenuItems = $extractor->getUsableItems();
318
+ }
319
+
320
+ $wpRoles = ameRoleUtils::get_roles();
321
+ $roles = [];
322
+ foreach ($wpRoles->role_objects as $roleId => $role) {
323
+ $roles[] = [
324
+ 'name' => $roleId,
325
+ 'displayName' => ameUtils::get($wpRoles->role_names, $roleId, $roleId),
326
+ ];
327
+ }
328
+
329
+ list($loadedUsers, $hasMoreUsers) = $this->preloadUsers($flattenedRedirects);
330
+
331
+ $scriptData = [
332
+ 'redirects' => $flattenedRedirects,
333
+ 'usableMenuItems' => $usableMenuItems,
334
+ 'roles' => $roles,
335
+ 'users' => $loadedUsers,
336
+ 'hasMoreUsers' => $hasMoreUsers,
337
+ ];
338
+
339
+ if ( isset($_GET['selectedTrigger']) && in_array($_GET['selectedTrigger'], Triggers::getValues()) ) {
340
+ $scriptData['selectedTrigger'] = $_GET['selectedTrigger'];
341
+ }
342
+
343
+ wp_add_inline_script(
344
+ self::UI_SCRIPT_HANDLE,
345
+ sprintf('wsAmeRedirectorSettings = (%s);', wp_json_encode($scriptData)),
346
+ 'before'
347
+ );
348
+ }
349
+
350
+ public function enqueueTabStyles() {
351
+ parent::enqueueTabStyles();
352
+
353
+ wp_enqueue_auto_versioned_style(
354
+ 'ame-redirector-ui-css',
355
+ plugins_url('redirector.css', __FILE__)
356
+ );
357
+ }
358
+
359
+ public function handleSettingsForm($post = array()) {
360
+ parent::handleSettingsForm($post);
361
+
362
+ $submittedSettings = json_decode($post['settings'], true);
363
+ $validationResult = $this->validateSubmittedSettings($submittedSettings);
364
+
365
+ if ( is_wp_error($validationResult) ) {
366
+ //It seems that wp_die() doesn't automatically escape special characters, so let's do that.
367
+ $message = esc_html($validationResult->get_error_message());
368
+ wp_die($message);
369
+ }
370
+
371
+ $newRedirects = new RedirectCollection();
372
+ foreach ($submittedSettings['redirects'] as $redirect) {
373
+ $newRedirects->add($redirect);
374
+ }
375
+ $this->redirects = $newRedirects;
376
+ $this->saveSettings();
377
+
378
+ //Remember the first time the admin changes settings. This can then be used to avoid applying
379
+ //"first login" redirects to users who existed before any custom redirects did.
380
+ $activityTimestamp = get_site_option(self::SETTINGS_INIT_TIME_KEY, null);
381
+ if ( empty($activityTimestamp) ) {
382
+ add_site_option(self::SETTINGS_INIT_TIME_KEY, time());
383
+ }
384
+
385
+ $params = ['updated' => 1];
386
+ if ( !empty($post['selectedTrigger']) ) {
387
+ $params['selectedTrigger'] = strval($post['selectedTrigger']);
388
+ }
389
+
390
+ wp_redirect($this->getTabUrl($params));
391
+ exit;
392
+ }
393
+
394
+ /**
395
+ * @param $settings
396
+ * @return bool|WP_Error
397
+ */
398
+ protected function validateSubmittedSettings($settings) {
399
+ if ( !is_array($settings) ) {
400
+ return new WP_Error(
401
+ 'ame_invalid_json',
402
+ sprintf('Invalid JSON data. Expected an associative array, got %s.', gettype($settings))
403
+ );
404
+ }
405
+
406
+ if ( !array_key_exists('redirects', $settings) ) {
407
+ return new WP_Error('rui_missing_redirects_key', 'The required "redirects" field is missing.');
408
+ }
409
+
410
+ $allowedProperties = [
411
+ //Actor IDs always follow the "prefix:value" format.
412
+ 'actorId' => /** @lang RegExp */
413
+ '@^[a-z]{1,15}+:[^\s].{0,300}+$@i',
414
+ //The URL can be basically anything, so we don't try to validate it.
415
+ 'urlTemplate' => null,
416
+ //Menu template IDs are based on menu URLs, so they're pretty unpredictable. If one is given, it must be non-empty.
417
+ 'menuTemplateId' => /** @lang RegExp */
418
+ '@^.@',
419
+ //A trigger is always a lowercase string. We could just list the supported values once dev. is done.
420
+ 'trigger' => /** @lang RegExp */
421
+ '@^[a-z\-]{2,20}+$@i',
422
+ //The shortcode flag is a boolean value. No regex for that.
423
+ 'shortcodesEnabled' => null,
424
+ ];
425
+ $requiredProperties = [
426
+ 'actorId' => true,
427
+ 'urlTemplate' => true,
428
+ 'shortcodesEnabled' => true,
429
+ 'trigger' => true,
430
+ ];
431
+
432
+ foreach ($settings['redirects'] as $key => $redirect) {
433
+ if ( !is_array($redirect) ) {
434
+ return new WP_Error(
435
+ 'rui_bad_redirect_data_type',
436
+ sprintf('Redirect %s should be an array but it is actually %s', $key, gettype($redirect))
437
+ );
438
+ }
439
+
440
+ //Verify that it has all the required properties.
441
+ $missingProperties = array_diff_key($requiredProperties, $redirect);
442
+ if ( !empty($missingProperties) ) {
443
+ $firstMissingProp = reset($missingProperties);
444
+ return new WP_Error(
445
+ 'rui_missing_key',
446
+ sprintf('Redirect %s is missing the required property "%s"', $key, $firstMissingProp)
447
+ );
448
+ }
449
+
450
+ //Verify that the redirect has only allowed properties.
451
+ $badProperties = array_diff_key($redirect, $allowedProperties);
452
+ if ( !empty($badProperties) ) {
453
+ $firstBadProp = reset($badProperties);
454
+ return new WP_Error(
455
+ 'rui_bad_key',
456
+ sprintf('Redirect %s has an unsupported property "%s"', $key, $firstBadProp)
457
+ );
458
+ }
459
+
460
+ //String properties must match their validation regex (if any).
461
+ foreach ($allowedProperties as $property => $regex) {
462
+ if (
463
+ is_string($regex) && isset($redirect[$property])
464
+ && (!is_string($redirect[$property]) || !preg_match($regex, $redirect[$property]))
465
+ ) {
466
+ return new WP_Error(
467
+ 'rui_invalid_property_value',
468
+ sprintf('Redirect %s: Property "%s" has an invalid value.', $key, $property)
469
+ );
470
+ }
471
+ }
472
+
473
+ //shortcodesEnabled must be a boolean.
474
+ if ( array_key_exists('shortcodesEnabled', $redirect) && !is_bool($redirect['shortcodesEnabled']) ) {
475
+ return new WP_Error(
476
+ 'rui_invalid_property_value',
477
+ sprintf(
478
+ 'Redirect %s: The "shortcodesEnabled" property is invalid.'
479
+ . ' Expected a boolean, but actual type is "%s".',
480
+ $key,
481
+ gettype($redirect['shortcodesEnabled'])
482
+ )
483
+ );
484
+ }
485
+
486
+ //URL template must be a string.
487
+ if ( !is_string($redirect['urlTemplate']) ) {
488
+ return new WP_Error(
489
+ 'rui_invalid_property_value',
490
+ sprintf(
491
+ 'Redirect %s: The "urlTemplate" property is invalid.'
492
+ . ' Expected a string, but actual type is "%s".',
493
+ $key,
494
+ gettype($redirect['urlTemplate'])
495
+ )
496
+ );
497
+ }
498
+
499
+ //URL template must be non-empty.
500
+ if ( trim($redirect['urlTemplate']) === '' ) {
501
+ return new WP_Error(
502
+ 'rui_empty_url',
503
+ sprintf('Redirect %s: The "urlTemplate" property is empty.', $key)
504
+ );
505
+ }
506
+ }
507
+
508
+ return true;
509
+ }
510
+
511
+ /**
512
+ * Load user data for display in the redirect management UI.
513
+ *
514
+ * Will load some or all users depending on how many users there are in total.
515
+ * Users that have custom redirects are always loaded.
516
+ *
517
+ * @param array $flattenedRedirects
518
+ * @return array{array,boolean} An array of users and a boolean indicating if the total number exceeds the limit.
519
+ */
520
+ protected function preloadUsers(array $flattenedRedirects) {
521
+ $loadedUsers = get_users([
522
+ //In Multisite, include all sites and not just the current site. Note that this might not work if used
523
+ //together with some other arguments (judging by WP_User_Query::prepare_query source code).
524
+ 'blog_id' => 0,
525
+ 'number' => self::PRELOADED_USER_LIMIT + 1,
526
+ 'count_total' => false, //Allegedly, this can improve performance.
527
+ 'fields' => self::$desiredUserFields,
528
+ ]);
529
+
530
+ $hasMoreUsers = count($loadedUsers) > self::PRELOADED_USER_LIMIT;
531
+
532
+ $isUserLoaded = [];
533
+ foreach ($loadedUsers as $user) {
534
+ $isUserLoaded[$user->user_login] = true;
535
+ }
536
+
537
+ //Always load users that already have custom redirects.
538
+ $userPrefix = 'user:';
539
+ $userPrefixLength = strlen($userPrefix);
540
+ $usersToLoad = [];
541
+ foreach ($flattenedRedirects as $details) {
542
+ if ( substr($details['actorId'], 0, $userPrefixLength) === $userPrefix ) {
543
+ $userLogin = substr($details['actorId'], $userPrefixLength);
544
+ if (
545
+ is_string($userLogin) && ($userLogin !== '')
546
+ && empty($isUserLoaded[$userLogin])
547
+ && empty($usersToLoad[$userLogin])
548
+ ) {
549
+ $usersToLoad[$userLogin] = true;
550
+ }
551
+ }
552
+ }
553
+
554
+ if ( !empty($usersToLoad) ) {
555
+ $additionalUsers = get_users([
556
+ 'blog_id' => 0,
557
+ 'count_total' => false,
558
+ 'fields' => self::$desiredUserFields,
559
+ 'login__in' => array_values($usersToLoad),
560
+ ]);
561
+ $loadedUsers = array_merge($loadedUsers, $additionalUsers);
562
+ }
563
+
564
+ return [$loadedUsers, $hasMoreUsers];
565
+ }
566
+
567
+ public function userCanSearchUsers() {
568
+ return $this->menuEditor->current_user_can_edit_menu();
569
+ }
570
+
571
+ public function ajaxSearchUsers($params) {
572
+ $foundUsers = get_users([
573
+ 'search' => '*' . $params['term'] . '*',
574
+ 'search_columns' => ['user_login', 'display_name'],
575
+ 'blog_id' => 0,
576
+ 'number' => self::SEARCH_USER_LIMIT,
577
+ 'count_total' => false,
578
+ 'fields' => self::$desiredUserFields,
579
+ ]);
580
+
581
+ $results = [];
582
+ foreach ($foundUsers as $user) {
583
+ $user = (array)$user;
584
+ $results[] = array_merge($user, ['label' => $user['user_login']]);
585
+ }
586
+
587
+ return $results;
588
+ }
589
+
590
+ public function addContextualHelp() {
591
+ if ( !is_callable('get_current_screen') ) {
592
+ return;
593
+ }
594
+ $screen = get_current_screen();
595
+ if ( $screen ) {
596
+ $screen->add_help_tab([
597
+ 'title' => 'Shortcodes',
598
+ 'id' => 'ame-rui-help-shortcodes',
599
+ 'content' => $this->getShortcodeHelp(),
600
+ ]);
601
+
602
+ $screen->add_help_tab([
603
+ 'title' => 'Priority',
604
+ 'id' => 'ame-rui-help-priority',
605
+ 'content' => $this->getPriorityHelp(),
606
+ ]);
607
+
608
+ $screen->add_help_tab([
609
+ 'title' => 'First Login',
610
+ 'id' => 'ame-rui-help-first-login',
611
+ 'content' => $this->getFirstLoginHelp(),
612
+ ]);
613
+
614
+ $screen->add_help_tab([
615
+ 'title' => 'Disabling Redirects',
616
+ 'id' => 'ame-rui-emergency-shutdown',
617
+ 'content' => $this->getEmergencyShutdownHelp(),
618
+ ]);
619
+ }
620
+ }
621
+
622
+ private function getShortcodeHelp() {
623
+ $message = '<p>You can use shortcodes in redirect URLs. This plugin comes with a few shortcodes that could be useful for redirects:</p>';
624
+ $message .= '<ul>';
625
+
626
+ $message .= '<li>' . $this->formatShortcodeInfo(
627
+ 'ame-wp-admin',
628
+ 'base URL of the WordPress dashboard. Includes the trailing slash.',
629
+ '[ame-wp-admin]'
630
+ ) . '</li>';
631
+
632
+ $message .= '<li>' . $this->formatShortcodeInfo(
633
+ 'ame-home-url',
634
+ 'site URL. Usually, this will be the same as the "Site Address (URL)" value in <em>Settings &rarr; General</em>.',
635
+ '[ame-home-url]'
636
+ ) . '</li>';
637
+
638
+ $message .= '<li>' . $this->formatShortcodeInfo(
639
+ 'ame-user-info',
640
+ 'information about the logged-in user. Use the <code>field</code> parameter to specify which field to output. Examples:'
641
+ ) . '<ul>';
642
+
643
+ $userExampleFields = [
644
+ 'ID',
645
+ 'user_login',
646
+ 'display_name',
647
+ 'locale',
648
+ 'user_nicename',
649
+ ];
650
+ foreach ($userExampleFields as $field) {
651
+ $code = '[ame-user-info field="' . $field . '"]';
652
+ $message .= sprintf('<li><code>%s</code> = %s</li>', $code, $this->getExampleShortcodeOutput($code));
653
+ }
654
+
655
+ $message .= '</ul></ul>';
656
+
657
+ $message .= '<p>Some shortcodes from other plugins may also work, but it depends on the shortcode.</p>';
658
+
659
+ return $message;
660
+ }
661
+
662
+ private function formatShortcodeInfo($tag, $description, $exampleCode = null) {
663
+ $result = sprintf('<code>[%s]</code> - %s', esc_html($tag), $description);
664
+ if ( $exampleCode !== null ) {
665
+ $result .= ' Example output:<br>' . $this->getExampleShortcodeOutput($exampleCode);
666
+ }
667
+ return $result;
668
+ }
669
+
670
+ private function getExampleShortcodeOutput($exampleCode) {
671
+ $output = do_shortcode($exampleCode);
672
+ if ( $output === '' ) {
673
+ return '<em>(empty string)</em>';
674
+ }
675
+ return sprintf('<code>%s</code>', esc_html($output));
676
+ }
677
+
678
+ private function getPriorityHelp() {
679
+ $tips = [
680
+ 'Redirects are processed from top to bottom and the first matching setting is used.',
681
+ 'You can drag and drop redirects to change their priority.',
682
+ 'When you create redirects for specific users their order doesn\'t matter, but you can still move them around to organize them.',
683
+ ];
684
+
685
+ return '<ul><li>'
686
+ . implode("</li>\n<li>", array_map('esc_html', $tips))
687
+ . '</li></ul>';
688
+ }
689
+
690
+ private function getFirstLoginHelp() {
691
+ $conditions = [
692
+ sprintf('The user was registered less than %d days ago.', self::FIRST_LOGIN_AGE_LIMIT_IN_DAYS),
693
+ 'The user was registered <em>after</em> redirect settings were changed for the first time.',
694
+ sprintf('The user has not logged in while this plugin and the "%s" module is active.', $this->tabTitle),
695
+ ];
696
+
697
+ return '<p>A "first login" redirect happens when a new user logs in for the first time.<p>'
698
+ . '<p>WordPress does not record logins, so sometimes it\'s not possible to reliably
699
+ determine if a user has already logged in before or not. To help avoid unnecessary
700
+ redirects, the plugin will only perform a "first login" redirect when all of the following
701
+ conditions are met:</p>'
702
+ . '<ul><li>'
703
+ . implode("</li>\n<li>", $conditions)
704
+ . '</li></ul>';
705
+ }
706
+
707
+ private function getEmergencyShutdownHelp() {
708
+ return '<p>If something goes wrong, you can disable all custom redirects by adding this code to wp-config.php:</p>'
709
+ . '<p><code>define(\'AME_DISABLE_REDIRECTS\', true);</code></p>'
710
+ . '<p>Note that this only applies to redirects created using this plugin. It will not prevent other plugins or themes from redirecting users.</p>';
711
+ }
712
+ }
713
+
714
+ class Redirect {
715
+ /**
716
+ * @var string
717
+ */
718
+ private $actorId;
719
+ /**
720
+ * @var string
721
+ */
722
+ private $urlTemplate;
723
+
724
+ /**
725
+ * @var boolean
726
+ */
727
+ private $shortcodesEnabled;
728
+
729
+ protected function __construct(
730
+ $actorId,
731
+ $urlTemplate,
732
+ $shortcodesEnabled = false
733
+ ) {
734
+ $this->actorId = $actorId;
735
+ $this->urlTemplate = $urlTemplate;
736
+ $this->shortcodesEnabled = $shortcodesEnabled;
737
+ }
738
+
739
+ public static function fromArray(array $properties) {
740
+ return new static(
741
+ $properties['actorId'],
742
+ $properties['urlTemplate'],
743
+ !empty($properties['shortcodesEnabled'])
744
+ );
745
+ }
746
+
747
+ /**
748
+ * @return string
749
+ */
750
+ public function getActorId() {
751
+ return $this->actorId;
752
+ }
753
+
754
+ /**
755
+ * @return string
756
+ */
757
+ public function getUrl() {
758
+ $url = $this->urlTemplate;
759
+ if ( $this->shortcodesEnabled && function_exists('do_shortcode') ) {
760
+ $url = do_shortcode($url);
761
+ }
762
+ return $url;
763
+ }
764
+ }
765
+
766
+ class RedirectCollection {
767
+ /**
768
+ * @var array<string,array[]>
769
+ */
770
+ protected $rawItems = [];
771
+
772
+ public function __construct($rawItems = []) {
773
+ $this->rawItems = $rawItems;
774
+ }
775
+
776
+ /**
777
+ * @param string $trigger
778
+ * @return Redirect[]
779
+ */
780
+ public function filterByTrigger($trigger) {
781
+ if ( isset($this->rawItems[$trigger]) ) {
782
+ return array_map([Redirect::class, 'fromArray'], $this->rawItems[$trigger]);
783
+ } else {
784
+ return [];
785
+ }
786
+ }
787
+
788
+ /**
789
+ * @return array
790
+ */
791
+ public function toDbFormat() {
792
+ return $this->rawItems;
793
+ }
794
+
795
+ /**
796
+ * @param array $items
797
+ * @return static
798
+ */
799
+ public static function fromDbFormat($items) {
800
+ return new static($items);
801
+ }
802
+
803
+ /**
804
+ * @return array
805
+ */
806
+ public function flatten() {
807
+ $results = [];
808
+ foreach ($this->rawItems as $trigger => $items) {
809
+ foreach ($items as $properties) {
810
+ if ( !is_array($properties) ) {
811
+ continue;
812
+ }
813
+ $properties['trigger'] = $trigger;
814
+ $results[] = $properties;
815
+ }
816
+ }
817
+ return $results;
818
+ }
819
+
820
+ /**
821
+ * Add a redirect to the collection.
822
+ *
823
+ * @param array $redirectProperties
824
+ */
825
+ public function add($redirectProperties) {
826
+ $trigger = $redirectProperties['trigger'];
827
+ if ( !isset($this->rawItems[$trigger]) ) {
828
+ $this->rawItems[$trigger] = [];
829
+ }
830
+ $this->rawItems[$trigger][] = $redirectProperties;
831
+ }
832
+ }
833
+
834
+ abstract class Triggers {
835
+ const LOGIN = 'login';
836
+ const LOGOUT = 'logout';
837
+ const REGISTRATION = 'registration';
838
+ const FIRST_LOGIN = 'firstLogin';
839
+
840
+ public static function getValues() {
841
+ return [self::LOGIN, self::LOGOUT, self::REGISTRATION, self::FIRST_LOGIN];
842
+ }
843
+ }
844
+
845
+ /**
846
+ * Really basic Option type implementation.
847
+ *
848
+ * @template T
849
+ */
850
+ abstract class Option {
851
+ /**
852
+ * @return T
853
+ */
854
+ abstract public function get();
855
+
856
+ /**
857
+ * @return boolean
858
+ */
859
+ abstract public function isEmpty();
860
+
861
+ /**
862
+ * @return boolean
863
+ */
864
+ abstract public function nonEmpty();
865
+
866
+ /**
867
+ * @return boolean
868
+ */
869
+ public function isDefined() {
870
+ return $this->nonEmpty();
871
+ }
872
+ }
873
+
874
+ /**
875
+ * @template T
876
+ * @extends Option<T>
877
+ */
878
+ final class Some extends Option {
879
+ /**
880
+ * @var T
881
+ */
882
+ private $value;
883
+
884
+ /**
885
+ * Some constructor.
886
+ *
887
+ * @param T $value
888
+ */
889
+ public function __construct($value) {
890
+ $this->value = $value;
891
+ }
892
+
893
+ /**
894
+ * @return T
895
+ */
896
+ public function get() {
897
+ return $this->value;
898
+ }
899
+
900
+ public function isEmpty() {
901
+ return false;
902
+ }
903
+
904
+ public function nonEmpty() {
905
+ return true;
906
+ }
907
+ }
908
+
909
+ /**
910
+ * @template T
911
+ * @extends Option<T>
912
+ */
913
+ final class None extends Option {
914
+ private function __construct() {
915
+ //This constructor only exists to prevent others from creating instances.
916
+ }
917
+
918
+ public function get() {
919
+ throw new RuntimeException('Option value is not set');
920
+ }
921
+
922
+ public function isEmpty() {
923
+ return true;
924
+ }
925
+
926
+ public function nonEmpty() {
927
+ return false;
928
+ }
929
+
930
+ public static function getInstance() {
931
+ static $instance = null;
932
+ if ( $instance === null ) {
933
+ $instance = new self();
934
+ }
935
+ return $instance;
936
+ }
937
+ }
938
+
939
+ class MenuExtractor {
940
+ private $items = [];
941
+
942
+ public function __construct($menuTree) {
943
+ foreach ($menuTree as $item) {
944
+ $this->processItem($item);
945
+ }
946
+ }
947
+
948
+ private function processItem($item, $parentTitle = null) {
949
+ //Skip separators.
950
+ if ( !empty($item['separator']) ) {
951
+ return;
952
+ }
953
+
954
+ $templateId = ameMenuItem::get($item, 'template_id');
955
+ $url = ameMenuItem::get($item, 'url');
956
+
957
+ $rawTitle = ameMenuItem::get($item, 'menu_title', '[Untitled]');
958
+ $fullTitle = trim(strip_tags(ameMenuItem::remove_update_count($rawTitle)));
959
+ if ( $parentTitle !== null ) {
960
+ $fullTitle = $parentTitle . ' → ' . $fullTitle;
961
+ }
962
+
963
+ if ( empty($item['custom']) && ($templateId !== null) && !$this->looksLikeUnusableSlug($url) ) {
964
+ //Add the admin URL shortcode to the URL if it looks like a relative URL that points
965
+ //to a dashboard page.
966
+ if ( $this->looksLikeDashboardUrl($url) ) {
967
+ $url = '[ame-wp-admin]' . $url;
968
+ }
969
+
970
+ $this->items[] = [
971
+ 'templateId' => $templateId,
972
+ 'title' => $fullTitle,
973
+ 'url' => $url,
974
+ ];
975
+ }
976
+
977
+ if ( !empty($item['items']) ) {
978
+ foreach ($item['items'] as $submenu) {
979
+ $this->processItem($submenu, $fullTitle);
980
+ }
981
+ }
982
+ }
983
+
984
+ /**
985
+ * @param string $url
986
+ * @return boolean
987
+ */
988
+ private function looksLikeDashboardUrl($url) {
989
+ $scheme = parse_url($url, PHP_URL_SCHEME);
990
+ if ( !empty($scheme) ) {
991
+ return false;
992
+ }
993
+
994
+ return preg_match('@^[a-z0-9][a-z0-9_-]{0,30}?\.php@i', $url) === 1;
995
+ }
996
+
997
+ /**
998
+ * Check if a string looks like a plain admin menu slug and not a usable URL.
999
+ *
1000
+ * Sometimes plugins create admin menus that don't have a callback function. A menu like that
1001
+ * will show up fine, and it can be used as a parent for other menu items. However, the menu
1002
+ * itself won't have a working URL, so we don't want to offer it as a redirect option.
1003
+ *
1004
+ * For example, the top level "WooCommerce" menu doesn't have a valid URL, it just has
1005
+ * a slug: "woocommerce". You just usually don't notice this because WordPress automatically
1006
+ * replaces the menu URL with the URL of the first child item.
1007
+ *
1008
+ * @param string|mixed $url
1009
+ * @return bool
1010
+ */
1011
+ private function looksLikeUnusableSlug($url) {
1012
+ if ( !is_string($url) ) {
1013
+ return true;
1014
+ }
1015
+
1016
+ //Technically, a menu slug could be anything, so we can't easily determine if a string
1017
+ //is really a slug or just a weird relative URL. However, it seems safe to assume that
1018
+ //a "URL" that has no dots (so no file extension or domain name) and no slashes (so no
1019
+ //protocol or relative directories) is probably unusable.
1020
+ $suspiciousSegmentLength = strcspn($url, './');
1021
+ return ($suspiciousSegmentLength === strlen($url));
1022
+ }
1023
+
1024
+ public function getUsableItems() {
1025
+ return $this->items;
1026
+ }
1027
+ }
modules/redirector/redirector.scss ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "../../css/boxes";
2
+
3
+ $redirectInputSpacing: 10px;
4
+
5
+ $itemBackground: #fff;
6
+ $defaultItemBorderColor: #dcdcde;
7
+ $itemPadding: 10px;
8
+
9
+ $inputHeight: 30px;
10
+ $inputBorderWidth: 1px;
11
+ $dropdownTriggerWidth: 30px;
12
+
13
+ .ame-rui-redirect-list {
14
+ display: flex;
15
+ flex-direction: column;
16
+
17
+ padding-top: 1px; //To offset the negative margin on the first item.
18
+ }
19
+
20
+ .ame-rui-redirect {
21
+ display: flex;
22
+ flex-direction: row;
23
+
24
+ background: $itemBackground;
25
+ border: 1px solid $defaultItemBorderColor;
26
+ margin-top: -1px;
27
+
28
+ &.ui-sortable-helper {
29
+ border: 1px solid #8c8f94;
30
+ box-shadow: 1px 1px 5px 0 rgba(0, 0, 0, 0.30);
31
+ }
32
+ }
33
+
34
+ .ame-rui-redirect-content {
35
+ flex-grow: 1;
36
+ display: flex;
37
+ flex-direction: row;
38
+
39
+ padding: $itemPadding $itemPadding $itemPadding 0;
40
+ }
41
+
42
+ .ame-rui-actor {
43
+ margin-right: $redirectInputSpacing;
44
+
45
+ font-size: 14px;
46
+ line-height: 27px;
47
+ }
48
+
49
+ .ame-rui-url-template, ame-redirect-url-input {
50
+ flex-grow: 1;
51
+ display: block; //Only needed when outside a flex container.
52
+
53
+ margin-right: $redirectInputSpacing;
54
+ position: relative;
55
+
56
+ input[type=text] {
57
+ width: 100%;
58
+
59
+ &.ame-rui-has-url-dropdown {
60
+ padding-right: $dropdownTriggerWidth + 2px;
61
+ }
62
+ }
63
+
64
+ input[readonly] {
65
+ background-color: #dbeeff; //Eeeh. Choosing colours is hard.
66
+ }
67
+ }
68
+
69
+ .ame-rui-url-dropdown-trigger {
70
+ $triggerHeight: $inputHeight - 2 * $inputBorderWidth;
71
+
72
+ display: block;
73
+ position: absolute;
74
+ width: $dropdownTriggerWidth;
75
+ height: $triggerHeight;
76
+
77
+ top: $inputBorderWidth;
78
+ right: $inputBorderWidth;
79
+ border-top-right-radius: 4px;
80
+ border-bottom-right-radius: 4px;
81
+
82
+ cursor: pointer;
83
+ text-align: center;
84
+
85
+ color: #787c82;
86
+
87
+ &:hover {
88
+ color: #1d2327;
89
+ }
90
+
91
+ .am-rui-trigger-icon {
92
+ &:before {
93
+ content: "\f140";
94
+ font: normal 20px dashicons;
95
+ line-height: $triggerHeight;
96
+ vertical-align: middle;
97
+ }
98
+ }
99
+ }
100
+
101
+ .ame-rui-shortcodes-enabled {
102
+ margin: 0 $redirectInputSpacing 0 0;
103
+
104
+ display: inline-block;
105
+ box-sizing: border-box;
106
+ min-height: $inputHeight;
107
+ padding: 0 10px;
108
+
109
+ $lineHeight: $inputHeight - 2 * $inputBorderWidth;
110
+ line-height: $lineHeight;
111
+
112
+ border: $inputBorderWidth solid #dcdcde;
113
+ border-radius: 3px;
114
+ background: #f6f7f7;
115
+
116
+ .dashicons {
117
+ vertical-align: top;
118
+ line-height: $lineHeight;
119
+ }
120
+
121
+ &:hover, &:active {
122
+ background: #f0f0f1;
123
+ border-color: #0a4b78;
124
+ }
125
+ }
126
+
127
+ .ame-rui-drag-handle {
128
+ cursor: grab;
129
+ min-width: 30px;
130
+
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: center;
134
+
135
+ .ui-sortable-helper > & {
136
+ cursor: grabbing;
137
+ }
138
+ }
139
+
140
+ .ame-rui-drag-icon {
141
+ fill: rgb(30, 30, 30);
142
+ }
143
+
144
+ #ame-rui-menu-items {
145
+ background-image: none;
146
+ max-width: 40rem; //WP 5.8 limits the width to 25 rem, but the menu input can be wider than that.
147
+ padding-right: 8px;
148
+
149
+ box-shadow: 3px 3px 4px -2px rgba(0, 0, 0, 0.50);
150
+
151
+ &:hover, &:focus {
152
+ color: unset;
153
+ }
154
+
155
+ option:hover {
156
+ background-color: #f0f0f5;
157
+ }
158
+ }
159
+
160
+ .ame-rui-default-redirect-container {
161
+ max-width: 768px;
162
+
163
+ .ame-rui-redirect-content {
164
+ padding-left: $itemPadding;
165
+ }
166
+ }
167
+
168
+ .ame-rui-add-actor-dropdown, #ame-rui-user-search-query {
169
+ min-width: 15em;
170
+ }
171
+
172
+ .ame-rui-found-users {
173
+ //Add a background color to the selected item. WP 5.8 doesn't have it by default, at least not for all
174
+ //autocomplete menus. The "ui-state-active" class is added to the wrapper, not to the entire LI, so we
175
+ //also need to adjust the paddings to make the wrapper expand to fill the item.
176
+ li.ui-menu-item {
177
+ padding: 0;
178
+ }
179
+
180
+ .ui-menu-item-wrapper {
181
+ padding: 4px 10px 6px;
182
+ }
183
+
184
+ .ui-state-active {
185
+ background-color: #2271b1;
186
+ color: #fff;
187
+ }
188
+ }
189
+
190
+ .ame-rui-missing-actor-indicator {
191
+ vertical-align: middle;
192
+ font-style: italic;
193
+ }
194
+
195
+ .ame-rui-trigger-selector {
196
+ display: inline-block;
197
+ margin-bottom: 0;
198
+
199
+ list-style: none;
200
+ font-size: 14px;
201
+
202
+ li {
203
+ display: inline-block;
204
+ margin: 0;
205
+
206
+ a {
207
+ text-decoration: none;
208
+ transition: none;
209
+ }
210
+ }
211
+ }
212
+
213
+ .ame-rui-small-tabs {
214
+ $tabBorderColor: #c3c4c7;
215
+ $tabBackgroundColor: #e3e3e5;
216
+ $activeTabBackground: transparent;
217
+ $contentBackgroundColor: #f0f0f1;
218
+ $activeTabText: #000;
219
+
220
+ display: inline-block;
221
+ padding-left: 7px;
222
+ border-bottom: 1px solid $tabBorderColor;
223
+
224
+
225
+ li {
226
+ display: inline-block;
227
+ margin: 0;
228
+ background: $tabBackgroundColor;
229
+ border: 1px solid $tabBorderColor;
230
+ border-bottom: none;
231
+
232
+ a {
233
+ display: inline-block;
234
+ transition: none;
235
+ min-width: 7em;
236
+
237
+ font-size: 14px;
238
+ padding: 5px 8px 6px 8px;
239
+ cursor: pointer;
240
+ text-decoration: none;
241
+
242
+ color: #444;
243
+
244
+ &:hover {
245
+ color: #2271b1;
246
+ }
247
+ }
248
+ }
249
+
250
+ .ame-rui-active-tab {
251
+ background: $activeTabBackground;
252
+ border-bottom: 1px solid $contentBackgroundColor;
253
+ margin-bottom: -1px;
254
+
255
+ a {
256
+ color: $activeTabText;
257
+
258
+ &:hover {
259
+ color: $activeTabText;
260
+ }
261
+ }
262
+ }
263
+ }
264
+
265
+ .ame-rui-filter-like-tabs {
266
+ $textColor: #646970;
267
+ $markerColor: #007cba; //Blue like in Gutenberg.
268
+ //$markerColor: #646970; //Grey like on the "Plugins -> Add New" page.
269
+
270
+ $hoverTextColor: #135e96; //Default for links in WP 5.8.
271
+ //$hoverMarkerColor: #c1cbd9; //Light grey.
272
+ $hoverMarkerColor: transparent;
273
+
274
+ display: inline-flex;
275
+ margin: 13px 0 0 0;
276
+ list-style: none;
277
+
278
+ border: 1px solid #c3c4c7;
279
+ background: #fff;
280
+
281
+ li {
282
+ display: inline-block;
283
+ margin: 0;
284
+ padding: 0;
285
+ border-style: none;
286
+
287
+ a {
288
+ display: inline-block;
289
+ margin: 0;
290
+ padding: 10px 14px;
291
+ min-width: 7em;
292
+
293
+ border: none;
294
+ border-bottom: 4px solid transparent;
295
+
296
+ color: $textColor;
297
+ text-decoration: none;
298
+ cursor: pointer;
299
+
300
+ &:hover {
301
+ color: $hoverTextColor;
302
+ border-bottom-color: $hoverMarkerColor;
303
+ }
304
+ }
305
+ }
306
+
307
+ .ame-rui-active-tab {
308
+ a {
309
+ border-bottom-color: $markerColor;
310
+ }
311
+
312
+ a:hover {
313
+ color: $textColor;
314
+ border-bottom-color: $markerColor;
315
+ }
316
+ }
317
+ }
318
+
319
+ .ame-rui-sub-tabs {
320
+ margin-top: 6px;
321
+ font-size: 13px;
322
+
323
+ li {
324
+ &::after {
325
+ content: '|';
326
+ }
327
+
328
+ &:last-child::after {
329
+ content: '';
330
+ }
331
+
332
+ a {
333
+ display: inline-block;
334
+ text-align: center;
335
+ padding: 0.2em;
336
+ line-height: 2;
337
+ cursor: pointer;
338
+
339
+ &::before {
340
+ display: block;
341
+ content: attr(data-text);
342
+ font-weight: bold;
343
+ height: 1px;
344
+ overflow: hidden;
345
+ visibility: hidden;
346
+ margin-bottom: -1px;
347
+ }
348
+ }
349
+
350
+ &:first-child a {
351
+ padding-left: 0;
352
+ text-align: left;
353
+ }
354
+ }
355
+
356
+ .ame-rui-active-tab {
357
+ a {
358
+ font-weight: 600;
359
+ color: #000;
360
+ }
361
+ }
362
+ }
363
+
364
+ .ame-rui-actions button {
365
+ .dashicons {
366
+ vertical-align: inherit;
367
+ line-height: 28px;
368
+ }
369
+ }
370
+
371
+ #ame-rui-column-container {
372
+ display: flex;
373
+ flex-direction: row;
374
+ }
375
+
376
+ #ame-rui-main-section {
377
+ display: block;
378
+ flex-grow: 1;
379
+ max-width: 1320px;
380
+ }
381
+
382
+ #ame-rui-sidebar {
383
+ display: none;
384
+ width: 300px;
385
+ flex-shrink: 0;
386
+ flex-grow: 0;
387
+ align-self: flex-start;
388
+
389
+ margin-left: 20px;
390
+ }
391
+
392
+ #ame-rui-main-actions {
393
+ @include ame-emulated-postbox();
394
+ margin-bottom: 0;
395
+ padding: 10px 8px;
396
+ border: 1px solid $amePostboxBorderColor;
397
+
398
+ input.button {
399
+ max-width: 150px;
400
+ width: 100%;
401
+ }
402
+ }
403
+
404
+ @media only screen and (min-width: 961px) {
405
+ .ame-rui-actor {
406
+ min-width: 12em;
407
+ }
408
+
409
+ .ame-rui-actions button .dashicons {
410
+ display: none;
411
+ }
412
+ }
413
+
414
+ @media only screen and (max-width: 960px) {
415
+ .ame-rui-actor {
416
+ min-width: 10em;
417
+ }
418
+
419
+ .ame-rui-shortcodes-enabled, .ame-rui-actions {
420
+ .ame-rui-button-label {
421
+ display: none;
422
+ }
423
+ }
424
+ }
425
+
426
+ @media only screen and (max-width: 782px) {
427
+ $lineHeight: 38px;
428
+
429
+ .ame-rui-actor {
430
+ line-height: $lineHeight;
431
+ }
432
+
433
+ .ame-rui-redirect .ame-rui-actions button {
434
+ margin-bottom: 0;
435
+
436
+ .dashicons {
437
+ line-height: $lineHeight;
438
+ vertical-align: top;
439
+ }
440
+ }
441
+
442
+ .ame-rui-shortcodes-enabled {
443
+ line-height: $lineHeight;
444
+
445
+ .dashicons {
446
+ vertical-align: top;
447
+ line-height: $lineHeight;
448
+ }
449
+ }
450
+ }
readme.txt CHANGED
@@ -3,13 +3,13 @@ Contributors: whiteshadow
3
  Donate link: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A6P9S6CE3SRSW
4
  Tags: admin, dashboard, menu, security, wpmu
5
  Requires at least: 4.1
6
- Tested up to: 5.7
7
- Stable tag: 1.9.10
8
 
9
  Lets you edit the WordPress admin menu. You can re-order, hide or rename menus, add custom menus and more.
10
 
11
  == Description ==
12
- Admin Menu Editor lets you manually edit the Dashboard menu. You can reorder the menus, show/hide specific items, change premissions, and more.
13
 
14
  **Features**
15
 
@@ -19,9 +19,21 @@ Admin Menu Editor lets you manually edit the Dashboard menu. You can reorder the
19
  * Move a menu item to a different submenu.
20
  * Create custom menus that point to any part of the Dashboard or an external URL.
21
  * Hide/show any menu or menu item. A hidden menu is invisible to all users, including administrators.
 
22
 
23
  The [Pro version](http://w-shadow.com/AdminMenuEditor/) lets you set per-role menu permissions, hide a menu from everyone except a specific user, export your admin menu, drag items between menu levels, make menus open in a new window and more. [Try online demo](http://amedemo.com/wpdemo/demo.php).
24
 
 
 
 
 
 
 
 
 
 
 
 
25
  **Notes**
26
 
27
  * If you delete any of the default menus they will reappear after saving. This is by design. To get rid of a menu for good, either hide it or change it's access permissions.
@@ -63,6 +75,17 @@ Plugins installed in the `mu-plugins` directory are treated as "always on", so y
63
 
64
  == Changelog ==
65
 
 
 
 
 
 
 
 
 
 
 
 
66
  = 1.9.10 =
67
  * Fixed a bug where the plugin could incorrectly identify a separator as the current menu item.
68
  * Fixed submenu box not expanding to align with the selected parent item.
3
  Donate link: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A6P9S6CE3SRSW
4
  Tags: admin, dashboard, menu, security, wpmu
5
  Requires at least: 4.1
6
+ Tested up to: 5.8.1
7
+ Stable tag: 1.10
8
 
9
  Lets you edit the WordPress admin menu. You can re-order, hide or rename menus, add custom menus and more.
10
 
11
  == Description ==
12
+ Admin Menu Editor lets you manually edit the Dashboard menu. You can reorder the menus, show/hide specific items, change permissions, and more.
13
 
14
  **Features**
15
 
19
  * Move a menu item to a different submenu.
20
  * Create custom menus that point to any part of the Dashboard or an external URL.
21
  * Hide/show any menu or menu item. A hidden menu is invisible to all users, including administrators.
22
+ * Create login redirects and logout redirects.
23
 
24
  The [Pro version](http://w-shadow.com/AdminMenuEditor/) lets you set per-role menu permissions, hide a menu from everyone except a specific user, export your admin menu, drag items between menu levels, make menus open in a new window and more. [Try online demo](http://amedemo.com/wpdemo/demo.php).
25
 
26
+ **Shortcodes**
27
+
28
+ The plugin provides a few utility shortcodes. These are mainly intended to help with creating login/logout redirects, but you can also use them in posts and pages.
29
+
30
+ * `[ame-wp-admin]` - URL of the WordPress dashboard (with a trailing slash).
31
+ * `[ame-home-url]` - Site URL. Usually, this is the same as the URL in the "Site Address" field in *Settings -> General*.
32
+ * `[ame-user-info field="..."]` - Information about the logged-in user. Parameters:
33
+ * `field` - The part of user profile to display. Supported fields include: `ID`, `user_login`, `display_name`, `locale`, `user_nicename`, `user_url`, and so on.
34
+ * `placeholder` - Optional. Text that will be shown if the visitor is not logged in.
35
+ * `encoding` - Optional. How to encode or escape the output. This is useful if you want to use the shortcode in your own HTML or JS code. Supported values: `auto` (default), `html`, `attr`, `js`, `none`.
36
+
37
  **Notes**
38
 
39
  * If you delete any of the default menus they will reappear after saving. This is by design. To get rid of a menu for good, either hide it or change it's access permissions.
75
 
76
  == Changelog ==
77
 
78
+ = 1.10 =
79
+ * Added a "Redirects" feature. You can create login redirects, logout redirects, and registration redirects. You can configure redirects for specific roles and users. You can also set up a default redirect that will apply to everyone who doesn't have a specific setting. Redirect URLs can contain shortcodes, but not all shortcodes will work in this context.
80
+ * Added a few utility shortcodes: `[ame-wp-admin]`, `[ame-home-url]`, `[ame-user-info field="..."]`. These are mainly intended to be used to create dynamic redirects, but they will also work in posts and pages.
81
+ * Slightly improved the appearance of settings page tabs on small screens and in narrow browser windows.
82
+ * Fixed a minor conflict where several hidden menu items created by "WP Grid Builder" would unexpectedly show up when AME is active.
83
+ * Fixed a conflict with "LoftLoader Pro", "WS Form", and probably a few other plugins that create new admin menu items that link to the theme customizer. Previously, it was impossible to hide or edit those menu items.
84
+ * Fixed a few jQuery deprecation warnings.
85
+ * Fixed an "Undefined array key" warning that could appear if another plugin created a user role that did not have a "capabilities" key.
86
+ * Fixed a minor BuddyBoss Platform compatibility issue where the menu editor would show a "BuddyBoss -> BuddyBoss" menu item that was not present in the actual admin menu. The item is created by BuddyBoss Platform, but it is apparently intended to be hidden.
87
+ * Refactored the menu editor and added limited support for editing three level menus. While the free version doesn't have the ability to actually render nested items in the admin menu, it should at least load a menu configuration that includes more than two levels without crashing. This will probably only matter if someone edits the settings in the database or copies a menu configuration from the Pro version.
88
+
89
  = 1.9.10 =
90
  * Fixed a bug where the plugin could incorrectly identify a separator as the current menu item.
91
  * Fixed submenu box not expanding to align with the selected parent item.
uninstall.php CHANGED
@@ -28,4 +28,14 @@ if( defined( 'ABSPATH') && defined('WP_UNINSTALL_PLUGIN') ) {
28
  delete_site_option('ws_ame_hide_pv_notice');
29
  delete_site_option('ws_ame_dashboard_widgets');
30
  }
 
 
 
 
 
 
 
 
 
 
31
  }
28
  delete_site_option('ws_ame_hide_pv_notice');
29
  delete_site_option('ws_ame_dashboard_widgets');
30
  }
31
+
32
+ //Remove redirect settings.
33
+ delete_option('ws_ame_redirects');
34
+ if ( function_exists('delete_site_option') ) {
35
+ delete_site_option('ws_ame_redirects');
36
+ delete_site_option('ws_ame_rui_first_change');
37
+ }
38
+ if ( function_exists('delete_metadata') ) {
39
+ delete_metadata('user', 0, 'ame_rui_first_login_done', '', true);
40
+ }
41
  }