Admin Menu Editor - Version 1.3

Version Description

  • Added a new settings page that lets you choose whether admin menu settings are per-site or network-wide, as well as specify who can access the plugin. To access this page, go to "Settings -> Menu Editor Pro" and click the small "Settings" link next to the page title.
  • Added a way to show/hide advanced menu options through the settings page in addition to the "Screen Options" panel.
  • Added a "Show menu access checks" option to make debugging menu permissions easier.
  • Added partial WPML support. Now you can translate custom menu titles with WPML.
  • The plugin will now display an error if you try to activate it when another version of it is already active.
  • Added a "Target page" dropdown as an alternative to the "URL" field. To enter a custom URL, choose "Custom" from the dropdown.
  • Fixed the "window title" setting only working for some menu items and not others.
  • Fixed a number of bugs related to moving plugin menus around.
  • Changed how the plugin stores menu settings. Note: The new format is not backwards-compatible with version 1.2.2.
Download this release

Release Info

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

Code changes from version 1.2.2 to 1.3

.htaccess DELETED
@@ -1,4 +0,0 @@
1
- <IfModule mod_rewrite.c>
2
- RewriteEngine on
3
- RewriteRule ^(.*)\.[\d]{10}\.(css|js)$ $1.$2 [L]
4
- </IfModule>
 
 
 
 
css/admin.css ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Miscellaneous menu styles that can be used on all admin pages.
3
+ */
4
+
5
+ /*
6
+ * Submenu separators.
7
+ */
8
+ hr.ws-submenu-separator {
9
+ display: block;
10
+ margin: 2px 0;
11
+ padding: 0;
12
+ height: 0;
13
+
14
+ border-width: 1px 0 0 0;
15
+ border-style: solid;
16
+ border-color: #ccc;
17
+ }
18
+
19
+ /* Custom separator style suggested by a customer (Slavo) */
20
+ /*
21
+ #adminmenu .ws-submenu-separator {
22
+ border-bottom: none;
23
+ border-top: 1px dotted rgba(0,0,0,.3);
24
+ width: 90%;
25
+ }
26
+ */
27
+
28
+ /* S2Member separator style */
29
+ /*
30
+ #adminmenu .ws-submenu-separator {
31
+ display: block;
32
+ border: 0;
33
+ margin: 1px 0 1px -5px;
34
+ padding: 0;
35
+ height: 1px;
36
+ line-height: 1px;
37
+ background: #CCCCCC;
38
+ }
39
+ */
40
+
41
+ /* No pointer/hand on separators. */
42
+ #adminmenu li.ws-submenu-separator-wrap a {
43
+ cursor: default;
44
+ }
45
+
46
+ /* Override the bluish highlight used by WP */
47
+ #adminmenu li.ws-submenu-separator-wrap a:hover,
48
+ #adminmenu li.ws-submenu-separator-wrap a:focus
49
+ {
50
+ background-color: white;
51
+ }
52
+
css/jquery.qtip.css ADDED
@@ -0,0 +1,573 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*! qTip2 - Pretty powerful tooltips - v2.0.0 - 2012-09-10
2
+ * http://craigsworks.com/projects/qtip2/
3
+ * Copyright (c) 2012 Craig Michael Thompson; Licensed MIT, GPL */
4
+
5
+ /* Fluid class for determining actual width in IE */
6
+ #qtip-rcontainer{
7
+ position: absolute;
8
+ left: -28000px;
9
+ top: -28000px;
10
+ display: block;
11
+ visibility: hidden;
12
+ }
13
+
14
+ /* Fluid class for determining actual width in IE */
15
+ #qtip-rcontainer .ui-tooltip{
16
+ display: block !important;
17
+ visibility: hidden !important;
18
+ position: static !important;
19
+ float: left !important;
20
+ }
21
+
22
+ /* Core qTip styles */
23
+ .ui-tooltip, .qtip{
24
+ position: absolute;
25
+ left: -28000px;
26
+ top: -28000px;
27
+ display: none;
28
+
29
+ max-width: 280px;
30
+ min-width: 50px;
31
+
32
+ font-size: 10.5px;
33
+ line-height: 12px;
34
+ }
35
+
36
+ .ui-tooltip-content{
37
+ position: relative;
38
+ padding: 5px 9px;
39
+ overflow: hidden;
40
+
41
+ text-align: left;
42
+ word-wrap: break-word;
43
+ }
44
+
45
+ .ui-tooltip-titlebar{
46
+ position: relative;
47
+ min-height: 14px;
48
+ padding: 5px 35px 5px 10px;
49
+ overflow: hidden;
50
+
51
+ border-width: 0 0 1px;
52
+ font-weight: bold;
53
+ }
54
+
55
+ .ui-tooltip-titlebar + .ui-tooltip-content{ border-top-width: 0 !important; }
56
+
57
+ /* Default close button class */
58
+ .ui-tooltip-titlebar .ui-state-default{
59
+ position: absolute;
60
+ right: 4px;
61
+ top: 50%;
62
+ margin-top: -9px;
63
+
64
+ cursor: pointer;
65
+ outline: medium none;
66
+
67
+ border-width: 1px;
68
+ border-style: solid;
69
+ }
70
+
71
+ * html .ui-tooltip-titlebar .ui-state-default{ top: 16px; } /* IE fix */
72
+
73
+ .ui-tooltip-titlebar .ui-icon,
74
+ .ui-tooltip-icon .ui-icon{
75
+ display: block;
76
+ text-indent: -1000em;
77
+ direction: ltr;
78
+ }
79
+
80
+ .ui-tooltip-icon, .ui-tooltip-icon .ui-icon{
81
+ -moz-border-radius: 3px;
82
+ -webkit-border-radius: 3px;
83
+ border-radius: 3px;
84
+ text-decoration: none;
85
+ }
86
+
87
+ .ui-tooltip-icon .ui-icon{
88
+ width: 18px;
89
+ height: 14px;
90
+
91
+ text-align: center;
92
+ text-indent: 0;
93
+ font: normal bold 10px/13px Tahoma,sans-serif;
94
+
95
+ color: inherit;
96
+ background: transparent none no-repeat -100em -100em;
97
+ }
98
+
99
+
100
+ /* Applied to 'focused' tooltips e.g. most recently displayed/interacted with */
101
+ .ui-tooltip-focus{}
102
+
103
+ /* Applied on hover of tooltips i.e. added/removed on mouseenter/mouseleave respectively */
104
+ .ui-tooltip-hover{}
105
+
106
+ /* Default tooltip style */
107
+ .ui-tooltip-default{
108
+ border-width: 1px;
109
+ border-style: solid;
110
+ border-color: #F1D031;
111
+
112
+ background-color: #FFFFA3;
113
+ color: #555;
114
+ }
115
+
116
+ .ui-tooltip-default .ui-tooltip-titlebar{
117
+ background-color: #FFEF93;
118
+ }
119
+
120
+ .ui-tooltip-default .ui-tooltip-icon{
121
+ border-color: #CCC;
122
+ background: #F1F1F1;
123
+ color: #777;
124
+ }
125
+
126
+ .ui-tooltip-default .ui-tooltip-titlebar .ui-state-hover{
127
+ border-color: #AAA;
128
+ color: #111;
129
+ }
130
+
131
+
132
+ /*! Light tooltip style */
133
+ .ui-tooltip-light{
134
+ background-color: white;
135
+ border-color: #E2E2E2;
136
+ color: #454545;
137
+ }
138
+
139
+ .ui-tooltip-light .ui-tooltip-titlebar{
140
+ background-color: #f1f1f1;
141
+ }
142
+
143
+
144
+ /*! Dark tooltip style */
145
+ .ui-tooltip-dark{
146
+ background-color: #505050;
147
+ border-color: #303030;
148
+ color: #f3f3f3;
149
+ }
150
+
151
+ .ui-tooltip-dark .ui-tooltip-titlebar{
152
+ background-color: #404040;
153
+ }
154
+
155
+ .ui-tooltip-dark .ui-tooltip-icon{
156
+ border-color: #444;
157
+ }
158
+
159
+ .ui-tooltip-dark .ui-tooltip-titlebar .ui-state-hover{
160
+ border-color: #303030;
161
+ }
162
+
163
+
164
+ /*! Cream tooltip style */
165
+ .ui-tooltip-cream{
166
+ background-color: #FBF7AA;
167
+ border-color: #F9E98E;
168
+ color: #A27D35;
169
+ }
170
+
171
+ .ui-tooltip-cream .ui-tooltip-titlebar{
172
+ background-color: #F0DE7D;
173
+ }
174
+
175
+ .ui-tooltip-cream .ui-state-default .ui-tooltip-icon{
176
+ background-position: -82px 0;
177
+ }
178
+
179
+
180
+ /*! Red tooltip style */
181
+ .ui-tooltip-red{
182
+ background-color: #F78B83;
183
+ border-color: #D95252;
184
+ color: #912323;
185
+ }
186
+
187
+ .ui-tooltip-red .ui-tooltip-titlebar{
188
+ background-color: #F06D65;
189
+ }
190
+
191
+ .ui-tooltip-red .ui-state-default .ui-tooltip-icon{
192
+ background-position: -102px 0;
193
+ }
194
+
195
+ .ui-tooltip-red .ui-tooltip-icon{
196
+ border-color: #D95252;
197
+ }
198
+
199
+ .ui-tooltip-red .ui-tooltip-titlebar .ui-state-hover{
200
+ border-color: #D95252;
201
+ }
202
+
203
+
204
+ /*! Green tooltip style */
205
+ .ui-tooltip-green{
206
+ background-color: #CAED9E;
207
+ border-color: #90D93F;
208
+ color: #3F6219;
209
+ }
210
+
211
+ .ui-tooltip-green .ui-tooltip-titlebar{
212
+ background-color: #B0DE78;
213
+ }
214
+
215
+ .ui-tooltip-green .ui-state-default .ui-tooltip-icon{
216
+ background-position: -42px 0;
217
+ }
218
+
219
+
220
+ /*! Blue tooltip style */
221
+ .ui-tooltip-blue{
222
+ background-color: #E5F6FE;
223
+ border-color: #ADD9ED;
224
+ color: #5E99BD;
225
+ }
226
+
227
+ .ui-tooltip-blue .ui-tooltip-titlebar{
228
+ background-color: #D0E9F5;
229
+ }
230
+
231
+ .ui-tooltip-blue .ui-state-default .ui-tooltip-icon{
232
+ background-position: -2px 0;
233
+ }
234
+
235
+
236
+ /* Add shadows to your tooltips in: FF3+, Chrome 2+, Opera 10.6+, IE9+, Safari 2+ */
237
+ .ui-tooltip-shadow{
238
+ -webkit-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15);
239
+ -moz-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15);
240
+ box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15);
241
+ }
242
+
243
+ /* Add rounded corners to your tooltips in: FF3+, Chrome 2+, Opera 10.6+, IE9+, Safari 2+ */
244
+ .ui-tooltip-rounded,
245
+ .ui-tooltip-tipsy,
246
+ .ui-tooltip-bootstrap{
247
+ -moz-border-radius: 5px;
248
+ -webkit-border-radius: 5px;
249
+ border-radius: 5px;
250
+ }
251
+
252
+ /* Youtube tooltip style */
253
+ .ui-tooltip-youtube{
254
+ -moz-border-radius: 2px;
255
+ -webkit-border-radius: 2px;
256
+ border-radius: 2px;
257
+
258
+ -webkit-box-shadow: 0 0 3px #333;
259
+ -moz-box-shadow: 0 0 3px #333;
260
+ box-shadow: 0 0 3px #333;
261
+
262
+ color: white;
263
+ border-width: 0;
264
+
265
+ background: #4A4A4A;
266
+ background-image: -webkit-gradient(linear,left top,left bottom,color-stop(0,#4A4A4A),color-stop(100%,black));
267
+ background-image: -webkit-linear-gradient(top,#4A4A4A 0,black 100%);
268
+ background-image: -moz-linear-gradient(top,#4A4A4A 0,black 100%);
269
+ background-image: -ms-linear-gradient(top,#4A4A4A 0,black 100%);
270
+ background-image: -o-linear-gradient(top,#4A4A4A 0,black 100%);
271
+ }
272
+
273
+ .ui-tooltip-youtube .ui-tooltip-titlebar{
274
+ background-color: #4A4A4A;
275
+ background-color: rgba(0,0,0,0);
276
+ }
277
+
278
+ .ui-tooltip-youtube .ui-tooltip-content{
279
+ padding: .75em;
280
+ font: 12px arial,sans-serif;
281
+
282
+ filter: progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#4a4a4a,EndColorStr=#000000);
283
+ -ms-filter: "progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#4a4a4a,EndColorStr=#000000);";
284
+ }
285
+
286
+ .ui-tooltip-youtube .ui-tooltip-icon{
287
+ border-color: #222;
288
+ }
289
+
290
+ .ui-tooltip-youtube .ui-tooltip-titlebar .ui-state-hover{
291
+ border-color: #303030;
292
+ }
293
+
294
+
295
+ /* jQuery TOOLS Tooltip style */
296
+ .ui-tooltip-jtools{
297
+ background: #232323;
298
+ background: rgba(0, 0, 0, 0.7);
299
+ background-image: -webkit-gradient(linear, left top, left bottom, from(#717171), to(#232323));
300
+ background-image: -moz-linear-gradient(top, #717171, #232323);
301
+ background-image: -webkit-linear-gradient(top, #717171, #232323);
302
+ background-image: -ms-linear-gradient(top, #717171, #232323);
303
+ background-image: -o-linear-gradient(top, #717171, #232323);
304
+
305
+ border: 2px solid #ddd;
306
+ border: 2px solid rgba(241,241,241,1);
307
+
308
+ -moz-border-radius: 2px;
309
+ -webkit-border-radius: 2px;
310
+ border-radius: 2px;
311
+
312
+ -webkit-box-shadow: 0 0 12px #333;
313
+ -moz-box-shadow: 0 0 12px #333;
314
+ box-shadow: 0 0 12px #333;
315
+ }
316
+
317
+ /* IE Specific */
318
+ .ui-tooltip-jtools .ui-tooltip-titlebar{
319
+ background-color: transparent;
320
+ filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A);
321
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A)";
322
+ }
323
+ .ui-tooltip-jtools .ui-tooltip-content{
324
+ filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323);
325
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323)";
326
+ }
327
+
328
+ .ui-tooltip-jtools .ui-tooltip-titlebar,
329
+ .ui-tooltip-jtools .ui-tooltip-content{
330
+ background: transparent;
331
+ color: white;
332
+ border: 0 dashed transparent;
333
+ }
334
+
335
+ .ui-tooltip-jtools .ui-tooltip-icon{
336
+ border-color: #555;
337
+ }
338
+
339
+ .ui-tooltip-jtools .ui-tooltip-titlebar .ui-state-hover{
340
+ border-color: #333;
341
+ }
342
+
343
+
344
+ /* Cluetip style */
345
+ .ui-tooltip-cluetip{
346
+ -webkit-box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4);
347
+ -moz-box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4);
348
+ box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4);
349
+
350
+ background-color: #D9D9C2;
351
+ color: #111;
352
+ border: 0 dashed transparent;
353
+ }
354
+
355
+ .ui-tooltip-cluetip .ui-tooltip-titlebar{
356
+ background-color: #87876A;
357
+ color: white;
358
+ border: 0 dashed transparent;
359
+ }
360
+
361
+ .ui-tooltip-cluetip .ui-tooltip-icon{
362
+ border-color: #808064;
363
+ }
364
+
365
+ .ui-tooltip-cluetip .ui-tooltip-titlebar .ui-state-hover{
366
+ border-color: #696952;
367
+ color: #696952;
368
+ }
369
+
370
+
371
+ /* Tipsy style */
372
+ .ui-tooltip-tipsy{
373
+ background: black;
374
+ background: rgba(0, 0, 0, .87);
375
+
376
+ color: white;
377
+ border: 0 solid transparent;
378
+
379
+ font-size: 11px;
380
+ font-family: 'Lucida Grande', sans-serif;
381
+ font-weight: bold;
382
+ line-height: 16px;
383
+ text-shadow: 0 1px black;
384
+ }
385
+
386
+ .ui-tooltip-tipsy .ui-tooltip-titlebar{
387
+ padding: 6px 35px 0 10;
388
+ background-color: transparent;
389
+ }
390
+
391
+ .ui-tooltip-tipsy .ui-tooltip-content{
392
+ padding: 6px 10;
393
+ }
394
+
395
+ .ui-tooltip-tipsy .ui-tooltip-icon{
396
+ border-color: #222;
397
+ text-shadow: none;
398
+ }
399
+
400
+ .ui-tooltip-tipsy .ui-tooltip-titlebar .ui-state-hover{
401
+ border-color: #303030;
402
+ }
403
+
404
+
405
+ /* Tipped style */
406
+ .ui-tooltip-tipped{
407
+ border: 3px solid #959FA9;
408
+
409
+ -moz-border-radius: 3px;
410
+ -webkit-border-radius: 3px;
411
+ border-radius: 3px;
412
+
413
+ background-color: #F9F9F9;
414
+ color: #454545;
415
+
416
+ font-weight: normal;
417
+ font-family: serif;
418
+ }
419
+
420
+ .ui-tooltip-tipped .ui-tooltip-titlebar{
421
+ border-bottom-width: 0;
422
+
423
+ color: white;
424
+ background: #3A79B8;
425
+ background-image: -webkit-gradient(linear, left top, left bottom, from(#3A79B8), to(#2E629D));
426
+ background-image: -webkit-linear-gradient(top, #3A79B8, #2E629D);
427
+ background-image: -moz-linear-gradient(top, #3A79B8, #2E629D);
428
+ background-image: -ms-linear-gradient(top, #3A79B8, #2E629D);
429
+ background-image: -o-linear-gradient(top, #3A79B8, #2E629D);
430
+ filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D);
431
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D)";
432
+ }
433
+
434
+ .ui-tooltip-tipped .ui-tooltip-icon{
435
+ border: 2px solid #285589;
436
+ background: #285589;
437
+ }
438
+
439
+ .ui-tooltip-tipped .ui-tooltip-icon .ui-icon{
440
+ background-color: #FBFBFB;
441
+ color: #555;
442
+ }
443
+
444
+
445
+ /**
446
+ * Twitter Bootstrap style.
447
+ *
448
+ * Tested with IE 8, IE 9, Chrome 18, Firefox 9, Opera 11.
449
+ * Does not work with IE 7.
450
+ */
451
+ .ui-tooltip-bootstrap{
452
+ font-size: 13px;
453
+ line-height: 18px;
454
+
455
+ color: #333333;
456
+ background-color: #ffffff;
457
+
458
+
459
+ border: 1px solid #ccc;
460
+ border: 1px solid rgba(0, 0, 0, 0.2);
461
+
462
+ *border-right-width: 2px;
463
+ *border-bottom-width: 2px;
464
+
465
+ -webkit-border-radius: 5px;
466
+ -moz-border-radius: 5px;
467
+ border-radius: 5px;
468
+
469
+ -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
470
+ -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
471
+ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
472
+
473
+ -webkit-background-clip: padding-box;
474
+ -moz-background-clip: padding;
475
+ background-clip: padding-box;
476
+ }
477
+
478
+ .ui-tooltip-bootstrap .ui-tooltip-titlebar{
479
+ font-size: 18px;
480
+ line-height: 22px;
481
+
482
+ border-bottom: 1px solid #ccc;
483
+ background-color: transparent;
484
+ }
485
+
486
+ .ui-tooltip-bootstrap .ui-tooltip-titlebar .ui-state-default{
487
+ right: 9px; top: 49%;
488
+ border-style: none;
489
+ }
490
+
491
+ .ui-tooltip-bootstrap .ui-tooltip-icon{
492
+ background: white;
493
+ }
494
+
495
+ .ui-tooltip-bootstrap .ui-tooltip-icon .ui-icon{
496
+ width: auto;
497
+ height: auto;
498
+ float: right;
499
+ font-size: 20px;
500
+ font-weight: bold;
501
+ line-height: 18px;
502
+ color: #000000;
503
+ text-shadow: 0 1px 0 #ffffff;
504
+ opacity: 0.2;
505
+ filter: alpha(opacity=20);
506
+ }
507
+
508
+ .ui-tooltip-bootstrap .ui-tooltip-icon .ui-icon:hover{
509
+ color: #000000;
510
+ text-decoration: none;
511
+ cursor: pointer;
512
+ opacity: 0.4;
513
+ filter: alpha(opacity=40);
514
+ }
515
+
516
+
517
+ /* IE9 fix - removes all filters */
518
+ .ui-tooltip:not(.ie9haxors) div.ui-tooltip-content,
519
+ .ui-tooltip:not(.ie9haxors) div.ui-tooltip-titlebar{
520
+ filter: none;
521
+ -ms-filter: none;
522
+ }
523
+
524
+
525
+ /* Tips plugin */
526
+ .ui-tooltip .ui-tooltip-tip{
527
+ margin: 0 auto;
528
+ overflow: hidden;
529
+ z-index: 10;
530
+ }
531
+
532
+ .ui-tooltip .ui-tooltip-tip,
533
+ .ui-tooltip .ui-tooltip-tip .qtip-vml{
534
+ position: absolute;
535
+
536
+ line-height: 0.1px !important;
537
+ font-size: 0.1px !important;
538
+ color: #123456;
539
+
540
+ background: transparent;
541
+ border: 0 dashed transparent;
542
+ }
543
+
544
+ .ui-tooltip .ui-tooltip-tip canvas{ top: 0; left: 0; }
545
+
546
+ .ui-tooltip .ui-tooltip-tip .qtip-vml{
547
+ behavior: url(#default#VML);
548
+ display: inline-block;
549
+ visibility: visible;
550
+ }
551
+ /* Modal plugin */
552
+ #qtip-overlay{
553
+ position: fixed;
554
+ left: -10000em;
555
+ top: -10000em;
556
+ }
557
+
558
+ /* Applied to modals with show.modal.blur set to true */
559
+ #qtip-overlay.blurs{ cursor: pointer; }
560
+
561
+ /* Change opacity of overlay here */
562
+ #qtip-overlay div{
563
+ position: absolute;
564
+ left: 0; top: 0;
565
+ width: 100%; height: 100%;
566
+
567
+ background-color: black;
568
+
569
+ opacity: 0.7;
570
+ filter:alpha(opacity=70);
571
+ -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)";
572
+ }
573
+
css/jquery.qtip.min.css ADDED
@@ -0,0 +1 @@
 
1
+ /*! qTip2 v2.0.0 | http://craigsworks.com/projects/qtip2/ | Licensed MIT, GPL */#qtip-rcontainer{position:absolute;left:-28000px;top:-28000px;display:block;visibility:hidden}#qtip-rcontainer .ui-tooltip{display:block!important;visibility:hidden!important;position:static!important;float:left!important}.ui-tooltip,.qtip{position:absolute;left:-28000px;top:-28000px;display:none;max-width:280px;min-width:50px;font-size:10.5px;line-height:12px}.ui-tooltip-content{position:relative;padding:5px 9px;overflow:hidden;text-align:left;word-wrap:break-word}.ui-tooltip-titlebar{position:relative;min-height:14px;padding:5px 35px 5px 10px;overflow:hidden;border-width:0 0 1px;font-weight:700}.ui-tooltip-titlebar+.ui-tooltip-content{border-top-width:0!important}.ui-tooltip-titlebar .ui-state-default{position:absolute;right:4px;top:50%;margin-top:-9px;cursor:pointer;outline:medium none;border-width:1px;border-style:solid}* html .ui-tooltip-titlebar .ui-state-default{top:16px}.ui-tooltip-titlebar .ui-icon,.ui-tooltip-icon .ui-icon{display:block;text-indent:-1000em;direction:ltr}.ui-tooltip-icon,.ui-tooltip-icon .ui-icon{-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;text-decoration:none}.ui-tooltip-icon .ui-icon{width:18px;height:14px;text-align:center;text-indent:0;font:normal bold 10px/13px Tahoma,sans-serif;color:inherit;background:transparent none no-repeat -100em -100em}.ui-tooltip-focus{}.ui-tooltip-hover{}.ui-tooltip-default{border-width:1px;border-style:solid;border-color:#F1D031;background-color:#FFFFA3;color:#555}.ui-tooltip-default .ui-tooltip-titlebar{background-color:#FFEF93}.ui-tooltip-default .ui-tooltip-icon{border-color:#CCC;background:#F1F1F1;color:#777}.ui-tooltip-default .ui-tooltip-titlebar .ui-state-hover{border-color:#AAA;color:#111}/*! Light tooltip style */.ui-tooltip-light{background-color:#fff;border-color:#E2E2E2;color:#454545}.ui-tooltip-light .ui-tooltip-titlebar{background-color:#f1f1f1}/*! Dark tooltip style */.ui-tooltip-dark{background-color:#505050;border-color:#303030;color:#f3f3f3}.ui-tooltip-dark .ui-tooltip-titlebar{background-color:#404040}.ui-tooltip-dark .ui-tooltip-icon{border-color:#444}.ui-tooltip-dark .ui-tooltip-titlebar .ui-state-hover{border-color:#303030}/*! Cream tooltip style */.ui-tooltip-cream{background-color:#FBF7AA;border-color:#F9E98E;color:#A27D35}.ui-tooltip-cream .ui-tooltip-titlebar{background-color:#F0DE7D}.ui-tooltip-cream .ui-state-default .ui-tooltip-icon{background-position:-82px 0}/*! Red tooltip style */.ui-tooltip-red{background-color:#F78B83;border-color:#D95252;color:#912323}.ui-tooltip-red .ui-tooltip-titlebar{background-color:#F06D65}.ui-tooltip-red .ui-state-default .ui-tooltip-icon{background-position:-102px 0}.ui-tooltip-red .ui-tooltip-icon{border-color:#D95252}.ui-tooltip-red .ui-tooltip-titlebar .ui-state-hover{border-color:#D95252}/*! Green tooltip style */.ui-tooltip-green{background-color:#CAED9E;border-color:#90D93F;color:#3F6219}.ui-tooltip-green .ui-tooltip-titlebar{background-color:#B0DE78}.ui-tooltip-green .ui-state-default .ui-tooltip-icon{background-position:-42px 0}/*! Blue tooltip style */.ui-tooltip-blue{background-color:#E5F6FE;border-color:#ADD9ED;color:#5E99BD}.ui-tooltip-blue .ui-tooltip-titlebar{background-color:#D0E9F5}.ui-tooltip-blue .ui-state-default .ui-tooltip-icon{background-position:-2px 0}.ui-tooltip-shadow{-webkit-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15);-moz-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15);box-shadow:1px 1px 3px 1px rgba(0,0,0,.15)}.ui-tooltip-rounded,.ui-tooltip-tipsy,.ui-tooltip-bootstrap{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px}.ui-tooltip-youtube{-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 0 3px #333;-moz-box-shadow:0 0 3px #333;box-shadow:0 0 3px #333;color:#fff;border-width:0;background:#4A4A4A;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(0, #4A4A4A),color-stop(100%,black));background-image:-webkit-linear-gradient(top, #4A4A4A 0,black 100%);background-image:-moz-linear-gradient(top, #4A4A4A 0,black 100%);background-image:-ms-linear-gradient(top, #4A4A4A 0,black 100%);background-image:-o-linear-gradient(top, #4A4A4A 0,black 100%)}.ui-tooltip-youtube .ui-tooltip-titlebar{background-color:#4A4A4A;background-color:rgba(0,0,0,0)}.ui-tooltip-youtube .ui-tooltip-content{padding:.75em;font:12px arial,sans-serif;filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=0, StartColorStr=#4a4a4a, EndColorStr=#000000);-ms-filter:"progid:DXImageTransform.Microsoft.Gradient(GradientType=0, StartColorStr=#4a4a4a, EndColorStr=#000000);"}.ui-tooltip-youtube .ui-tooltip-icon{border-color:#222}.ui-tooltip-youtube .ui-tooltip-titlebar .ui-state-hover{border-color:#303030}.ui-tooltip-jtools{background:#232323;background:rgba(0,0,0,.7);background-image:-webkit-gradient(linear,left top,left bottom,from( #717171),to( #232323));background-image:-moz-linear-gradient(top, #717171, #232323);background-image:-webkit-linear-gradient(top, #717171, #232323);background-image:-ms-linear-gradient(top, #717171, #232323);background-image:-o-linear-gradient(top, #717171, #232323);border:2px solid #ddd;border:2px solid rgba(241,241,241,1);-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 0 12px #333;-moz-box-shadow:0 0 12px #333;box-shadow:0 0 12px #333}.ui-tooltip-jtools .ui-tooltip-titlebar{background-color:transparent;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171, endColorstr=#4A4A4A);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171, endColorstr=#4A4A4A)"}.ui-tooltip-jtools .ui-tooltip-content{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A, endColorstr=#232323);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A, endColorstr=#232323)"}.ui-tooltip-jtools .ui-tooltip-titlebar,.ui-tooltip-jtools .ui-tooltip-content{background:transparent;color:#fff;border:0 dashed transparent}.ui-tooltip-jtools .ui-tooltip-icon{border-color:#555}.ui-tooltip-jtools .ui-tooltip-titlebar .ui-state-hover{border-color:#333}.ui-tooltip-cluetip{-webkit-box-shadow:4px 4px 5px rgba(0,0,0,.4);-moz-box-shadow:4px 4px 5px rgba(0,0,0,.4);box-shadow:4px 4px 5px rgba(0,0,0,.4);background-color:#D9D9C2;color:#111;border:0 dashed transparent}.ui-tooltip-cluetip .ui-tooltip-titlebar{background-color:#87876A;color:#fff;border:0 dashed transparent}.ui-tooltip-cluetip .ui-tooltip-icon{border-color:#808064}.ui-tooltip-cluetip .ui-tooltip-titlebar .ui-state-hover{border-color:#696952;color:#696952}.ui-tooltip-tipsy{background:#000;background:rgba(0,0,0,.87);color:#fff;border:0 solid transparent;font-size:11px;font-family:'Lucida Grande',sans-serif;font-weight:700;line-height:16px;text-shadow:0 1px black}.ui-tooltip-tipsy .ui-tooltip-titlebar{padding:6px 35px 0 10;background-color:transparent}.ui-tooltip-tipsy .ui-tooltip-content{padding:6px 10}.ui-tooltip-tipsy .ui-tooltip-icon{border-color:#222;text-shadow:none}.ui-tooltip-tipsy .ui-tooltip-titlebar .ui-state-hover{border-color:#303030}.ui-tooltip-tipped{border:3px solid #959FA9;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;background-color:#F9F9F9;color:#454545;font-weight:400;font-family:serif}.ui-tooltip-tipped .ui-tooltip-titlebar{border-bottom-width:0;color:#fff;background:#3A79B8;background-image:-webkit-gradient(linear,left top,left bottom,from( #3A79B8),to( #2E629D));background-image:-webkit-linear-gradient(top, #3A79B8, #2E629D);background-image:-moz-linear-gradient(top, #3A79B8, #2E629D);background-image:-ms-linear-gradient(top, #3A79B8, #2E629D);background-image:-o-linear-gradient(top, #3A79B8, #2E629D);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8, endColorstr=#2E629D);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8, endColorstr=#2E629D)"}.ui-tooltip-tipped .ui-tooltip-icon{border:2px solid #285589;background:#285589}.ui-tooltip-tipped .ui-tooltip-icon .ui-icon{background-color:#FBFBFB;color:#555}.ui-tooltip-bootstrap{font-size:13px;line-height:18px;color:#333;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.ui-tooltip-bootstrap .ui-tooltip-titlebar{font-size:18px;line-height:22px;border-bottom:1px solid #ccc;background-color:transparent}.ui-tooltip-bootstrap .ui-tooltip-titlebar .ui-state-default{right:9px;top:49%;border-style:none}.ui-tooltip-bootstrap .ui-tooltip-icon{background:#fff}.ui-tooltip-bootstrap .ui-tooltip-icon .ui-icon{width:auto;height:auto;float:right;font-size:20px;font-weight:700;line-height:18px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.ui-tooltip-bootstrap .ui-tooltip-icon .ui-icon:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}.ui-tooltip:not(.ie9haxors) div.ui-tooltip-content,.ui-tooltip:not(.ie9haxors) div.ui-tooltip-titlebar{filter:none;-ms-filter:none}.ui-tooltip .ui-tooltip-tip{margin:0 auto;overflow:hidden;z-index:10}.ui-tooltip .ui-tooltip-tip,.ui-tooltip .ui-tooltip-tip .qtip-vml{position:absolute;line-height:.1px!important;font-size:.1px!important;color:#123456;background:transparent;border:0 dashed transparent}.ui-tooltip .ui-tooltip-tip canvas{top:0;left:0}.ui-tooltip .ui-tooltip-tip .qtip-vml{behavior:url(#default#VML);display:inline-block;visibility:visible}#qtip-overlay{position:fixed;left:-10000em;top:-10000em}#qtip-overlay.blurs{cursor:pointer}#qtip-overlay div{position:absolute;left:0;top:0;width:100%;height:100%;background-color:#000;opacity:.7;filter:alpha(opacity=70);-ms-filter:"alpha(Opacity=70)"}
css/menu-editor.css CHANGED
@@ -1,4 +1,8 @@
1
- /* Admin Menu Editor CSS file */
 
 
 
 
2
 
3
  .ws_main_container {
4
  margin: 2px;
@@ -17,7 +21,12 @@
17
  width: 100%;
18
  margin: 0;
19
  padding-top: 2px;
20
- padding-bottom: 4px;
 
 
 
 
 
21
  }
22
 
23
  #ws_menu_box {
@@ -26,6 +35,60 @@
26
  #ws_submenu_box {
27
  }
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  /*
30
  * The sidebar
31
  */
@@ -35,18 +98,18 @@
35
  padding: 2px;
36
  }
37
 
38
- #ws_editor_sidebar .ws_main_button {
39
  clear: both;
40
  display: block;
41
  margin: 4px;
42
  width: 120px;
43
  }
44
 
45
- #ws_editor_sidebar #ws_save_menu {
46
  margin-bottom: 20px;
47
  }
48
 
49
- #ws_editor_sidebar #ws_export_menu {
50
  margin-top: 12px;
51
  }
52
 
@@ -59,28 +122,15 @@
59
  width: 290px;
60
 
61
  padding : 3px;
62
- margin: 2px;
63
- margin-left: auto;
64
- margin-right: auto;
65
-
66
- border: 1px solid #a9badb;
67
- background-color: #bdd3ff;
68
  }
69
 
70
- .ws_active {
71
- background-color : #8eb0f1 !important; /* make sure this overrides ws_menu_separator */
72
- }
73
 
74
  .ws_menu { }
75
  .ws_item { }
76
 
77
- .ws_menu_separator {
78
- background-image: url("../images/menu-arrows.png");
79
- background-repeat: no-repeat;
80
- background-position : 4px 8px;
81
- background-color: #F9F9F9;
82
- border-color: #d9d9d9;
83
- }
84
 
85
  .ws_submenu {
86
  min-height: 2em;
@@ -92,56 +142,25 @@
92
  }
93
 
94
  .ws_item_title {
95
- display: block;
96
  padding: 2px;
97
  cursor: default;
98
  }
99
 
100
  .ws_edit_link {
101
  float: right;
102
- margin-right: 0px;
103
  cursor: pointer;
104
  display:block;
105
  width: 40px;
106
  height: 22px;
107
 
108
- background-image: url('../images/bullet_arrow_down2.png');
109
- background-repeat: no-repeat;
110
- background-position: center;
111
-
112
  border-radius: 3px;
113
  -moz-border-radius: 3px;
114
  -webkit-border-radius: 3px;
115
  }
116
 
117
- a.ws_edit_link:hover {
118
- background-color: #ffffd0;
119
- background-image: url('../images/bullet_arrow_down2.png');
120
- }
121
-
122
- .ws_edit_link:active {
123
- background-repeat: no-repeat;
124
- background-position: center;
125
- }
126
-
127
- .ws_edit_link_expanded {
128
- background-color: #ffffd0;
129
- border-bottom: none;
130
- border-color: #ffffd0;
131
-
132
- background-image: url('../images/bullet_arrow_down2.png');
133
- padding-bottom: 1px;
134
- background-position: center 3px;
135
-
136
- border-bottom-right-radius: 0;
137
- border-bottom-left-radius: 0;
138
-
139
- -moz-border-radius-bottomright: 0;
140
- -moz-border-radius-bottomleft: 0;
141
-
142
- -webkit-border-bottom-right-radius: 0;
143
- -webkit-border-bottom-left-radius: 0;
144
- }
145
 
146
 
147
  .ws_menu_drop_hover {
@@ -152,23 +171,51 @@ a.ws_edit_link:hover {
152
  cursor: move !important;
153
  }
154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  /****************************************
156
  Per-menu settings fields & panels
157
  *****************************************/
158
 
159
  .ws_editbox {
160
  display: block;
161
- background-color: #ffffd0;
162
  padding: 4px;
163
-
164
  border-radius: 2px;
165
- border-top-right-radius: 0px;
166
 
167
  -moz-border-radius: 2px;
168
- -moz-border-radius-topright: 0px;
169
 
170
  -webkit-border-radius: 2px;
171
- -webkit-border-top-right-radius: 0px;
172
  }
173
 
174
  .ws_edit_panel {
@@ -199,10 +246,8 @@ a.ws_edit_link:hover {
199
  width: 16px;
200
  height: 16px;
201
  vertical-align: top;
202
-
203
- background-image: url("../images/pencil_delete_gray.png");
204
- background-repeat: no-repeat;
205
- background-position: center;
206
  }
207
 
208
  .ws_reset_button:hover {
@@ -213,8 +258,9 @@ a.ws_edit_link:hover {
213
  color: gray;
214
  }
215
 
216
- /* No reset button for fields set to the default value */
217
- .ws_input_default .ws_reset_button {
 
218
  visibility: hidden;
219
  }
220
 
@@ -235,16 +281,15 @@ a.ws_edit_link:hover {
235
  }
236
 
237
  #ws_menu_editor .ws_edit_field-custom input[type="checkbox"] {
238
- margin-top: 0px;
239
  }
240
 
241
  /* Dropdown button for combo-box fields */
242
- #ws_menu_editor .ws_dropdown_button {
243
- display : block;
244
- float: left;
245
-
246
  width: 20px;
247
- height: 22px;
248
 
249
  margin: 1px 1px 1px 0;
250
  padding: 0;
@@ -257,37 +302,57 @@ a.ws_edit_link:hover {
257
 
258
  border-top-right-radius: 3px;
259
  border-bottom-right-radius: 3px;
260
- border-top-left-radius: 0px;
261
- border-bottom-left-radius: 0px;
262
 
263
  -moz-border-radius-topright: 3px;
264
  -moz-border-radius-bottomright: 3px;
265
- -moz-border-radius-topleft: 0px;
266
- -moz-border-radius-bottomleft: 0px;
267
 
268
  -webkit-border-top-right-radius: 3px;
269
  -webkit-border-bottom-right-radius: 3px;
270
- -webkit-border-top-left-radius: 0px;
271
- -webkit-border-bottom-left-radius: 0px;
 
 
 
 
 
 
 
 
 
 
 
272
  }
273
 
274
  /*
275
- The appearance and size of combobox fields need to be changed
276
- to accomodate the dropdown button.
277
  */
278
- #ws_menu_editor .ws_has_dropdown input.ws_field_value {
279
- width: 230px;
 
280
  margin-right: 0;
281
  border-right: 0;
282
 
283
- border-top-right-radius: 0px;
284
- border-bottom-right-radius: 0px;
 
 
 
 
 
 
 
285
 
286
- -moz-border-radius-topright: 0px;
287
- -moz-border-radius-bottomright: 0px;
 
288
 
289
- -webkit-border-top-right-radius: 0px;
290
- -webkit-border-bottom-right-radius: 0px;
291
  }
292
 
293
  /* Unlike others, this field is just a single checkbox, so it has a smaller height */
@@ -338,15 +403,10 @@ to accomodate the dropdown button.
338
  }
339
 
340
  /* user-created items */
341
- .ws_custom_item_flag {
342
  background-image: url('../images/page-add.png');
343
  }
344
 
345
- /* items not present in the default menu */
346
- .ws_missing_flag {
347
- background-image: url('../images/plugin_error.png');
348
- }
349
-
350
  /* unused items - those that are in the default menu but not in the custom one */
351
  .ws_unused_flag {
352
  background-image: url('../images/plugin_add.png');
@@ -358,8 +418,7 @@ to accomodate the dropdown button.
358
  }
359
 
360
  /* These classes could be used to apply different styles to items depending on their flags */
361
- .ws_missing { }
362
- .ws_custom_item { }
363
  .ws_hidden { }
364
  .ws_unused { }
365
 
@@ -383,11 +442,10 @@ to accomodate the dropdown button.
383
  display: block;
384
  margin-right: 3px;
385
  padding: 4px;
386
- border: 1px solid #c0c0e0;
387
  float: left;
388
 
389
- width: 16px;
390
- height: 16px;
391
 
392
  border-radius: 3px;
393
  -moz-border-radius: 3px;
@@ -408,7 +466,7 @@ a.ws_button:hover {
408
  Capability selector
409
  *************************************/
410
 
411
- #wpbody select.ws_dropdown {
412
  width: 252px;
413
  height: 20em;
414
 
@@ -420,13 +478,13 @@ a.ws_button:hover {
420
  font-size: 12px;
421
  }
422
 
423
- #wpbody select.ws_dropdown option {
424
  font-family : "Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif;
425
  font-size: 12px;
426
  padding: 3px;
427
  }
428
 
429
- #wpbody select.ws_dropdown optgroup option {
430
  padding-left: 10px;
431
  }
432
 
@@ -531,12 +589,12 @@ a.ws_button:hover {
531
 
532
  /* MP6 admin style compatibility */
533
  #ws_icon_selector .ws_icon_option .icon16::before {
534
- margin: 0;
535
- padding: 0;
536
  }
537
  .ws_select_icon .icon16::before {
538
- padding: 0;
539
- margin: 1px 0 0 2px;
540
  }
541
 
542
  #ws_choose_icon_from_media {
@@ -554,8 +612,8 @@ a.ws_button:hover {
554
  .ui-widget-overlay {
555
  background-color: black;
556
  position: absolute;
557
- left: 0px;
558
- top: 0px;
559
  opacity: 0.70;
560
  -moz-opacity: 0.70;
561
  filter: alpha(opacity=70);
@@ -593,11 +651,7 @@ a.ws_button:hover {
593
  }
594
 
595
  .ui-dialog-titlebar-close {
596
- background-image: url(../images/x.png);
597
- background-repeat: no-repeat;
598
- background-position: center;
599
- background-color: #86A7E3;
600
-
601
  width: 22px;
602
  height: 22px;
603
  display: block;
@@ -623,37 +677,35 @@ a.ws_button:hover {
623
  font-size: 1.1em;
624
  }
625
 
626
- .ws_dialog_panel {
627
- height: 84px;
628
  }
629
 
630
- #export_dialog .ws_dialog_panel {
631
- height: 70px;
632
  }
633
 
634
  .ws_dialog_buttons {
635
  height: 23px;
636
  text-align: right;
 
637
  }
638
 
639
  .ws_dialog_buttons .button-primary {
640
  display: block;
641
  float: left;
642
- margin-top: 0px;
643
  }
644
 
645
  .ws_dialog_buttons .button {
646
- margin-top: 0px;
647
  }
648
 
649
  #import_file_selector {
650
  display: block;
651
  width: 286px;
652
-
653
- margin-top: 6px;
654
- margin-bottom: 12px;
655
- margin-left: auto;
656
- margin-right: auto;
657
  }
658
 
659
  #ws_start_import {
@@ -666,6 +718,117 @@ a.ws_button:hover {
666
  padding-top: 25px;
667
  }
668
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
  /************************************
670
  Tooltips and hints
671
  *************************************/
@@ -702,8 +865,8 @@ a.ws_button:hover {
702
  border-radius: 3px;
703
 
704
  position: absolute;
705
- right: 0px;
706
- top: 0px;
707
  }
708
 
709
  .ws_hint_close:hover {
@@ -771,8 +934,7 @@ a.ws_button:hover {
771
 
772
  /* "Upgrade to Pro" */
773
  #ws-pro-version-notice {
774
- background-color: #00C31F;
775
- background-image: none;
776
  }
777
 
778
 
1
+ /* Admin Menu Editor CSS file */
2
+
3
+ #ws_menu_editor {
4
+ min-width: 780px;
5
+ }
6
 
7
  .ws_main_container {
8
  margin: 2px;
21
  width: 100%;
22
  margin: 0;
23
  padding-top: 2px;
24
+ padding-bottom: 2px;
25
+ }
26
+
27
+ .ws_basic_container {
28
+ float: left;
29
+ display:block;
30
  }
31
 
32
  #ws_menu_box {
35
  #ws_submenu_box {
36
  }
37
 
38
+ .ws_dropzone {
39
+ margin: 0 5px 2px 5px;
40
+ border: none;
41
+ height: 25px;
42
+ }
43
+
44
+ .ws_dropzone_hover {
45
+ border: 1px dotted silver;
46
+ background: yellow;
47
+ height: 30px;
48
+ }
49
+
50
+ /*************************************************
51
+ Actor UI
52
+ *************************************************/
53
+ #ws_actor_selector li:after {
54
+ content: ' | ';
55
+ }
56
+
57
+ #ws_actor_selector li:last-child:after {
58
+ content: '';
59
+ }
60
+
61
+ /**
62
+ * The checkbox that lets the user show/hide a menu for the currently selected actor.
63
+ */
64
+ #ws_menu_editor .ws_actor_access_checkbox,
65
+ #ws_menu_editor input[type="checkbox"].ws_actor_access_checkbox /* Ensure we override WP defaults. */
66
+ {
67
+ margin-right: 2px;
68
+ margin-left: 2px;
69
+ margin-top: 1px;
70
+ vertical-align: text-top;
71
+ }
72
+
73
+ /* The checkbox is only visible when viewing the menu configuration for a specific actor. */
74
+ #ws_menu_editor .ws_actor_access_checkbox {
75
+ display: none;
76
+ }
77
+
78
+ #ws_menu_editor.ws_is_actor_view .ws_actor_access_checkbox {
79
+ display: inline-block;
80
+ }
81
+
82
+ /* Gray-out items inaccessible to the currently selected actor */
83
+
84
+ .ws_is_actor_view .ws_container.ws_is_hidden_for_actor {
85
+ background-color: #F9F9F9;
86
+ }
87
+
88
+ .ws_is_actor_view .ws_is_hidden_for_actor .ws_item_title {
89
+ color: #777;
90
+ }
91
+
92
  /*
93
  * The sidebar
94
  */
98
  padding: 2px;
99
  }
100
 
101
+ #ws_menu_editor .ws_main_button {
102
  clear: both;
103
  display: block;
104
  margin: 4px;
105
  width: 120px;
106
  }
107
 
108
+ #ws_menu_editor #ws_save_menu {
109
  margin-bottom: 20px;
110
  }
111
 
112
+ #ws_menu_editor #ws_export_menu {
113
  margin-top: 12px;
114
  }
115
 
122
  width: 290px;
123
 
124
  padding : 3px;
125
+ margin: 2px auto;
 
 
 
 
 
126
  }
127
 
128
+ .ws_active { }
 
 
129
 
130
  .ws_menu { }
131
  .ws_item { }
132
 
133
+ .ws_menu_separator { }
 
 
 
 
 
 
134
 
135
  .ws_submenu {
136
  min-height: 2em;
142
  }
143
 
144
  .ws_item_title {
145
+ display: inline-block;
146
  padding: 2px;
147
  cursor: default;
148
  }
149
 
150
  .ws_edit_link {
151
  float: right;
152
+ margin-right: 0;
153
  cursor: pointer;
154
  display:block;
155
  width: 40px;
156
  height: 22px;
157
 
 
 
 
 
158
  border-radius: 3px;
159
  -moz-border-radius: 3px;
160
  -webkit-border-radius: 3px;
161
  }
162
 
163
+ .ws_edit_link_expanded { }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
 
166
  .ws_menu_drop_hover {
171
  cursor: move !important;
172
  }
173
 
174
+ /*
175
+ If you ever want to apply a right-arrow style to the currently selected menu item,
176
+ you can do it like this. Commented out for now since it doesn't look all that great,
177
+ but might be useful in the future.
178
+ */
179
+ /*
180
+ .ws_container {
181
+ position: relative;
182
+ }
183
+
184
+ .ws_menu.ws_active::after {
185
+ content: "";
186
+ display: block;
187
+ z-index: 1002;
188
+
189
+ border-left: 14px solid #8EB0F1;
190
+ border-top: 14px solid transparent;
191
+ border-bottom: 14px solid transparent;
192
+ background: #8EB0F1;
193
+
194
+ position: absolute;
195
+ right: -14px;
196
+ top: -1px;
197
+
198
+ width: 0;
199
+ height: 0;
200
+ }
201
+ */
202
+
203
  /****************************************
204
  Per-menu settings fields & panels
205
  *****************************************/
206
 
207
  .ws_editbox {
208
  display: block;
 
209
  padding: 4px;
210
+
211
  border-radius: 2px;
212
+ border-top-right-radius: 0;
213
 
214
  -moz-border-radius: 2px;
215
+ -moz-border-radius-topright: 0;
216
 
217
  -webkit-border-radius: 2px;
218
+ -webkit-border-top-right-radius: 0;
219
  }
220
 
221
  .ws_edit_panel {
246
  width: 16px;
247
  height: 16px;
248
  vertical-align: top;
249
+
250
+ background: url("../images/pencil_delete_gray.png") no-repeat center;
 
 
251
  }
252
 
253
  .ws_reset_button:hover {
258
  color: gray;
259
  }
260
 
261
+ /* No reset button for fields set to the default value and fields without a default value */
262
+ .ws_input_default .ws_reset_button,
263
+ .ws_has_no_default .ws_reset_button {
264
  visibility: hidden;
265
  }
266
 
281
  }
282
 
283
  #ws_menu_editor .ws_edit_field-custom input[type="checkbox"] {
284
+ margin-top: 0;
285
  }
286
 
287
  /* Dropdown button for combo-box fields */
288
+ #ws_menu_editor .ws_dropdown_button,
289
+ #ws_menu_access_editor .ws_dropdown_button
290
+ {
 
291
  width: 20px;
292
+ height: 20px;
293
 
294
  margin: 1px 1px 1px 0;
295
  padding: 0;
302
 
303
  border-top-right-radius: 3px;
304
  border-bottom-right-radius: 3px;
305
+ border-top-left-radius: 0;
306
+ border-bottom-left-radius: 0;
307
 
308
  -moz-border-radius-topright: 3px;
309
  -moz-border-radius-bottomright: 3px;
310
+ -moz-border-radius-topleft: 0;
311
+ -moz-border-radius-bottomleft: 0;
312
 
313
  -webkit-border-top-right-radius: 3px;
314
  -webkit-border-bottom-right-radius: 3px;
315
+ -webkit-border-top-left-radius: 0;
316
+ -webkit-border-bottom-left-radius: 0;
317
+ }
318
+
319
+ #ws_menu_access_editor .ws_dropdown_button {
320
+ display: inline-block;
321
+ height: 22px;
322
+ margin-bottom: 2px;
323
+ }
324
+
325
+ #ws_menu_editor .ws_dropdown_button {
326
+ display: block;
327
+ float: left;
328
  }
329
 
330
  /*
331
+ The appearance and size of combo-box fields need to be changed
332
+ to accommodate the drop-down button.
333
  */
334
+ #ws_menu_editor .ws_has_dropdown input.ws_field_value,
335
+ #ws_menu_access_editor input.ws_has_dropdown
336
+ {
337
  margin-right: 0;
338
  border-right: 0;
339
 
340
+ border-top-right-radius: 0;
341
+ border-bottom-right-radius: 0;
342
+
343
+ -moz-border-radius-topright: 0;
344
+ -moz-border-radius-bottomright: 0;
345
+
346
+ -webkit-border-top-right-radius: 0;
347
+ -webkit-border-bottom-right-radius: 0;
348
+ }
349
 
350
+ #ws_menu_access_editor input.ws_has_dropdown {
351
+ width: 90%;
352
+ }
353
 
354
+ #ws_menu_editor .ws_has_dropdown input.ws_field_value {
355
+ width: 230px;
356
  }
357
 
358
  /* Unlike others, this field is just a single checkbox, so it has a smaller height */
403
  }
404
 
405
  /* user-created items */
406
+ .ws_custom_flag {
407
  background-image: url('../images/page-add.png');
408
  }
409
 
 
 
 
 
 
410
  /* unused items - those that are in the default menu but not in the custom one */
411
  .ws_unused_flag {
412
  background-image: url('../images/plugin_add.png');
418
  }
419
 
420
  /* These classes could be used to apply different styles to items depending on their flags */
421
+ .ws_custom { }
 
422
  .ws_hidden { }
423
  .ws_unused { }
424
 
442
  display: block;
443
  margin-right: 3px;
444
  padding: 4px;
 
445
  float: left;
446
 
447
+ width: 16px;
448
+ height: 16px;
449
 
450
  border-radius: 3px;
451
  -moz-border-radius: 3px;
466
  Capability selector
467
  *************************************/
468
 
469
+ select.ws_dropdown {
470
  width: 252px;
471
  height: 20em;
472
 
478
  font-size: 12px;
479
  }
480
 
481
+ select.ws_dropdown option {
482
  font-family : "Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif;
483
  font-size: 12px;
484
  padding: 3px;
485
  }
486
 
487
+ select.ws_dropdown optgroup option {
488
  padding-left: 10px;
489
  }
490
 
589
 
590
  /* MP6 admin style compatibility */
591
  #ws_icon_selector .ws_icon_option .icon16::before {
592
+ margin: 0;
593
+ padding: 0;
594
  }
595
  .ws_select_icon .icon16::before {
596
+ padding: 0;
597
+ margin: 1px 0 0 2px;
598
  }
599
 
600
  #ws_choose_icon_from_media {
612
  .ui-widget-overlay {
613
  background-color: black;
614
  position: absolute;
615
+ left: 0;
616
+ top: 0;
617
  opacity: 0.70;
618
  -moz-opacity: 0.70;
619
  filter: alpha(opacity=70);
651
  }
652
 
653
  .ui-dialog-titlebar-close {
654
+ background: #86A7E3 url(../images/x.png) no-repeat center;
 
 
 
 
655
  width: 22px;
656
  height: 22px;
657
  display: block;
677
  font-size: 1.1em;
678
  }
679
 
680
+ #export_dialog .ws_dialog_panel {
681
+ height: 50px;
682
  }
683
 
684
+ #import_dialog .ws_dialog_panel {
685
+ height: 64px;
686
  }
687
 
688
  .ws_dialog_buttons {
689
  height: 23px;
690
  text-align: right;
691
+ margin-top: 20px;
692
  }
693
 
694
  .ws_dialog_buttons .button-primary {
695
  display: block;
696
  float: left;
697
+ margin-top: 0;
698
  }
699
 
700
  .ws_dialog_buttons .button {
701
+ margin-top: 0;
702
  }
703
 
704
  #import_file_selector {
705
  display: block;
706
  width: 286px;
707
+
708
+ margin: 6px auto 12px;
 
 
 
709
  }
710
 
711
  #ws_start_import {
718
  padding-top: 25px;
719
  }
720
 
721
+ /************************************
722
+ Menu access editor
723
+ *************************************/
724
+
725
+ /* The launch button */
726
+ #ws_menu_editor .ws_edit_field-access_level input.ws_field_value
727
+ {
728
+ width: 190px;
729
+ margin-right: 5px;
730
+ }
731
+
732
+ .ws_launch_access_editor {
733
+ min-width: 40px;
734
+ }
735
+
736
+ #ws_menu_access_editor {
737
+ width: 400px;
738
+ display: none;
739
+ }
740
+
741
+ .ws_dialog_subpanel {
742
+ margin-bottom: 1em;
743
+ }
744
+
745
+ #ws_menu_access_editor .ws_column_access {
746
+ text-align: center;
747
+ width: 5em;
748
+ }
749
+
750
+ #ws_role_table_body_container {
751
+ max-height: 400px;
752
+ overflow: auto;
753
+ }
754
+
755
+ .ws_role_table_body {
756
+ margin-top: 2px;
757
+ }
758
+
759
+ .ws_has_separate_header .ws_role_table_header {
760
+ border-bottom: none;
761
+
762
+ -moz-border-radius-bottomleft: 0;
763
+ -moz-border-radius-bottomright: 0;
764
+ -webkit-border-bottom-left-radius: 0;
765
+ -webkit-border-bottom-right-radius: 0;
766
+ border-bottom-left-radius: 0;
767
+ border-bottom-right-radius: 0;
768
+ }
769
+
770
+ .ws_has_separate_header .ws_role_table_body {
771
+ border-top: none;
772
+ margin-top: 0;
773
+
774
+ -moz-border-radius-topleft: 0;
775
+ -moz-border-radius-topright: 0;
776
+ -webkit-border-top-left-radius: 0;
777
+ -webkit-border-top-right-radius: 0;
778
+ border-top-left-radius: 0;
779
+ border-top-right-radius: 0;
780
+ }
781
+
782
+ .ws_role_id {
783
+ display: none;
784
+ }
785
+
786
+ #ws_extra_capability {
787
+ width: 100%;
788
+ }
789
+
790
+ #ws_role_access_container {
791
+ position: relative;
792
+ }
793
+
794
+ #ws_role_access_overlay {
795
+ width: 100%;
796
+ height: 100%;
797
+ position: absolute;
798
+
799
+ line-height: 100%;
800
+
801
+ background: white;
802
+ filter: alpha(opacity=60);
803
+ opacity: 0.6;
804
+ -moz-opacity:0.6;
805
+ -khtml-opacity: 0.6;
806
+ }
807
+
808
+ #ws_role_access_overlay_content {
809
+ position: absolute;
810
+ width: 50%;
811
+ left: 22%;
812
+ top: 30%;
813
+
814
+ background: white;
815
+ padding: 8px;
816
+
817
+ border: 2px solid silver;
818
+ border-radius: 5px;
819
+ color: #555;
820
+ }
821
+
822
+ #ws_menu_access_editor div.error {
823
+ margin-left: 0;
824
+ margin-right: 0;
825
+ margin-bottom: 5px;
826
+ }
827
+
828
+ #ws_hardcoded_role_error {
829
+ display: none;
830
+ }
831
+
832
  /************************************
833
  Tooltips and hints
834
  *************************************/
865
  border-radius: 3px;
866
 
867
  position: absolute;
868
+ right: 0;
869
+ top: 0;
870
  }
871
 
872
  .ws_hint_close:hover {
934
 
935
  /* "Upgrade to Pro" */
936
  #ws-pro-version-notice {
937
+ background: #00C31F none;
 
938
  }
939
 
940
 
css/style-classic.css ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .ws_container {
2
+ border: 1px solid #a9badb;
3
+ background-color: #bdd3ff;
4
+ }
5
+
6
+ #ws_menu_editor .ws_active {
7
+ background-color : #8eb0f1; /* make sure this overrides ws_menu_separator */
8
+ }
9
+
10
+ #ws_menu_editor.ws_is_actor_view .ws_is_hidden_for_actor.ws_active {
11
+ background-color : #dadee6;
12
+ }
13
+
14
+ .ws_menu_separator {
15
+ background: #F9F9F9 url("../images/menu-arrows.png") no-repeat 4px 8px;
16
+ border-color: #d9d9d9;
17
+ }
18
+
19
+ .ws_edit_link {
20
+ background-image: url('../images/bullet_arrow_down2.png');
21
+ background-repeat: no-repeat;
22
+ background-position: center;
23
+ }
24
+
25
+ a.ws_edit_link:hover {
26
+ background-color: #ffffd0;
27
+ background-image: url('../images/bullet_arrow_down2.png');
28
+ }
29
+
30
+ .ws_edit_link:active {
31
+ background-repeat: no-repeat;
32
+ background-position: center;
33
+ }
34
+
35
+ .ws_edit_link_expanded {
36
+ background: #ffffd0 url('../images/bullet_arrow_down2.png') center 3px no-repeat;
37
+ border-bottom: none;
38
+ border-color: #ffffd0;
39
+
40
+ padding-bottom: 1px;
41
+ border-bottom-right-radius: 0;
42
+ border-bottom-left-radius: 0;
43
+
44
+ -moz-border-radius-bottomright: 0;
45
+ -moz-border-radius-bottomleft: 0;
46
+
47
+ -webkit-border-bottom-right-radius: 0;
48
+ -webkit-border-bottom-left-radius: 0;
49
+ }
50
+
51
+
52
+ .ws_editbox {
53
+ background-color: #ffffd0;
54
+ }
55
+
56
+ .ws_input_default input, .ws_input_default select {
57
+ color: gray;
58
+ }
59
+
60
+ /*
61
+ * Show/Hide advanced fields
62
+ */
63
+
64
+ .ws_toggle_advanced_fields {
65
+ color: #6087CB;
66
+ font-size: 0.85em;
67
+ }
68
+
69
+ .ws_toggle_advanced_fields:visited, .ws_toggle_advanced_fields:active {
70
+ color: #6087CB;
71
+ }
72
+
73
+ .ws_toggle_advanced_fields:hover {
74
+ color: #d54e21;
75
+ text-decoration: underline;
76
+ }
77
+
78
+
79
+ /*
80
+ * Toolbars
81
+ */
82
+
83
+ .ws_button {
84
+ border: 1px solid #c0c0e0;
85
+ }
86
+
87
+ a.ws_button:hover {
88
+ background-color: #d0e0ff;
89
+ border-color: #9090c0;
90
+ }
91
+
92
+ /************************************
93
+ Export and import
94
+ *************************************/
95
+
96
+ .ui-dialog {
97
+ background: white;
98
+ border: 1px solid #c0c0c0;
99
+ }
100
+
101
+ .ui-dialog-titlebar {
102
+ background-color: #86A7E3;
103
+ }
104
+
105
+ .ui-dialog-title {
106
+ color: white;
107
+ }
108
+
109
+ .ui-dialog-titlebar-close {
110
+ background: #86A7E3 url(../images/x.png) no-repeat center;
111
+ color: white;
112
+ }
113
+
114
+ .ui-dialog-titlebar-close:hover {
115
+ /*background-image: url(../images/x-light.png);*/
116
+ background-color: #a6c2f5;
117
+ }
css/style-wp-gray.css ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .ws_container {
2
+ padding: 0;
3
+ width: 296px;
4
+ margin-bottom: 5px;
5
+
6
+ background: white;
7
+
8
+ border: 1px solid #aeaeae;
9
+
10
+ -webkit-box-shadow: inset 0 1px 0 #fff;
11
+ box-shadow: inset 0 1px 0 #fff;
12
+
13
+ -webkit-border-radius: 3px;
14
+ border-radius: 3px;
15
+ }
16
+
17
+ /**
18
+ * Item head elements
19
+ */
20
+
21
+ .ws_item_head {
22
+ padding: 0;
23
+ border-top-left-radius: 3px;
24
+ border-top-right-radius: 3px;
25
+
26
+ background-color: #d9d9d9;
27
+ background-image: -o-linear-gradient(top, #efefef, #d9d9d9);
28
+ background-image: -o-linear-gradient(top, #e9e9e9, #d9d9d9);
29
+ background-image: -ms-linear-gradient(top, #e9e9e9, #d9d9d9);
30
+ background-image: -moz-linear-gradient(top, #e9e9e9, #d9d9d9);
31
+ background-image: -webkit-gradient(linear, left top, left bottom, from(#e9e9e9), to(#d9d9d9));
32
+ background-image: -webkit-linear-gradient(top, #e9e9e9, #d9d9d9);
33
+ background-image: linear-gradient(top, #e9e9e9, #d9d9d9);
34
+ /*background-color: #c7c7c7;*/
35
+ }
36
+
37
+ .ws_item_title {
38
+ padding: 6px 5px;
39
+
40
+ /*font-weight: bold;*/
41
+ color: #222;
42
+ text-shadow: #FFFFFF 0px 1px 0px;
43
+ }
44
+
45
+ .ws_flag_container {
46
+ padding-top: 6px;
47
+ }
48
+
49
+ /**
50
+ * The down-arrow that expands menu settings
51
+ */
52
+
53
+ .ws_edit_link {
54
+ width: 30px;
55
+ height: 30px;
56
+
57
+ background: transparent url(../images/arrows.png) no-repeat center 5px;
58
+ overflow: hidden;
59
+ text-indent:-999em;
60
+ }
61
+
62
+ a.ws_edit_link:hover {
63
+ background-image: url(../images/arrows-dark.png);
64
+ }
65
+
66
+ .ws_edit_link:active {
67
+ background: transparent url(../images/arrows-dark.png) no-repeat center 5px;
68
+ }
69
+
70
+ .ws_edit_link_expanded {
71
+ border-bottom: none;
72
+ border-color: #ffffd0;
73
+
74
+ padding-bottom: 1px;
75
+ border-bottom-right-radius: 0;
76
+ border-bottom-left-radius: 0;
77
+
78
+ -moz-border-radius-bottomright: 0;
79
+ -moz-border-radius-bottomleft: 0;
80
+
81
+ -webkit-border-bottom-right-radius: 0;
82
+ -webkit-border-bottom-left-radius: 0;
83
+ }
84
+
85
+
86
+ /**
87
+ * Separators
88
+ */
89
+
90
+ .ws_menu_separator {
91
+ border-color: #d9d9d9;
92
+ }
93
+
94
+ .ws_menu_separator .ws_item_head {
95
+ height: 29px;
96
+ border-radius: 3px;
97
+ background: #F9F9F9 url("../images/menu-arrows.png") no-repeat 4px 8px;
98
+ }
99
+
100
+ .ws_menu_separator .ws_item_title {
101
+ display: none;
102
+ }
103
+
104
+ .ws_menu_separator.ws_active .ws_item_head {
105
+ background: #999 url("../images/menu-arrows.png") no-repeat 4px 8px;
106
+ }
107
+
108
+ /**
109
+ * Active item
110
+ */
111
+
112
+ .ws_active .ws_item_head {
113
+ background: #777;
114
+ background-image: -webkit-gradient(linear, left bottom, left top, from(#6d6d6d), to(#808080));
115
+ background-image: -webkit-linear-gradient(bottom, #6d6d6d, #808080);
116
+ background-image: -moz-linear-gradient(bottom, #6d6d6d, #808080);
117
+ background-image: -o-linear-gradient(bottom, #6d6d6d, #808080);
118
+ background-image: linear-gradient(to top, #6d6d6d, #808080);
119
+ }
120
+
121
+ .ws_active .ws_item_title {
122
+ text-shadow: 0 -1px 0 #333;
123
+ color: #fff;
124
+ border-top-color: #808080;
125
+ border-bottom-color: #6d6d6d;
126
+ }
127
+
128
+ /**
129
+ * Dropping menus on other menus.
130
+ */
131
+
132
+ .ws_menu_drop_hover, .ws_menu_drop_hover .ws_item_head {
133
+ background: #43b529;
134
+ }
135
+
136
+ .ws_menu_drop_hover .ws_item_title {
137
+ text-shadow: none;
138
+ }
139
+
140
+ /**
141
+ * Misc
142
+ */
143
+
144
+ .ws_editbox {
145
+ /*background-color: #ffffd0;*/
146
+ background-color: #FBFBFB;
147
+ padding: 4px 6px;
148
+ }
149
+
150
+ .ws_input_default input, .ws_input_default select {
151
+ color: gray;
152
+ }
153
+
154
+ /*
155
+ * Show/Hide advanced fields
156
+ */
157
+
158
+ .ws_toggle_advanced_fields {
159
+ color: #6087CB;
160
+ font-size: 0.85em;
161
+ }
162
+
163
+ .ws_toggle_advanced_fields:visited, .ws_toggle_advanced_fields:active {
164
+ color: #6087CB;
165
+ }
166
+
167
+ .ws_toggle_advanced_fields:hover {
168
+ color: #d54e21;
169
+ text-decoration: underline;
170
+ }
171
+
172
+
173
+ /*
174
+ * Toolbars
175
+ */
176
+
177
+ .ws_button {
178
+ border: 1px solid #c0c0e0;
179
+ }
180
+
181
+ a.ws_button:hover {
182
+ background-color: #d0e0ff;
183
+ border-color: #9090c0;
184
+ }
185
+
186
+ /************************************
187
+ Export and import
188
+ *************************************/
189
+
190
+ .ui-dialog {
191
+ background: white;
192
+ border: 1px solid #c0c0c0;
193
+ }
194
+
195
+ .ui-dialog-titlebar {
196
+ background-color: #86A7E3;
197
+ }
198
+
199
+ .ui-dialog-title {
200
+ color: white;
201
+ }
202
+
203
+ .ui-dialog-titlebar-close {
204
+ background: #86A7E3 url(../images/x.png) no-repeat center;
205
+ color: white;
206
+ }
207
+
208
+ .ui-dialog-titlebar-close:hover {
209
+ /*background-image: url(../images/x-light.png);*/
210
+ background-color: #a6c2f5;
211
+ }
images/arrows-dark.png ADDED
Binary file
images/arrows.png ADDED
Binary file
images/check-all.png ADDED
Binary file
images/external.png ADDED
Binary file
images/logo-medium.png ADDED
Binary file
images/plugin_disabled.png ADDED
Binary file
includes/access-editor-dialog.php ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div id="ws_menu_access_editor" title="Permissions">
2
+
3
+ <div class="ws_dialog_panel">
4
+ <div class="ws_hint" id="ws_hint_menu_permissions">
5
+ <div class="ws_hint_close" title="Close">x</div>
6
+ <div class="ws_hint_content">
7
+ <strong>Hint:</strong> Individual submenus can override these settings.
8
+ As a result, this menu will stay visible as long as at least one of its
9
+ submenus is accessible.
10
+ </div>
11
+ </div>
12
+
13
+ <div class="error inline" id="ws_hardcoded_role_error">
14
+ <p>
15
+ <strong>Note:</strong>
16
+ Only users with the "<span id="ws_hardcoded_role_name">[role]</span>" role
17
+ can access this menu. This restriction is hard&shy;coded in the plugin that
18
+ created the menu.
19
+ </p>
20
+ </div>
21
+
22
+ <div id="ws_role_access_container" class="ws_dialog_subpanel">
23
+ <strong>Grant access</strong>
24
+ <a class="ws_tooltip_trigger" title="
25
+ Check a box to give that role or user the required capability and access to this menu.
26
+ Clear the box to prevent access.
27
+ ">[?]</a>
28
+ <br>
29
+
30
+ <div id="ws_role_table_body_container">
31
+ <div id="ws_role_access_overlay" class="ws_hide_if_pro"></div>
32
+ <div id="ws_role_access_overlay_content" class="ws_hide_if_pro">
33
+ Pro only feature.
34
+ Use capabilities (below) instead.
35
+ </div>
36
+
37
+ <table class="widefat fixed ws_role_table_body">
38
+ <tbody>
39
+ <!-- Table contents will be generated by JavaScript. -->
40
+ </tbody>
41
+ </table>
42
+ </div>
43
+ </div>
44
+
45
+
46
+ <div id="ws_required_cap_container" class="ws_dialog_subpanel">
47
+ <strong>Required capability</strong>
48
+ <a class="ws_tooltip_trigger" title="
49
+ This capability check is hard-coded in WordPress or the plugin that created the menu.
50
+
51
+ &lt;ul class=&quot;ws_tooltip_content_list&quot;&gt;
52
+ &lt;li&gt;
53
+ Only roles with the this capability will be able to access this menu.
54
+ &lt;li&gt;
55
+ Admin Menu Editor will automatically grant the required capability to
56
+ all roles you check in the &quot;Roles&quot; list.
57
+ &lt;li&gt;
58
+ Custom menus have no hard-coded capability requirements.
59
+ &lt;/ul&gt;
60
+ ">[?]</a>
61
+ <br>
62
+ <span id="ws_required_capability">capability_here</span>
63
+ </div>
64
+
65
+ <div id="ws_extra_cap_container" class="ws_dialog_subpanel">
66
+ <label for="ws_extra_capability">
67
+ <strong>Extra capability</strong>
68
+ </label>
69
+ <a class="ws_tooltip_trigger" title="
70
+ Optional. An additional capability check that will be applied on top of
71
+ the &quot;Roles&quot; and &quot;Required capability&quot; settings.
72
+ Leave empty to disable.
73
+ ">[?]</a>
74
+ <br>
75
+ <input type="text" id="ws_extra_capability" class="ws_has_dropdown" value=""><input type="button" id="ws_trigger_capability_dropdown" value="&#9660;"
76
+ class="button ws_dropdown_button" tabindex="-1">
77
+ </div>
78
+ </div>
79
+
80
+ <div class="ws_dialog_buttons">
81
+ <input type="button" class="button-primary" value="Save Changes" id="ws_save_access_settings">
82
+ <input type="button" class="button ws_close_dialog" value="Cancel">
83
+ </div>
84
+
85
+ </div>
includes/admin-menu-editor-mu.php CHANGED
@@ -1,4 +1,11 @@
1
  <?php
 
 
 
 
 
 
 
2
 
3
  /**
4
  To install Admin Menu Editor as a global plugin in WPMU :
@@ -29,7 +36,7 @@ if ( file_exists($ws_menu_editor_filename) ) {
29
  }
30
 
31
  function ws_ame_installation_error(){
32
- if ( !is_site_admin() ) return;
33
  ?>
34
  <div class="error fade"><p>
35
  <strong>Admin Menu Editor is installed incorrectly!</strong>
@@ -42,5 +49,3 @@ function ws_ame_installation_error(){
42
  </div>
43
  <?php
44
  }
45
-
46
- ?>
1
  <?php
2
+ /*
3
+ Plugin Name: Admin Menu Editor Pro (Multisite module)
4
+ Plugin URI: http://w-shadow.com/admin-menu-editor-pro/
5
+ Description: Lets you edit the WordPress admin menu. To access the editor, go to the Dashboard of one of your network sites and open the Settings -&gt; Menu Editor page.
6
+ Author: Janis Elsts
7
+ Author URI: http://w-shadow.com/
8
+ */
9
 
10
  /**
11
  To install Admin Menu Editor as a global plugin in WPMU :
36
  }
37
 
38
  function ws_ame_installation_error(){
39
+ if ( !is_super_admin() ) return;
40
  ?>
41
  <div class="error fade"><p>
42
  <strong>Admin Menu Editor is installed incorrectly!</strong>
49
  </div>
50
  <?php
51
  }
 
 
includes/auto-versioning.php ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('AutoVersioning') ) {
4
+
5
+ /**
6
+ * This class enables automatic versioning of CSS/JS by adding file modification time to the URLs.
7
+ * @see http://stackoverflow.com/questions/118884/
8
+ */
9
+ class AutoVersioning {
10
+ private static $version_in_filename = false;
11
+
12
+ /**
13
+ * An auto-versioning wrapper for wp_register_s*() and wp_enqueue_s*() dependency APIs.
14
+ *
15
+ * @static
16
+ * @param string $wp_api_function The name of the WP dependency API to call.
17
+ * @param string $handle Script or stylesheet handle.
18
+ * @param string $src Script or stylesheet URL.
19
+ * @param array $deps Dependencies.
20
+ * @param bool|string $last_param Either $media (for wp_register_style) or $in_footer (for wp_register_script).
21
+ * @param bool $add_ver_to_filename TRUE = add version to filename, FALSE = add it to the query string.
22
+ */
23
+ public static function add_dependency($wp_api_function, $handle, $src, $deps, $last_param, $add_ver_to_filename = false ) {
24
+ list($src, $version) = self::auto_version($src, $add_ver_to_filename);
25
+ call_user_func($wp_api_function, $handle, $src, $deps, $version, $last_param);
26
+ }
27
+
28
+ /**
29
+ * Automatically version a script or style sheet URL based on file modification time.
30
+ *
31
+ * Returns auto-versioned $src and $ver values suitable for use with WordPress dependency APIs like
32
+ * wp_register_script() and wp_register_style().
33
+ *
34
+ * @static
35
+ * @param string $url
36
+ * @param bool $add_ver_to_filename
37
+ * @return array array($url, $version)
38
+ */
39
+ private static function auto_version($url, $add_ver_to_filename = false) {
40
+ $version = false;
41
+ $filename = self::guess_filename_from_url($url);
42
+
43
+ if ( ($filename !== null) && is_file($filename) ) {
44
+ $mtime = filemtime($filename);
45
+ if ( $add_ver_to_filename ) {
46
+ $url = preg_replace('@\.([^./\?]+)(\?.*)?$@', '.' . $mtime . '.$1', $url);
47
+ $version = null;
48
+ } else {
49
+ $version = $mtime;
50
+ }
51
+ }
52
+
53
+ return array($url, $version);
54
+ }
55
+
56
+ private static function guess_filename_from_url($url) {
57
+ $url_mappings = array(
58
+ plugins_url() => WP_PLUGIN_DIR,
59
+ plugins_url('', WPMU_PLUGIN_DIR . '/dummy') => WPMU_PLUGIN_DIR,
60
+ get_stylesheet_directory_uri() => get_stylesheet_directory(),
61
+ get_template_directory_uri() => get_template_directory(),
62
+ content_url() => WP_CONTENT_DIR,
63
+ site_url('/' . WPINC) => ABSPATH . WPINC,
64
+ );
65
+
66
+ $filename = null;
67
+ foreach($url_mappings as $root_url => $directory) {
68
+ if ( strpos($url, $root_url) === 0 ) {
69
+ $filename = $directory . '/' . substr($url, strlen($root_url));
70
+ //Get rid of the query string, if any.
71
+ list($filename, ) = explode('?', $filename, 2);
72
+ break;
73
+ }
74
+ }
75
+
76
+ return $filename;
77
+ }
78
+
79
+ /**
80
+ * Apply automatic versioning to all scripts and style sheets added using WP dependency APIs.
81
+ *
82
+ * If you set $add_ver_to_filename to TRUE, make sure to also add the following code to your
83
+ * .htaccess file or your site may break:
84
+ *
85
+ * <IfModule mod_rewrite.c>
86
+ * RewriteEngine On
87
+ * RewriteRule ^(.*)\.[\d]{10}\.(css|js)$ $1.$2 [L]
88
+ * </IfModule>
89
+ *
90
+ * @static
91
+ * @param bool $add_ver_to_filename
92
+ */
93
+ public static function apply_to_all_dependencies($add_ver_to_filename = false) {
94
+ self::$version_in_filename = $add_ver_to_filename;
95
+ foreach(array('script_loader_src', 'style_loader_src') as $hook) {
96
+ add_filter($hook, __CLASS__ . '::_filter_dependency_src', 10, 1);
97
+ }
98
+ }
99
+
100
+ public static function _filter_dependency_src($src) {
101
+ //Only add version info to CSS/JS files that don't already have it in the file name.
102
+ if ( preg_match('@(?<!\.\d{10})\.(css|js)(\?|$)@i', $src) ) {
103
+ list($src, $version) = self::auto_version($src, self::$version_in_filename);
104
+ if ( !empty($version) ) {
105
+ $src = add_query_arg('ver', $version, $src);
106
+ }
107
+ }
108
+ return $src;
109
+ }
110
+ }
111
+
112
+ } //class_exists()
113
+
114
+ if ( !function_exists('wp_register_auto_versioned_script') ) {
115
+ function wp_register_auto_versioned_script($handle, $src, $deps = array(), $in_footer = false, $add_ver_to_filename = false) {
116
+ AutoVersioning::add_dependency('wp_register_script', $handle, $src, $deps, $in_footer, $add_ver_to_filename);
117
+ }
118
+ }
119
+
120
+ if ( !function_exists('wp_register_auto_versioned_style') ) {
121
+ function wp_register_auto_versioned_style( $handle, $src, $deps = array(), $media = 'all', $add_ver_to_filename = false) {
122
+ AutoVersioning::add_dependency('wp_register_style', $handle, $src, $deps, $media, $add_ver_to_filename);
123
+ }
124
+ }
125
+
126
+ if ( !function_exists('wp_enqueue_auto_versioned_script') ) {
127
+ function wp_enqueue_auto_versioned_script( $handle, $src, $deps = array(), $in_footer = false, $add_ver_to_filename = false ) {
128
+ AutoVersioning::add_dependency('wp_enqueue_script', $handle, $src, $deps, $in_footer, $add_ver_to_filename);
129
+ }
130
+ }
131
+
132
+ if ( !function_exists('wp_enqueue_auto_versioned_style') ) {
133
+ function wp_enqueue_auto_versioned_style( $handle, $src, $deps = array(), $media = 'all', $add_ver_to_filename = false ) {
134
+ AutoVersioning::add_dependency('wp_enqueue_style', $handle, $src, $deps, $media, $add_ver_to_filename);
135
+ }
136
+ }
includes/consistency-check.php ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !defined('ABSPATH') ) {
3
+ die();
4
+ }
5
+
6
+ /** @var string $pluginFile Should be provided by the including file. */
7
+
8
+ $log = array();
9
+ $log[] = sprintf(
10
+ '[OK] Main plugin file: %s',
11
+ $pluginFile
12
+ );
13
+
14
+ $log[] = sprintf(
15
+ '[Info] WordPress version: %s',
16
+ $GLOBALS['wp_version']
17
+ );
18
+
19
+ $log[] = sprintf(
20
+ '[Info] WP_PLUGIN_DIR: %s',
21
+ WP_PLUGIN_DIR
22
+ );
23
+
24
+ $log[] = sprintf(
25
+ '[Info] WP_PLUGIN_URL: %s',
26
+ WP_PLUGIN_URL
27
+ );
28
+
29
+ $log[] = sprintf(
30
+ '[Info] WPMU_PLUGIN_DIR: %s',
31
+ WPMU_PLUGIN_DIR
32
+ );
33
+
34
+ $log[] = sprintf(
35
+ '[Info] WPMU_PLUGIN_URL: %s',
36
+ WPMU_PLUGIN_URL
37
+ );
38
+
39
+ $expectedPluginRoot = dirname(dirname(__FILE__));
40
+ $actualPluginRoot = dirname($pluginFile);
41
+
42
+ if ( $expectedPluginRoot === $actualPluginRoot ) {
43
+ $log[] = sprintf(
44
+ '[OK] Plugin root directory is "%s"',
45
+ $actualPluginRoot
46
+ );
47
+ } else {
48
+ $log[] = sprintf(
49
+ '[Error] Actual plugin directory: "%s", expected: "%s"',
50
+ $actualPluginRoot,
51
+ $expectedPluginRoot
52
+ );
53
+ }
54
+
55
+ $requiredFiles = array(
56
+ 'css/menu-editor.css',
57
+ 'css/jquery.qtip.min.css',
58
+ 'js/menu-editor.js',
59
+ 'js/menu-highlight-fix.js',
60
+ 'js/jquery.sort.js',
61
+ 'js/jquery.qtip.min.js',
62
+ 'js/jquery.json.js',
63
+ 'images/cut.png',
64
+ 'images/delete.png',
65
+ 'images/page_white_add.png',
66
+ 'images/spinner.gif',
67
+ 'includes/editor-page.php',
68
+ 'includes/menu-editor-core.php',
69
+ 'includes/access-editor-dialog.php',
70
+ 'includes/menu-item.php',
71
+ 'menu-editor.php',
72
+ 'uninstall.php',
73
+ );
74
+
75
+ foreach($requiredFiles as $filename) {
76
+ $fullPath = dirname($pluginFile) . '/' . $filename;
77
+ if ( is_readable($fullPath) ) {
78
+ $log[] = sprintf(
79
+ '[OK] File exists: %s',
80
+ $fullPath
81
+ );
82
+ } else {
83
+ $log[] = sprintf(
84
+ '[Error] File does not exist: %s',
85
+ $fullPath
86
+ );
87
+ }
88
+ }
89
+
90
+ foreach($requiredFiles as $filename) {
91
+ if ( !preg_match('@\.(css|js|png)$@', $filename) ) {
92
+ continue;
93
+ }
94
+
95
+ $url = plugins_url($filename, $pluginFile);
96
+ $log[] = ame_test_url_access($url, $filename);
97
+ }
98
+
99
+ echo '<pre>';
100
+ $divider = str_repeat('-', 50);
101
+ echo "File consistency checks:\n", $divider, "\n";
102
+ foreach($log as $message) {
103
+ echo $message, "\n";
104
+ }
105
+
106
+ //Test for buggy plugins_url filters.
107
+ echo $divider, "\nTesting for problems with the 'plugins_url' hook...\n";
108
+ add_filter('plugins_url', 'ame_plugins_url_test_first', -9999, 3);
109
+ add_filter('plugins_url', 'ame_plugins_url_test_last', 9999, 3);
110
+
111
+ $url = plugins_url('css/menu-editor.css', $pluginFile);
112
+
113
+ remove_filter('plugins_url', 'ame_plugins_url_test_first', -9999, 3);
114
+ remove_filter('plugins_url', 'ame_plugins_url_test_last', 9999, 3);
115
+
116
+ function ame_plugins_url_test_first($url, $path = '', $plugin = '') {
117
+ printf(
118
+ '[Info] plugins_url() output before plugin hooks: %s' . "\n",
119
+ esc_html($url)
120
+ );
121
+ echo ame_test_url_access($url, 'css/menu-editor.css'), "\n";
122
+ return $url;
123
+ }
124
+
125
+ function ame_plugins_url_test_last($url, $path = '', $plugin = '') {
126
+ printf(
127
+ '[Info] plugins_url() output after plugin hooks: %s' . "\n",
128
+ esc_html($url)
129
+ );
130
+ echo ame_test_url_access($url, 'css/menu-editor.css'), "\n";
131
+ return $url;
132
+ }
133
+
134
+ function ame_test_url_access($url, $filename) {
135
+ $result = wp_remote_get($url);
136
+
137
+ if ( is_wp_error($result) ) {
138
+ return sprintf(
139
+ '[Error] Can not load URL: %s (%s)',
140
+ esc_html($url),
141
+ $result->get_error_message()
142
+ );
143
+ } else if ( $result['response']['code'] == 200 ) {
144
+ return sprintf(
145
+ '[OK] URL is accessible: %s',
146
+ esc_html($url)
147
+ );
148
+ } else {
149
+ return sprintf(
150
+ '[Error] Can no load "%s", URL : %s (%d %s)',
151
+ esc_html($filename),
152
+ esc_html($url),
153
+ $result['response']['code'],
154
+ $result['response']['message']
155
+ );
156
+ }
157
+ }
158
+
159
+ echo $divider;
160
+ echo '</pre>';
includes/editor-page.php ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @var array $editor_data Various pieces of data passed by the plugin.
4
+ */
5
+ $images_url = $editor_data['images_url'];
6
+
7
+ $icons = array(
8
+ 'cut' => '/gnome-icon-theme/edit-cut-blue.png',
9
+ 'copy' => '/gion/edit-copy.png',
10
+ 'paste' => '/gnome-icon-theme/edit-paste.png',
11
+ 'hide' => '/icon-extension-grey.png',
12
+ 'new' => '/page-add.png',
13
+ 'delete' => '/page-delete.png',
14
+ 'new-separator' => '/separator-add.png',
15
+ 'toggle-all' => '/check-all.png',
16
+ );
17
+ foreach($icons as $name => $url) {
18
+ $icons[$name] = $images_url . $url;
19
+ }
20
+
21
+ //Output the "Upgrade to Pro" message
22
+ if ( !apply_filters('admin_menu_editor_is_pro', false) ){
23
+ ?>
24
+ <script type="text/javascript">
25
+ (function($){
26
+ $('#screen-meta-links').append(
27
+ '<div id="ws-pro-version-notice">' +
28
+ '<a href="http://adminmenueditor.com/upgrade-to-pro/?utm_source=Admin%2BMenu%2BEditor%2Bfree&utm_medium=text_link&utm_content=top_upgrade_link&utm_campaign=Plugins" id="ws-pro-version-notice-link" class="show-settings" target="_blank" title="View Pro version details">Upgrade to Pro</a>' +
29
+ '</div>'
30
+ );
31
+ })(jQuery);
32
+ </script>
33
+ <?php
34
+ }
35
+
36
+ ?>
37
+ <div class="wrap">
38
+ <h2>
39
+ <?php echo apply_filters('admin_menu_editor-self_page_title', 'Menu Editor'); ?>
40
+ <a href="<?php echo esc_attr($editor_data['settings_page_url']); ?>" class="add-new-h2" id="ws_plugin_settings_button"
41
+ title="Configure plugin settings">Settings</a>
42
+ </h2>
43
+
44
+ <?php
45
+ if ( !empty($_GET['message']) ){
46
+ if ( intval($_GET['message']) == 1 ){
47
+ echo '<div id="message" class="updated fade"><p><strong>Settings saved.</strong></p></div>';
48
+ } elseif ( intval($_GET['message']) == 2 ) {
49
+ echo '<div id="message" class="error"><p><strong>Failed to decode input! The menu wasn\'t modified.</strong></p></div>';
50
+ }
51
+ }
52
+ ?>
53
+
54
+ <?php
55
+ $hint_id = 'ws_whats_new_120';
56
+ $show_whats_new = false && apply_filters('admin_menu_editor_is_pro', false) && !empty($editor_data['show_hints'][$hint_id]);
57
+ if ( $show_whats_new ):
58
+ ?>
59
+ <div class="ws_hint" id="<?php echo esc_attr($hint_id); ?>">
60
+ <div class="ws_hint_close" title="Close">x</div>
61
+ <div class="ws_hint_content">
62
+ <strong>What's New In 1.20 and 1.30</strong>
63
+ <ul>
64
+ <li>New menu permissions interface.
65
+ <a href="http://w-shadow.com/admin-menu-editor-pro/permissions/">Learn more.</a></li>
66
+
67
+ <li>You can now use "not:user:username", "capability1,capability2", "capability1+capability2" and other
68
+ advanced syntax in the capability field. See the link above for details.</li>
69
+
70
+ <li>You can drag sub-menu items to the top level and the other way around. To do it,
71
+ drag the item to the very end of the (sub-)menu and drop it on the yellow rectangle that will appear.</li>
72
+
73
+ <li>Added a "Target page" drop-down to simplify setting menu URLs. You can still enter an arbitrary URL
74
+ by selecting "Custom".</li>
75
+
76
+ <li>Miscellaneous bug fixes.</li>
77
+
78
+ </ul>
79
+ </div>
80
+ </div>
81
+ <?php
82
+ endif;
83
+ ?>
84
+
85
+ <?php include dirname(__FILE__) . '/access-editor-dialog.php'; ?>
86
+
87
+ <div id='ws_menu_editor'>
88
+ <div id="ws_actor_selector_container">
89
+ <ul id="ws_actor_selector" class="subsubsub" style="display: none;">
90
+ <!-- Contents will be generated by JS -->
91
+ </ul>
92
+ <div class="clear"></div>
93
+ </div>
94
+
95
+ <div>
96
+
97
+ <div class='ws_main_container'>
98
+ <div class='ws_toolbar'>
99
+ <div class="ws_button_container">
100
+ <a id='ws_cut_menu' class='ws_button' href='javascript:void(0)' title='Cut'><img src='<?php echo $icons['cut']; ?>' alt="Cut" /></a>
101
+ <a id='ws_copy_menu' class='ws_button' href='javascript:void(0)' title='Copy'><img src='<?php echo $icons['copy']; ?>' alt="Copy" /></a>
102
+ <a id='ws_paste_menu' class='ws_button' href='javascript:void(0)' title='Paste'><img src='<?php echo $icons['paste']; ?>' alt="Paste" /></a>
103
+
104
+ <div class="ws_separator">&nbsp;</div>
105
+
106
+ <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>
107
+ <a id='ws_hide_menu' class='ws_button' href='javascript:void(0)' title='Show/Hide'><img src='<?php echo $icons['hide']; ?>' alt="Show/Hide" /></a>
108
+ <a id='ws_delete_menu' class='ws_button' href='javascript:void(0)' title='Delete menu'><img src='<?php echo $icons['delete']; ?>' alt="Delete menu" /></a>
109
+
110
+ <div class="ws_separator">&nbsp;</div>
111
+
112
+ <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>
113
+
114
+ <?php if ( apply_filters('admin_menu_editor_is_pro', false) ): ?>
115
+ <div class="ws_separator">&nbsp;</div>
116
+
117
+ <a id='ws_toggle_all_menus' class='ws_button' href='javascript:void(0)'
118
+ title='Toggle all menus for the selected role'><img src='<?php echo $icons['toggle-all']; ?>' alt="Toggle all" /></a>
119
+ <?php endif; ?>
120
+ </div>
121
+ </div>
122
+
123
+ <div id='ws_menu_box' class="ws_box">
124
+ </div>
125
+
126
+ <?php do_action('admin_menu_editor_container', 'menu'); ?>
127
+ </div>
128
+
129
+ <div class='ws_main_container'>
130
+ <div class='ws_toolbar'>
131
+ <div class="ws_button_container">
132
+ <a id='ws_cut_item' class='ws_button' href='javascript:void(0)' title='Cut'><img src='<?php echo $icons['cut']; ?>' alt="Cut" /></a>
133
+ <a id='ws_copy_item' class='ws_button' href='javascript:void(0)' title='Copy'><img src='<?php echo $icons['copy']; ?>' alt="Copy" /></a>
134
+ <a id='ws_paste_item' class='ws_button' href='javascript:void(0)' title='Paste'><img src='<?php echo $icons['paste']; ?>' alt="Paste" /></a>
135
+
136
+ <div class="ws_separator">&nbsp;</div>
137
+
138
+ <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>
139
+ <a id='ws_hide_item' class='ws_button' href='javascript:void(0)' title='Show/Hide'><img src='<?php echo $icons['hide']; ?>' alt="Show/Hide" /></a>
140
+ <a id='ws_delete_item' class='ws_button' href='javascript:void(0)' title='Delete menu item'><img src='<?php echo $icons['delete']; ?>' alt="Delete menu item" /></a>
141
+
142
+ <div class="ws_separator">&nbsp;</div>
143
+
144
+ <?php if ( apply_filters('admin_menu_editor_is_pro', false) ): ?>
145
+ <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>
146
+ <div class="ws_separator">&nbsp;</div>
147
+ <?php endif; ?>
148
+
149
+ <a id='ws_sort_ascending' class='ws_button' href='javascript:void(0)' title='Sort ascending'>
150
+ <img src='<?php echo $images_url; ?>/sort_ascending.png' alt="Sort ascending" />
151
+ </a>
152
+ <a id='ws_sort_descending' class='ws_button' href='javascript:void(0)' title='Sort descending'>
153
+ <img src='<?php echo $images_url; ?>/sort_descending.png' alt="Sort descending" />
154
+ </a>
155
+ </div>
156
+ </div>
157
+
158
+ <div id='ws_submenu_box' class="ws_box">
159
+ </div>
160
+
161
+ <?php do_action('admin_menu_editor_container', 'submenu'); ?>
162
+ </div>
163
+
164
+ <div class="ws_basic_container">
165
+
166
+ <div class="ws_main_container" id="ws_editor_sidebar">
167
+ <form method="post" action="<?php echo admin_url('options-general.php?page=menu_editor&noheader=1'); ?>" id='ws_main_form' name='ws_main_form'>
168
+ <?php wp_nonce_field('menu-editor-form'); ?>
169
+ <input type="hidden" name="action" value="save_menu">
170
+ <input type="hidden" name="data" id="ws_data" value="">
171
+ <input type="hidden" name="data_length" id="ws_data_length" value="">
172
+ <input type="hidden" name="selected_actor" id="ws_selected_actor" value="">
173
+ <input type="button" id='ws_save_menu' class="button-primary ws_main_button" value="Save Changes" />
174
+ </form>
175
+
176
+ <input type="button" id='ws_reset_menu' value="Undo changes" class="button ws_main_button" />
177
+ <input type="button" id='ws_load_menu' value="Load default menu" class="button ws_main_button" />
178
+
179
+ <?php
180
+ do_action('admin_menu_editor_sidebar');
181
+ ?>
182
+ </div>
183
+
184
+ <?php
185
+ $hint_id = 'ws_sidebar_pro_ad';
186
+ $show_pro_benefits = !apply_filters('admin_menu_editor_is_pro', false) && (!isset($editor_data['show_hints'][$hint_id]) || $editor_data['show_hints'][$hint_id]);
187
+
188
+ if ( $show_pro_benefits ):
189
+ $benefit_variations = array(
190
+ 'Simplified, role-based permissions.',
191
+ 'Role-based menu permissions.',
192
+ 'Simpler, role-based permissions.',
193
+ );
194
+ //Pseudo-randomly select one phrase based on the site URL.
195
+ $variation_index = hexdec( substr(md5(get_site_url()), -1) ) % count($benefit_variations);
196
+ $selected_variation = $benefit_variations[$variation_index];
197
+
198
+ $pro_version_link = 'http://adminmenueditor.com/upgrade-to-pro/?utm_source=Admin%2BMenu%2BEditor%2Bfree&utm_medium=text_link&utm_content=sidebar_link_cv' . $variation_index . '&utm_campaign=Plugins';
199
+ ?>
200
+ <div class="clear"></div>
201
+
202
+ <div class="ws_hint" id="<?php echo esc_attr($hint_id); ?>">
203
+ <div class="ws_hint_close" title="Close">x</div>
204
+ <div class="ws_hint_content">
205
+ <strong>Upgrade to Pro:</strong>
206
+ <ul>
207
+ <li><?php echo $selected_variation; ?></li>
208
+ <li>Drag items between menu levels.</li>
209
+ <li>Menu export &amp; import.</li>
210
+ </ul>
211
+ <a href="<?php echo esc_attr($pro_version_link); ?>" target="_blank">Learn more</a>
212
+ |
213
+ <a href="http://amedemo.com/" target="_blank">Try online demo</a>
214
+ </div>
215
+ </div>
216
+ <?php
217
+ endif;
218
+ ?>
219
+
220
+ </div> <!-- / .ws_basic_container -->
221
+
222
+ </div>
223
+
224
+ <div class="clear"></div>
225
+
226
+ </div> <!-- / .ws_menu_editor -->
227
+
228
+ </div> <!-- / .wrap -->
229
+
230
+
231
+
232
+ <?php
233
+ //Create a pop-up capability selector
234
+ $capSelector = array('<select id="ws_cap_selector" class="ws_dropdown" size="10">');
235
+
236
+ $capSelector[] = '<optgroup label="Roles">';
237
+ foreach($editor_data['all_roles'] as $role_id => $role_name){
238
+ $capSelector[] = sprintf(
239
+ '<option value="%s">%s</option>',
240
+ esc_attr($role_id),
241
+ $role_name
242
+ );
243
+ }
244
+ $capSelector[] = '</optgroup>';
245
+
246
+ $capSelector[] = '<optgroup label="Capabilities">';
247
+ foreach($editor_data['all_capabilities'] as $cap){
248
+ $capSelector[] = sprintf(
249
+ '<option value="%s">%s</option>',
250
+ esc_attr($cap),
251
+ $cap
252
+ );
253
+ }
254
+ $capSelector[] = '</optgroup>';
255
+ $capSelector[] = '</select>';
256
+
257
+ echo implode("\n", $capSelector);
258
+ ?>
259
+
260
+ <!-- Menu icon selector widget -->
261
+ <div id="ws_icon_selector" style="display: none;">
262
+ <?php
263
+ //Let the user select a custom icon via the media uploader.
264
+ //We only support the new WP 3.5+ media API. Hence the function_exists() check.
265
+ if ( function_exists('wp_enqueue_media') ):
266
+ ?>
267
+ <input type="button" class="button"
268
+ id="ws_choose_icon_from_media"
269
+ title="Upload an image or choose one from your media library"
270
+ value="Choose Icon">
271
+ <div class="clear"></div>
272
+ <?php
273
+ endif;
274
+ ?>
275
+
276
+ <?php
277
+ $defaultWpIcons = array(
278
+ 'generic', 'dashboard', 'post', 'media', 'links', 'page', 'comments',
279
+ 'appearance', 'plugins', 'users', 'tools', 'settings', 'site',
280
+ );
281
+ foreach($defaultWpIcons as $icon) {
282
+ printf(
283
+ '<div class="ws_icon_option" title="%1$s" data-icon-class="menu-icon-%2$s">
284
+ <div class="ws_icon_image icon16 icon-%2$s"><br></div>
285
+ </div>',
286
+ esc_attr(ucwords($icon)),
287
+ $icon
288
+ );
289
+ }
290
+
291
+ $defaultIconImages = array(
292
+ 'images/generic.png',
293
+ );
294
+ foreach($defaultIconImages as $icon) {
295
+ printf(
296
+ '<div class="ws_icon_option" data-icon-url="%1$s">
297
+ <img src="%1$s">
298
+ </div>',
299
+ esc_attr($icon)
300
+ );
301
+ }
302
+ ?>
303
+ <div class="ws_icon_option ws_custom_image_icon" title="Custom image" style="display: none;">
304
+ <img src="<?php echo esc_attr(admin_url('images/loading.gif')); ?>">
305
+ </div>
306
+ <div class="clear"></div>
307
+ </div>
308
+
309
+ <span id="ws-ame-screen-meta-contents" style="display:none;">
310
+ <label for="ws-hide-advanced-settings">
311
+ <input type="checkbox" id="ws-hide-advanced-settings"<?php
312
+ if ( $this->options['hide_advanced_settings'] ){
313
+ echo ' checked="checked"';
314
+ }
315
+ ?> /> Hide advanced options
316
+ </label>
317
+ </span>
318
+
319
+ <script type='text/javascript'>
320
+ var defaultMenu = <?php echo $editor_data['default_menu_js']; ?>;
321
+ var customMenu = <?php echo $editor_data['custom_menu_js']; ?>;
322
+ </script>
323
+
324
+ <?php
325
+
326
+ //Let the Pro version script output it's extra HTML & scripts.
327
+ do_action('admin_menu_editor_footer');
includes/menu-editor-core.php CHANGED
@@ -8,47 +8,94 @@ if (class_exists('WPMenuEditor')){
8
  );
9
  }
10
 
11
- //Load the "framework"
12
  $thisDirectory = dirname(__FILE__);
13
  require $thisDirectory . '/shadow_plugin_framework.php';
 
14
  require $thisDirectory . '/menu-item.php';
15
-
16
- if ( !class_exists('WPMenuEditor') ) :
17
 
18
  class WPMenuEditor extends MenuEd_ShadowPluginFramework {
 
 
 
19
 
20
- protected $default_wp_menu = null; //Holds the default WP menu for later use in the editor
21
- protected $default_wp_submenu = null; //Holds the default WP menu for later use
22
- private $filtered_wp_menu = null; //The final, ready-for-display top-level menu and sub-menu.
23
- private $filtered_wp_submenu = null;
 
 
 
 
 
 
 
 
24
 
25
- protected $title_lookups = array(); //A list of page titles indexed by $item['file']. Used to
26
  //fix the titles of moved plugin pages.
27
- private $custom_menu = null; //The current custom menu with defaults merged in
28
- public $menu_format_version = 4;
29
-
30
- private $templates = null; //Template arrays for various menu structures. See the constructor for details.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  //Our personal copy of the request vars, without any "magic quotes".
33
  private $post = array();
34
  private $get = array();
35
 
36
  function init(){
37
- //Determine if the plugin is active network-wide (i.e. either installed in
38
- //the /mu-plugins/ directory or activated "network wide" by the super admin.
39
- if ( $this->is_super_plugin() ){
40
- $this->sitewide_options = true;
41
- }
42
-
43
  //Set some plugin-specific options
44
  if ( empty($this->option_name) ){
45
  $this->option_name = 'ws_menu_editor';
46
  }
47
  $this->defaults = array(
48
  'hide_advanced_settings' => true,
49
- 'menu_format_version' => 0,
50
- 'display_survey_notice' => true,
51
  'first_install_time' => null,
 
 
 
 
 
 
 
 
 
 
 
 
52
  );
53
  $this->serialize_with_json = false; //(Don't) store the options in JSON format
54
 
@@ -57,51 +104,8 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
57
  $this->magic_hooks = true;
58
  $this->magic_hook_priority = 99999;
59
 
60
- //Build some template arrays
61
- $this->templates['basic_defaults'] = array(
62
- 'page_title' => '',
63
- 'menu_title' => '',
64
- 'access_level' => 'read',
65
- 'file' => '',
66
- 'css_class' => '',
67
- 'hookname' => '',
68
- 'icon_url' => '',
69
- 'position' => 0,
70
- 'separator' => false,
71
- 'custom' => false,
72
- 'open_in' => 'same_window', //'new_window', 'iframe' or 'same_window' (the default)
73
- );
74
-
75
- //Template for a basic top-level menu
76
- $this->templates['blank_menu'] = array(
77
- 'page_title' => null,
78
- 'menu_title' => null,
79
- 'access_level' => null,
80
- 'file' => null,
81
- 'css_class' => null,
82
- 'hookname' => null,
83
- 'icon_url' => null,
84
- 'position' => null,
85
- 'separator' => null,
86
- 'custom' => null,
87
- 'open_in' => null,
88
- 'defaults' => $this->templates['basic_defaults'],
89
- 'items' => array(),
90
- );
91
- //Template for menu items
92
- $this->templates['blank_item'] = array(
93
- 'menu_title' => null,
94
- 'access_level' => null,
95
- 'file' => null,
96
- 'page_title' => null,
97
- 'position' => null,
98
- 'custom' => null,
99
- 'open_in' => null,
100
- 'defaults' => $this->templates['basic_defaults'],
101
- );
102
-
103
  //AJAXify screen options
104
- add_action( 'wp_ajax_ws_ame_save_screen_options', array(&$this,'ajax_save_screen_options') );
105
 
106
  //AJAXify hints
107
  add_action('wp_ajax_ws_ame_hide_hint', array($this, 'ajax_hide_hint'));
@@ -109,38 +113,52 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
109
  //Make sure we have access to the original, un-mangled request data.
110
  //This is necessary because WordPress will stupidly apply "magic quotes"
111
  //to the request vars even if this PHP misfeature is disabled.
112
- add_action('plugins_loaded', array($this, 'capture_request_vars'));
113
 
114
  add_action('admin_enqueue_scripts', array($this, 'enqueue_menu_fix_script'));
115
 
 
 
 
 
116
  //User survey
117
  add_action('admin_notices', array($this, 'display_survey_notice'));
118
  }
119
-
120
  function init_finish() {
121
  parent::init_finish();
 
122
 
123
- if ( !isset($this->options['first_install_time']) ) {
124
- $this->options['first_install_time'] = time();
125
- $this->save_options();
126
- }
127
- }
128
-
129
- /**
130
- * Activation hook
131
- *
132
- * @return void
133
- */
134
- function activate(){
135
  //If we have no stored settings for this version of the plugin, try importing them
136
  //from other versions (i.e. the free or the Pro version).
137
  if ( !$this->load_options() ){
138
  $this->import_settings();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  }
140
 
141
- parent::activate();
 
 
 
142
  }
143
-
144
  /**
145
  * Import settings from a different version of the plugin.
146
  *
@@ -153,71 +171,9 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
153
  return true;
154
  }
155
  }
156
-
157
  return false;
158
  }
159
 
160
- /**
161
- * Add the JS required by the editor to the page header
162
- *
163
- * @return void
164
- */
165
- function enqueue_scripts(){
166
- //jQuery JSON plugin
167
- wp_enqueue_script('jquery-json', plugins_url('js/jquery.json-1.3.js', $this->plugin_file), array('jquery'), '1.3');
168
- //jQuery sort plugin
169
- wp_enqueue_script('jquery-sort', plugins_url('js/jquery.sort.js', $this->plugin_file), array('jquery'));
170
- //jQuery UI Droppable
171
- wp_enqueue_script('jquery-ui-droppable');
172
-
173
- //We use WordPress media uploader to let the user upload custom menu icons (WP 3.5+).
174
- if ( function_exists('wp_enqueue_media') ) {
175
- wp_enqueue_media();
176
- }
177
-
178
- //Editor's scipts
179
- wp_enqueue_script(
180
- 'menu-editor',
181
- plugins_url('js/menu-editor.js', $this->plugin_file),
182
- array('jquery', 'jquery-ui-sortable', 'jquery-ui-dialog', 'jquery-form'),
183
- '20130221'
184
- );
185
-
186
- //The editor will need access to some of the plugin data and WP data.
187
- wp_localize_script(
188
- 'menu-editor',
189
- 'wsEditorData',
190
- array(
191
- 'adminAjaxUrl' => admin_url('admin-ajax.php'),
192
- 'showHints' => $this->get_hint_visibility(),
193
- )
194
- );
195
- }
196
-
197
- /**
198
- * Compatibility workaround for Participants Database 1.4.5.2.
199
- *
200
- * Participants Database loads its settings JavaScript on every page in the "Settings" menu,
201
- * not just its own. It doesn't bother to also load the script's dependencies, though, so
202
- * the script crashes *and* it breaks the menu editor by way of collateral damage.
203
- *
204
- * Fix by forcibly removing the offending script from the queue.
205
- */
206
- public function dequeue_pd_scripts() {
207
- if ( is_plugin_active('participants-database/participants-database.php') ) {
208
- wp_dequeue_script('settings_script');
209
- }
210
- }
211
-
212
- /**
213
- * Add the editor's CSS file to the page header
214
- *
215
- * @return void
216
- */
217
- function enqueue_styles(){
218
- wp_enqueue_style('menu-editor-style', plugins_url('css/menu-editor.css', $this->plugin_file), array(), '20130805');
219
- }
220
-
221
  /**
222
  * Create a configuration page and load the custom menu
223
  *
@@ -225,706 +181,994 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
225
  */
226
  function hook_admin_menu(){
227
  global $menu, $submenu;
228
-
229
  //Menu reset (for emergencies). Executed by accessing http://example.com/wp-admin/?reset_admin_menu=1
230
  $reset_requested = isset($this->get['reset_admin_menu']) && $this->get['reset_admin_menu'];
231
  if ( $reset_requested && $this->current_user_can_edit_menu() ){
232
- $this->options['custom_menu'] = null;
233
- $this->save_options();
234
  }
235
 
236
  //The menu editor is only visible to users with the manage_options privilege.
237
  //Or, if the plugin is installed in mu-plugins, only to the site administrator(s).
238
  if ( $this->current_user_can_edit_menu() ){
 
 
239
  $page = add_options_page(
240
  apply_filters('admin_menu_editor-self_page_title', 'Menu Editor'),
241
  apply_filters('admin_menu_editor-self_menu_title', 'Menu Editor'),
242
- 'manage_options',
243
  'menu_editor',
244
  array(&$this, 'page_menu_editor')
245
  );
246
  //Output our JS & CSS on that page only
247
  add_action("admin_print_scripts-$page", array(&$this, 'enqueue_scripts'));
248
  add_action("admin_print_styles-$page", array(&$this, 'enqueue_styles'));
249
-
 
 
 
250
  //Compatibility fix for Participants Database.
251
  add_action("admin_print_scripts-$page", array($this, 'dequeue_pd_scripts'));
252
-
 
 
 
253
  //Make a placeholder for our screen options (hacky)
254
- add_meta_box("ws-ame-screen-options", "You should never see this", array(&$this, 'noop'), $page);
255
- }
256
-
257
- //WP 3.0 in multisite mode has two separators with the same filename. This plugin
258
- //expects all top-level menus to have unique filenames/URLs.
259
- $first_separator1 = -1;
260
- $last_separator1 = -1;
261
- foreach($menu as $index => $item){
262
- if ( $item[2] == 'separator1' ){
263
- $last_separator1 = $index;
264
- if ( $first_separator1 == -1 ){
265
- $first_separator1 = $index;
266
- }
267
- }
268
- }
269
- if ( $first_separator1 != $last_separator1 ){
270
- $menu[$first_separator1][2] = 'separator0';
271
  }
272
 
273
  //Store the "original" menus for later use in the editor
274
  $this->default_wp_menu = $menu;
275
  $this->default_wp_submenu = $submenu;
276
 
 
 
 
277
  //Is there a custom menu to use?
278
- if ( !empty($this->options['custom_menu']) ){
279
- //Check if we need to upgrade the menu structure
280
- if ( empty($this->options['menu_format_version']) || ($this->options['menu_format_version'] < $this->menu_format_version) ){
281
- $this->options['custom_menu'] = $this->upgrade_menu_structure($this->options['custom_menu']);
282
- $this->options['menu_format_version'] = $this->menu_format_version;
283
- $this->save_options();
284
- }
285
  //Merge in data from the default menu
286
- $tree = $this->menu_merge($this->options['custom_menu'], $menu, $submenu);
287
- //Save for later - the editor page will need it
288
- $this->custom_menu = $tree;
289
- //Apply the custom menu
290
- list($menu, $submenu, $this->title_lookups) = $this->tree2wp($tree);
291
- //Re-filter the menu (silly WP should do that itself, oh well)
292
- $this->filter_menu();
293
- $this->filtered_wp_menu = $menu;
294
- $this->filtered_wp_submenu = $submenu;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  }
296
  }
297
 
298
  /**
299
- * Activate the 'menu_order' filter.
300
  *
301
- * @return bool
 
302
  */
303
- function hook_custom_menu_order(){
304
- return true;
 
 
 
 
 
 
 
 
 
305
  }
306
 
307
  /**
308
- * Override the order of the top-level menu entries.
309
  *
310
- * @param array $menu_order
311
- * @return array
312
- */
313
- function hook_menu_order($menu_order){
314
- if (empty($this->custom_menu)){
315
- return $menu_order;
316
- }
317
- $custom_menu_order = array();
318
- foreach($this->filtered_wp_menu as $topmenu){
319
- $filename = $topmenu[2];
320
- if ( in_array($filename, $menu_order) ){
321
- $custom_menu_order[] = $filename;
322
- }
323
- }
324
- return $custom_menu_order;
325
- }
326
-
327
- /**
328
- * Determine if the current user may use the menu editor.
329
- *
330
- * @return bool
331
- */
332
- function current_user_can_edit_menu(){
333
- if ( $this->is_super_plugin() ){
334
- return is_super_admin();
335
- } else {
336
- return current_user_can('manage_options');
337
- }
338
- }
339
-
340
- /**
341
- * Intercept a handy action to fix the page title for moved plugin pages.
342
- *
343
- * @return void
344
- */
345
- function hook_admin_xml_ns(){
346
- global $title;
347
- if ( empty($title) ){
348
- $title = $this->get_real_page_title();
349
- }
350
- }
351
-
352
- /**
353
- * Fix the page title for move plugin pages.
354
- * The 'admin_title' filter is only available in WP 3.1+
355
- *
356
- * @param string $admin_title The current admin title.
357
- * @param string $title The current page title.
358
- * @return string New admin title.
359
  */
360
- function hook_admin_title($admin_title, $title){
361
- if ( empty($title) ){
362
- $admin_title = $this->get_real_page_title() . $admin_title;
363
- }
364
- return $admin_title;
365
  }
366
-
367
  /**
368
- * Get the correct page title for a plugin page that's been moved to a different menu.
369
- *
370
- * @return string
 
 
 
 
 
 
 
 
 
 
 
371
  */
372
- function get_real_page_title(){
373
- global $title;
374
- global $pagenow;
375
- global $plugin_page;
376
-
377
- if ( empty($title) && !empty($plugin_page) && !empty($pagenow) ){
378
- $file = sprintf('%s?page=%s', $pagenow, $plugin_page);
379
- if ( isset($this->title_lookups[$file]) ){
380
- $title = esc_html( strip_tags( $this->title_lookups[$file] ) );
 
 
 
 
 
 
 
 
 
 
 
381
  }
382
  }
383
-
384
- return $title;
385
- }
386
-
387
 
388
- /**
389
- * Loop over the Dashboard submenus and remove pages for which the current user does not have privs.
390
- *
391
- * @return void
392
- */
393
- function filter_menu(){
394
- global $menu, $submenu, $_wp_submenu_nopriv, $_wp_menu_nopriv;
395
-
396
- foreach ( array( 'submenu' ) as $sub_loop ) {
397
- foreach ($$sub_loop as $parent => $sub) {
398
- foreach ($sub as $index => $data) {
399
- if ( ! current_user_can($data[1]) ) {
400
- unset(${$sub_loop}[$parent][$index]);
401
- $_wp_submenu_nopriv[$parent][$data[2]] = true;
402
- }
 
403
  }
 
404
 
405
- if ( empty(${$sub_loop}[$parent]) )
406
- unset(${$sub_loop}[$parent]);
 
 
 
 
407
  }
408
  }
409
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
 
411
- /**
412
- * Encode a menu tree as JSON
413
- *
414
- * @param array $tree
415
- * @return string
416
- */
417
- function getMenuAsJS($tree){
418
- return $this->json_encode($tree);
419
- }
420
 
421
- /**
422
- * Convert a WP menu structure to an associative array
423
- *
424
- * @param array $item An element of the $menu array
425
- * @param integer $pos The position (index) of the menu item
426
- * @return array
427
- */
428
- function menu2assoc($item, $pos=0){
429
- $item = array(
430
- 'menu_title' => $item[0],
431
- 'access_level' => $item[1],
432
- 'file' => $item[2],
433
- 'page_title' => $item[3],
434
- 'css_class' => $item[4],
435
- 'hookname' => (isset($item[5])?$item[5]:''), //ID
436
- 'icon_url' => (isset($item[6])?$item[6]:''),
437
- 'position' => $pos,
438
- );
439
- $item['separator'] = strpos($item['css_class'], 'wp-menu-separator') !== false;
440
- //Flag plugin pages
441
- $item['is_plugin_page'] = (get_plugin_page_hook($item['file'], '') != null);
442
-
443
- return array_merge($this->templates['basic_defaults'], $item);
444
- }
445
 
446
- /**
447
- * Convert a WP submenu structure to an associative array
448
- *
449
- * @param array $item An element of the $submenu array
450
- * @param integer $pos The position (index) of that element
451
- * @param string $parent Parent file that this menu item belongs to.
452
- * @return array
453
- */
454
- function submenu2assoc($item, $pos = 0, $parent = ''){
455
- $item = array(
456
- 'menu_title' => $item[0],
457
- 'access_level' => $item[1],
458
- 'file' => $item[2],
459
- 'page_title' => (isset($item[3])?$item[3]:''),
460
- 'position' => $pos,
461
- );
462
- //Save the default parent menu
463
- $item['parent'] = $parent;
464
- //Flag plugin pages
465
- $item['is_plugin_page'] = (get_plugin_page_hook($item['file'], $parent) != null);
466
-
467
- return array_merge($this->templates['basic_defaults'], $item);
468
  }
469
 
470
- /**
471
- * Populate lookup arrays with default values from $menu and $submenu. Used later to merge
472
- * a custom menu with the native WordPress menu structure somewhat gracefully.
473
- *
474
- * @param array $menu
475
- * @param array $submenu
476
- * @return array An array with two elements containing menu and submenu defaults.
477
- */
478
- function build_lookups($menu, $submenu){
479
- //Process the top menu
480
- $menu_defaults = array();
481
- foreach($menu as $pos => $item){
482
- $item = $this->menu2assoc($item, $pos);
483
- $menu_defaults[$item['file']] = $item; //index by filename
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
  }
485
 
486
- //Process the submenu
487
- $submenu_defaults = array();
488
- foreach($submenu as $parent => $items){
489
- foreach($items as $pos => $item){
490
- $item = $this->submenu2assoc($item, $pos, $parent);
491
- //File itself is not guaranteed to be unique, so we use a surrogate ID to identify submenus.
492
- $uid = $this->unique_submenu_id($item['file'], $parent);
493
- $submenu_defaults[$uid] = $item;
494
- }
495
  }
496
 
497
- return array($menu_defaults, $submenu_defaults);
498
- }
499
 
500
- /**
501
- * Merge $menu and $submenu into the $tree. Adds/replaces defaults, inserts new items
502
- * and marks missing items as such.
503
- *
504
- * @param array $tree A menu in plugin's internal form
505
- * @param array $menu WordPress menu structure
506
- * @param array $submenu WordPress submenu structure
507
- * @return array Updated menu tree
508
- */
509
- function menu_merge($tree, $menu, $submenu){
510
- list($menu_defaults, $submenu_defaults) = $this->build_lookups($menu, $submenu);
511
 
512
- //Iterate over all menus and submenus and look up default values
513
- foreach ($tree as &$topmenu){
514
- $topfile = $this->get_menu_field($topmenu, 'file');
515
- //Is this menu present in the default WP menu?
516
- if (isset($menu_defaults[$topfile])){
517
- //Yes, load defaults from that item
518
- $topmenu['defaults'] = $menu_defaults[$topfile];
519
- //Note that the original item was used
520
- $menu_defaults[$topfile]['used'] = true;
521
- } else {
522
- //Record the menu as missing, unless it's a menu separator
523
- if ( empty($topmenu['separator']) ){
524
- $topmenu['missing'] = true;
525
- //[Nasty] Fill the 'defaults' array for menu's that don't have it.
526
- //This should never be required - saving a custom menu should set the defaults
527
- //for all menus it contains automatically.
528
- if ( empty($topmenu['defaults']) ){
529
- $tmp = $topmenu;
530
- $topmenu['defaults'] = $tmp;
531
- }
532
- }
533
- }
534
 
535
- if (isset($topmenu['items']) && is_array($topmenu['items'])) {
536
- //Iterate over submenu items
537
- foreach ($topmenu['items'] as $file => &$item){
538
- $uid = $this->unique_submenu_id($item, $topfile);
539
-
540
- //Is this item present in the default WP menu?
541
- if (isset($submenu_defaults[$uid])){
542
- //Yes, load defaults from that item
543
- $item['defaults'] = $submenu_defaults[$uid];
544
- $submenu_defaults[$uid]['used'] = true;
545
- } else {
546
- //Record as missing
547
- $item['missing'] = true;
548
- if ( empty($item['defaults']) ){
549
- $tmp = $item;
550
- $item['defaults'] = $tmp;
551
- }
552
- }
553
- }
554
- }
555
  }
556
 
557
- //If we don't unset these they will fuck up the next two loops where the same names are used.
558
- unset($topmenu);
559
- unset($item);
560
 
561
- //Note : Now we have some items marked as missing, and some items in lookup arrays
562
- //that are not marked as used. The missing items are handled elsewhere (e.g. tree2wp()),
563
- //but lets merge in the unused items now.
 
 
 
 
 
564
 
565
- //Find and merge unused toplevel menus
566
- foreach ($menu_defaults as $topfile => $topmenu){
567
- //Skip used menus and separators
568
- if ( !empty($topmenu['used']) || !empty($topmenu['separator'])) {
569
- continue;
570
- };
571
 
572
- //Found an unused item. Build the tree entry.
573
- $entry = $this->templates['blank_menu'];
574
- $entry['defaults'] = $topmenu;
575
- $entry['items'] = array(); //prepare a place for menu items, if any.
576
- //Note that this item is unused
577
- $entry['unused'] = true;
578
- //Add the new entry to the menu tree
579
- $tree[$topfile] = $entry;
580
- }
581
- unset($topmenu);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
 
583
- //Find and merge submenu items
584
- foreach($submenu_defaults as $uid => $item){
585
- if ( !empty($item['used']) ) continue;
586
- //Found an unused item. Build an entry and attach it under the default toplevel menu.
587
- $entry = $this->templates['blank_item'];
588
- $entry['defaults'] = $item;
589
- //Note that this item is unused
590
- $entry['unused'] = true;
591
-
592
- //Check if the toplevel menu exists
593
- if (isset($tree[$item['parent']])) {
594
- //Okay, insert the item.
595
- $tree[$item['parent']]['items'][$item['file']] = $entry;
596
- } else {
597
- //Ooops? This should never happen. Some kind of inconsistency?
598
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
599
  }
 
600
 
601
- //Resort the tree to ensure the found items are in the right spots
602
- $tree = $this->sort_menu_tree($tree);
 
603
 
604
- return $tree;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
605
  }
606
-
607
- /**
608
- * Generate an ID that uniquely identifies a given submenu item.
609
- *
610
- * @param string|array $file Menu item in question
611
- * @param string $parent Parent menu. Optional. If $file is an array, the function will try to get the parent value from $file['defaults'] instead.
612
- * @return string Unique ID
613
- */
614
- function unique_submenu_id($file, $parent = ''){
615
- if ( is_array($file) ){
616
- if ( isset($file['defaults']) && isset($file['defaults']['parent']) ){
617
- $parent = $file['defaults']['parent'];
 
 
618
  }
619
- $file = $this->get_menu_field($file, 'file');
620
- }
621
-
622
- if ( !empty($parent) ){
623
- return $parent . '::' . $file;
624
  } else {
625
- return $file;
 
626
  }
 
 
 
627
  }
628
 
629
- /**
630
- * Convert the WP menu structure to the internal representation. All properties set as defaults.
631
- *
632
- * @param array $menu
633
- * @param array $submenu
634
- * @return array Menu in the internal tree format.
635
- */
636
- function wp2tree($menu, $submenu){
637
- $tree = array();
638
- $separator_count = 0;
639
- foreach ($menu as $pos => $item){
640
-
641
- $tree_item = $this->templates['blank_menu'];
642
- $tree_item['defaults'] = $this->menu2assoc($item, $pos);
643
- $tree_item['separator'] = empty($item[2]) || empty($item[0]) || (strpos($item[4], 'wp-menu-separator') !== false);
644
-
645
- if ( empty($tree_item['defaults']['file']) ){
646
- $tree_item['defaults']['file'] = 'separator_'.$separator_count;
647
- $separator_count++;
648
  }
649
-
650
- //Attach submenu items
651
- $parent = $tree_item['defaults']['file'];
652
- if ( isset($submenu[$parent]) ){
653
- foreach($submenu[$parent] as $subitem_pos => $subitem){
654
- $tree_item['items'][$subitem[2]] = array_merge(
655
- $this->templates['blank_item'],
656
- array('defaults' => $this->submenu2assoc($subitem, $subitem_pos, $parent))
657
- );
658
- }
659
  }
660
-
661
- $tree[$parent] = $tree_item;
662
  }
663
 
664
- $tree = $this->sort_menu_tree($tree);
 
665
 
666
- return $tree;
 
 
 
 
 
 
 
 
 
 
 
 
667
  }
668
 
669
- /**
670
- * Set all undefined menu fields to the default value
671
- *
672
- * @param array $item Menu item in the plugin's internal form
673
- * @return array
674
- */
675
- function apply_defaults($item){
676
- foreach($item as $key => $value){
677
- //Is the field set?
678
- if ($value === null){
679
- //Use default, if available
680
- if (isset($item['defaults']) && isset($item['defaults'][$key])){
681
- $item[$key] = $item['defaults'][$key];
682
- }
683
- }
684
  }
685
- return $item;
686
  }
687
 
688
-
689
- /**
690
- * Apply custom menu filters to an item of the custom menu.
691
- *
692
- * Calls two types of filters :
693
- * 'custom_admin_$item_type' with the entire $item passed as the argument.
694
- * 'custom_admin_$item_type-$field' with the value of a single field of $item as the argument.
695
- *
696
- * Used when converting the current custom menu to a WP-format menu.
697
- *
698
- * @param array $item Associative array representing one menu item (either top-level or submenu).
699
- * @param string $item_type 'menu' or 'submenu'
700
- * @param mixed $extra Optional extra data to pass to hooks.
701
- * @return array Filtered menu item.
702
- */
703
- function apply_menu_filters($item, $item_type = '', $extra = null){
704
- if ( empty($item_type) ){
705
- //Only top-level menus have an icon
706
- $item_type = isset($item['icon_url'])?'menu':'submenu';
707
  }
708
-
709
- $item = apply_filters("custom_admin_{$item_type}", $item, $extra);
710
- foreach($item as $field => $value){
711
- $item[$field] = apply_filters("custom_admin_{$item_type}-$field", $value, $extra);
 
 
 
 
 
 
 
712
  }
713
-
714
- return $item;
715
  }
716
 
717
  /**
718
- * Get the value of a menu/submenu field.
719
- * Will return the corresponding value from the 'defaults' entry of $item if the
720
- * specified field is not set in the item itself.
721
  *
722
- * @param array $item
723
- * @param string $field_name
724
- * @param mixed $default Returned if the requested field is not set and is not listed in $item['defaults']. Defaults to null.
725
- * @return mixed Field value.
726
  */
727
- function get_menu_field($item, $field_name, $default = null){
728
- if ( isset($item[$field_name]) && ($item[$field_name] !== null) ){
729
- return $item[$field_name];
730
- } else {
731
- if ( isset($item['defaults']) && isset($item['defaults'][$field_name]) ){
732
- return $item['defaults'][$field_name];
733
- } else {
734
- return $default;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
735
  }
736
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
737
  }
738
 
739
  /**
740
- * Custom comparison function that compares menu items based on their position in the menu.
 
741
  *
742
- * @param array $a
743
- * @param array $b
744
- * @return int
 
745
  */
746
- function compare_position($a, $b){
747
- if ($a['position']!==null) {
748
- $p1 = $a['position'];
749
- } else {
750
- if ( isset($a['defaults']['position']) ){
751
- $p1 = $a['defaults']['position'];
752
- } else {
753
- $p1 = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
754
  }
755
  }
756
 
757
- if ($b['position']!==null) {
758
- $p2 = $b['position'];
759
- } else {
760
- if ( isset($b['defaults']['position']) ){
761
- $p2 = $b['defaults']['position'];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
762
  } else {
763
- $p2 = 0;
764
  }
765
  }
766
 
767
- return $p1 - $p2;
768
- }
769
-
770
- /**
771
- * Sort the menus and menu items of a given menu according to their positions
772
- *
773
- * @param array $tree A menu structure in the internal format
774
- * @return array Sorted menu in the internal format
775
- */
776
- function sort_menu_tree($tree){
777
  //Resort the tree to ensure the found items are in the right spots
778
- uasort($tree, array(&$this, 'compare_position'));
779
- //Resort all submenus as well
780
- foreach ($tree as &$topmenu){
781
- if (!empty($topmenu['items'])){
782
- uasort($topmenu['items'], array(&$this, 'compare_position'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
783
  }
 
 
784
  }
785
-
786
- return $tree;
 
 
 
 
787
  }
788
 
789
  /**
790
- * Convert internal menu representation to the form used by WP.
791
  *
792
- * Note : While this function doesn't cause any side effects of its own,
793
- * it executes several filters that may modify global state. Specifically,
794
- * IFrame-handling callbacks in 'extras.php' may insert items into the
795
- * global $menu and $submenu arrays.
 
 
796
  *
797
- * @param array $tree
798
- * @return array $menu and $submenu
 
 
 
799
  */
800
- function tree2wp($tree){
801
- $menu = array();
802
- $submenu = array();
803
- $title_lookup = array();
 
804
 
805
  //Sort the menu by position
806
- uasort($tree, array(&$this, 'compare_position'));
807
 
808
  //Prepare the top menu
809
  $first_nonseparator_found = false;
810
  foreach ($tree as $topmenu){
811
-
812
- //Skip missing menus, unless they're user-created and thus might point to a non-standard file
813
- $custom = $this->get_menu_field($topmenu, 'custom', false);
814
- if ( !empty($topmenu['missing']) && !$custom ) {
815
  continue;
816
- };
817
 
818
  //Skip leading menu separators. Fixes a superfluous separator showing up
819
  //in WP 3.0 (multisite mode) when there's a custom menu and the current user
820
  //can't access its first item ("Super Admin").
821
- if ( !empty($topmenu['separator']) && !$first_nonseparator_found ) continue;
822
-
 
823
  $first_nonseparator_found = true;
824
 
825
- //Menus that have both a custom icon URL and a "menu-icon-*" class will get two overlapping icons.
826
- //Fix this by automatically removing the class. The user can set a custom class attr. to override.
827
- if (
828
- ameMenuItem::is_default($topmenu, 'css_class')
829
- && !ameMenuItem::is_default($topmenu, 'icon_url')
830
- && !in_array($topmenu['icon_url'], array('', 'none', 'div')) //Skip "no custom icon" icons.
831
- ) {
832
- $new_classes = preg_replace('@\bmenu-icon-[^\s]+\b@', '', $topmenu['defaults']['css_class']);
833
- if ( $new_classes !== $topmenu['defaults']['css_class'] ) {
834
- $topmenu['css_class'] = $new_classes;
835
- }
836
- }
837
-
838
- //Apply defaults & filters
839
- $topmenu = $this->apply_defaults($topmenu);
840
- $topmenu = $this->apply_menu_filters($topmenu, 'menu');
841
 
842
- //Skip hidden entries
843
- if (!empty($topmenu['hidden'])) continue;
844
-
845
- //Build the menu structure that WP expects
846
- $menu[] = array(
847
- $topmenu['menu_title'],
848
- $topmenu['access_level'],
849
- $topmenu['file'],
850
- $topmenu['page_title'],
851
- $topmenu['css_class'],
852
- $topmenu['hookname'], //ID
853
- $topmenu['icon_url']
854
- );
855
 
856
  //Prepare the submenu of this menu
 
857
  if( !empty($topmenu['items']) ){
858
  $items = $topmenu['items'];
859
  //Sort by position
860
- uasort($items, array(&$this, 'compare_position'));
861
 
862
  foreach ($items as $item) {
863
-
864
- //Skip missing items, unless they're user-created
865
- $custom = $this->get_menu_field($item, 'custom', false);
866
- if ( !empty($item['missing']) && !$custom ) continue;
867
-
868
- //Special case : plugin pages that have been moved to a different menu.
869
- //If the file field hasn't already been modified, we'll need to adjust it
870
- //to point to the old parent. This is required because WP identifies
871
- //plugin pages using *both* the plugin file and the parent file.
872
- if ( $this->get_menu_field($item, 'is_plugin_page', false) && ($item['file'] === null) ){
873
- $default_parent = '';
874
- if ( isset($item['defaults']) && isset($item['defaults']['parent'])){
875
- $default_parent = $item['defaults']['parent'];
876
- }
877
- if ( $topmenu['file'] != $default_parent ){
878
- $item['file'] = $default_parent . '?page=' . $item['defaults']['file'];
879
- }
880
- }
881
-
882
- $item = $this->apply_defaults($item);
883
- $item = $this->apply_menu_filters($item, 'submenu', $topmenu['file']);
884
-
885
- //Skip hidden items
886
- if (!empty($item['hidden'])) {
887
  continue;
888
  }
889
-
890
- $submenu[$topmenu['file']][] = array(
891
- $item['menu_title'],
892
- $item['access_level'],
893
- $item['file'],
894
- $item['page_title'],
895
- );
896
-
897
- //Make a note of the page's correct title so we can fix it later
898
- //if necessary.
899
- $title_lookup[$item['file']] = $item['menu_title'];
900
  }
901
  }
 
 
 
 
 
 
 
 
 
902
  }
903
- return array($menu, $submenu, $title_lookup);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
904
  }
905
-
906
- /**
907
- * Upgrade a menu tree to the currently used structure
908
- * Does nothing if the menu is already up to date.
909
- *
910
- * @param array $tree
911
- * @return array
912
- */
913
- function upgrade_menu_structure($tree){
914
-
915
- //Append new fields, if any
916
- foreach($tree as &$menu){
917
- $menu = array_merge($this->templates['blank_menu'], $menu);
918
- $menu['defaults'] = array_merge($this->templates['basic_defaults'], $menu['defaults']);
919
-
920
- foreach($menu['items'] as $item_file => $item){
921
- $item = array_merge($this->templates['blank_item'], $item);
922
- $item['defaults'] = array_merge($this->templates['basic_defaults'], $item['defaults']);
923
- $menu['items'][$item_file] = $item;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
924
  }
925
  }
926
-
927
- return $tree;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
928
  }
929
 
930
  /**
@@ -933,424 +1177,264 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
933
  * @return void
934
  */
935
  function page_menu_editor(){
936
- global $menu, $submenu;
937
- global $wp_roles;
938
-
939
  if ( !$this->current_user_can_edit_menu() ){
940
- die("Access denied");
 
 
 
941
  }
942
 
943
  $action = isset($this->post['action']) ? $this->post['action'] : (isset($this->get['action']) ? $this->get['action'] : '');
944
  do_action('admin_menu_editor_header', $action);
945
-
946
- //Handle form submissions
947
- if (isset($this->post['data'])){
948
- check_admin_referer('menu-editor-form');
949
-
950
- //Try to decode a menu tree encoded as JSON
951
- $data = $this->json_decode($this->post['data'], true);
952
- if (!$data || (count($data) < 2) ){
953
- $fixed = stripslashes($this->post['data']);
954
- $data = $this->json_decode( $fixed, true );
955
- }
956
 
957
- $url = remove_query_arg('noheader');
958
- if ($data){
959
- //Ensure the user doesn't change the required capability to something they themselves don't have.
960
- if ( isset($data['options-general.php']['items']['menu_editor']) ){
961
- $item = $data['options-general.php']['items']['menu_editor'];
962
- if ( !empty($item['access_level']) && !current_user_can($item['access_level']) ){
963
- $item['access_level'] = null;
964
- $data['options-general.php']['items']['menu_editor'] = $item;
965
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
966
  }
967
 
968
  //Save the custom menu
969
- $this->options['custom_menu'] = $data;
970
- $this->save_options();
971
- //Redirect back to the editor and display the success message
972
- wp_redirect( add_query_arg('message', 1, $url) );
 
 
 
 
 
 
973
  } else {
974
- //Or redirect & display the error message
975
- wp_redirect( add_query_arg('message', 2, $url) );
 
 
 
 
 
 
976
  }
977
- die();
978
- }
979
 
980
- //Kindly remind the user to give me money
981
- if ( !apply_filters('admin_menu_editor_is_pro', false) ){
982
- $this->print_upgrade_notice();
983
- }
984
- ?>
985
- <div class="wrap">
986
- <h2>
987
- <?php echo apply_filters('admin_menu_editor-self_page_title', 'Menu Editor'); ?>
988
- </h2>
989
 
990
- <?php
991
-
992
- if ( !empty($this->get['message']) ){
993
- if ( intval($this->get['message']) == 1 ){
994
- echo '<div id="message" class="updated fade"><p><strong>Settings saved.</strong></p></div>';
995
- } elseif ( intval($this->get['message']) == 2 ) {
996
- echo '<div id="message" class="error"><p><strong>Failed to decode input! The menu wasn\'t modified.</strong></p></div>';
997
- }
998
- }
999
-
1000
- //Build a tree struct. for the default menu
1001
- $default_menu = $this->wp2tree($this->default_wp_menu, $this->default_wp_submenu);
1002
-
1003
- //Is there a custom menu?
1004
- if (!empty($this->custom_menu)){
1005
- $custom_menu = $this->custom_menu;
1006
- } else {
1007
- //Start out with the default menu if there is no user-created one
1008
- $custom_menu = $default_menu;
1009
- }
1010
-
1011
- //Encode both menus as JSON
1012
- $default_menu_js = $this->getMenuAsJS($default_menu);
1013
- $custom_menu_js = $this->getMenuAsJS($custom_menu);
1014
-
1015
- $plugin_url = $this->plugin_dir_url;
1016
- $images_url = plugins_url('images', $this->plugin_file);
1017
-
1018
- $icons = array(
1019
- 'cut' => '/gnome-icon-theme/edit-cut-blue.png',
1020
- 'copy' => '/gion/edit-copy.png',
1021
- 'paste' => '/gnome-icon-theme/edit-paste.png',
1022
- 'hide' => '/icon-extension-grey.png',
1023
- 'new' => '/page-add.png',
1024
- 'delete' => '/page-delete.png',
1025
- 'new-separator' => '/separator-add.png',
1026
- );
1027
- foreach($icons as $name => $url) {
1028
- $icons[$name] = $images_url . $url;
1029
- }
1030
-
1031
- //Create a list of all known capabilities and roles. Used for the dropdown list on the access field.
1032
- $all_capabilities = $this->get_all_capabilities();
1033
- //"level_X" capabilities are deprecated so we don't want people using them.
1034
- //This would look better with array_filter() and an anonymous function as a callback.
1035
- for($level = 0; $level <= 10; $level++){
1036
- $cap = 'level_' . $level;
1037
- if ( isset($all_capabilities[$cap]) ){
1038
- unset($all_capabilities[$cap]);
1039
- }
1040
- }
1041
- $all_capabilities = array_keys($all_capabilities);
1042
- natcasesort($all_capabilities);
1043
-
1044
- $all_roles = $this->get_all_roles();
1045
- //Multi-site installs also get the virtual "Super Admin" role
1046
- if ( is_multisite() ){
1047
- $all_roles['super_admin'] = 'Super Admin';
1048
- }
1049
- asort($all_roles);
1050
- ?>
1051
- <div id='ws_menu_editor'>
1052
- <div class='ws_main_container'>
1053
- <div class='ws_toolbar'>
1054
- <div class="ws_button_container">
1055
- <a id='ws_cut_menu' class='ws_button' href='javascript:void(0)' title='Cut'><img src='<?php echo $icons['cut']; ?>' alt="Cut" /></a>
1056
- <a id='ws_copy_menu' class='ws_button' href='javascript:void(0)' title='Copy'><img src='<?php echo $icons['copy']; ?>' alt="Copy" /></a>
1057
- <a id='ws_paste_menu' class='ws_button' href='javascript:void(0)' title='Paste'><img src='<?php echo $icons['paste']; ?>' alt="Paste" /></a>
1058
-
1059
- <div class="ws_separator">&nbsp;</div>
1060
 
1061
- <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>
1062
- <a id='ws_hide_menu' class='ws_button' href='javascript:void(0)' title='Show/Hide'><img src='<?php echo $icons['hide']; ?>' alt="Show/Hide" /></a>
1063
- <a id='ws_delete_menu' class='ws_button' href='javascript:void(0)' title='Delete menu'><img src='<?php echo $icons['delete']; ?>' alt="Delete menu" /></a>
1064
-
1065
- <div class="ws_separator">&nbsp;</div>
 
 
 
1066
 
1067
- <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>
1068
- </div>
1069
- </div>
1070
-
1071
- <div id='ws_menu_box' class="ws_box">
1072
- </div>
1073
- </div>
1074
- <div class='ws_main_container'>
1075
- <div class='ws_toolbar'>
1076
- <div class="ws_button_container">
1077
- <a id='ws_cut_item' class='ws_button' href='javascript:void(0)' title='Cut'><img src='<?php echo $icons['cut']; ?>' alt="Cut" /></a>
1078
- <a id='ws_copy_item' class='ws_button' href='javascript:void(0)' title='Copy'><img src='<?php echo $icons['copy']; ?>' alt="Copy" /></a>
1079
- <a id='ws_paste_item' class='ws_button' href='javascript:void(0)' title='Paste'><img src='<?php echo $icons['paste']; ?>' alt="Paste" /></a>
1080
-
1081
- <div class="ws_separator">&nbsp;</div>
1082
 
1083
- <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>
1084
- <a id='ws_hide_item' class='ws_button' href='javascript:void(0)' title='Show/Hide'><img src='<?php echo $icons['hide']; ?>' alt="Show/Hide" /></a>
1085
- <a id='ws_delete_item' class='ws_button' href='javascript:void(0)' title='Delete menu item'><img src='<?php echo $icons['delete']; ?>' alt="Delete menu item" /></a>
1086
-
1087
- <div class="ws_separator">&nbsp;</div>
1088
-
1089
- <a id='ws_sort_ascending' class='ws_button' href='javascript:void(0)' title='Sort ascending'>
1090
- <img src='<?php echo $images_url; ?>/sort_ascending.png' alt="Sort ascending" />
1091
- </a>
1092
- <a id='ws_sort_descending' class='ws_button' href='javascript:void(0)' title='Sort descending'>
1093
- <img src='<?php echo $images_url; ?>/sort_descending.png' alt="Sort descending" />
1094
- </a>
1095
- </div>
1096
- </div>
1097
-
1098
- <div id='ws_submenu_box' class="ws_box">
1099
- </div>
1100
- </div>
1101
- </div>
1102
-
1103
- <div class="ws_main_container" id="ws_editor_sidebar">
1104
- <form method="post" action="<?php echo admin_url('options-general.php?page=menu_editor&noheader=1'); ?>" id='ws_main_form' name='ws_main_form'>
1105
- <?php wp_nonce_field('menu-editor-form'); ?>
1106
- <input type="hidden" name="data" id="ws_data" value="">
1107
- <input type="button" id='ws_save_menu' class="button-primary ws_main_button" value="Save Changes" />
1108
- </form>
1109
-
1110
- <input type="button" id='ws_reset_menu' value="Undo changes" class="button ws_main_button" />
1111
- <input type="button" id='ws_load_menu' value="Load default menu" class="button ws_main_button" />
1112
-
1113
- <?php
1114
- do_action('admin_menu_editor_sidebar');
1115
- ?>
1116
- </div>
1117
-
1118
- <?php
1119
- $show_hints = $this->get_hint_visibility();
1120
- $hint_id = 'ws_sidebar_pro_ad';
1121
- $show_pro_benefits = !apply_filters('admin_menu_editor_is_pro', false) && (!isset($show_hints[$hint_id]) || $show_hints[$hint_id]);
1122
- if ( $show_pro_benefits ):
1123
- $benefit_variations = array(
1124
- 'Simplified, role-based permissions.',
1125
- 'Role-based menu permissions',
1126
- 'Per-role menu permissions',
1127
- );
1128
- //Pseudo-randomly select one phrase based on the site URL.
1129
- $variation_index = hexdec( substr(md5(get_site_url()), -1) ) % count($benefit_variations);
1130
- $selected_variation = $benefit_variations[$variation_index];
1131
-
1132
- $pro_version_link = 'http://adminmenueditor.com/upgrade-to-pro/?utm_source=Admin%2BMenu%2BEditor%2Bfree&utm_medium=text_link&utm_content=sidebar_link_bv' . $variation_index . '&utm_campaign=Plugins';
1133
- ?>
1134
- <div class="clear"></div>
1135
-
1136
- <div class="ws_hint" id="<?php echo esc_attr($hint_id); ?>">
1137
- <div class="ws_hint_close" title="Close">x</div>
1138
- <div class="ws_hint_content">
1139
- <strong>Upgrade to Pro:</strong>
1140
- <ul>
1141
- <li><?php echo $selected_variation; ?></li>
1142
- <li>Drag items between menu levels.</li>
1143
- <li>Menu export &amp; import.</li>
1144
- </ul>
1145
- <a href="<?php echo esc_attr($pro_version_link); ?>" target="_blank">Learn more</a>
1146
- </div>
1147
- </div>
1148
- <?php
1149
- endif;
1150
- ?>
1151
 
1152
- </div>
 
1153
 
1154
- <?php
1155
- //Create a pop-up capability selector
1156
- $capSelector = array('<select id="ws_cap_selector" class="ws_dropdown" size="10">');
1157
-
1158
- $capSelector[] = '<optgroup label="Roles">';
1159
- foreach($all_roles as $role_id => $role_name){
1160
- $capSelector[] = sprintf(
1161
- '<option value="%s">%s</option>',
1162
- esc_attr($role_id),
1163
- $role_name
1164
- );
1165
- }
1166
- $capSelector[] = '</optgroup>';
1167
-
1168
- $capSelector[] = '<optgroup label="Capabilities">';
1169
- foreach($all_capabilities as $cap){
1170
- $capSelector[] = sprintf(
1171
- '<option value="%s">%s</option>',
1172
- esc_attr($cap),
1173
- $cap
1174
- );
1175
- }
1176
- $capSelector[] = '</optgroup>';
1177
- $capSelector[] = '</select>';
1178
-
1179
- echo implode("\n", $capSelector);
1180
-
1181
- //Create a pop-up page selector
1182
- $pageSelector = array('<select id="ws_page_selector" class="ws_dropdown" size="10">');
1183
- foreach($default_menu as $toplevel){
1184
- if ( $toplevel['separator'] ) continue;
1185
-
1186
- $top_title = strip_tags( preg_replace('@<span[^>]*>.*</span>@i', '', $this->get_menu_field($toplevel, 'menu_title')) );
1187
-
1188
- if ( empty($toplevel['items'])) {
1189
- //This menu has no items, so it can only link to itself
1190
- $pageSelector[] = sprintf(
1191
- '<option value="%s">%s -&gt; %s</option>',
1192
- esc_attr($this->get_menu_field($toplevel, 'file')),
1193
- $top_title,
1194
- $top_title
1195
- );
1196
- } else {
1197
- //When a menu has some items, it's own URL is ignored by WP and the first item is used instead.
1198
- foreach($toplevel['items'] as $subitem){
1199
- $sub_title = strip_tags( preg_replace('@<span[^>]*>.*</span>@i', '', $this->get_menu_field($subitem, 'menu_title')) );
1200
-
1201
- $pageSelector[] = sprintf(
1202
- '<option value="%s">%s -&gt; %s</option>',
1203
- esc_attr($this->get_menu_field($subitem, 'file')),
1204
- $top_title,
1205
- $sub_title
1206
- );
1207
- }
1208
- }
1209
- }
1210
-
1211
- $pageSelector[] = '</select>';
1212
- echo implode("\n", $pageSelector);
1213
- ?>
1214
-
1215
- <!-- Menu icon selector widget -->
1216
- <div id="ws_icon_selector" style="display: none;">
1217
- <?php
1218
- //Let the user select a custom icon via the media uploader.
1219
- //We only support the new WP 3.5+ media API. Hence the function_exists() check.
1220
- if ( function_exists('wp_enqueue_media') ):
1221
- ?>
1222
- <input type="button" class="button"
1223
- id="ws_choose_icon_from_media"
1224
- title="Upload an image or choose one from your media library"
1225
- value="Choose Icon">
1226
- <div class="clear"></div>
1227
- <?php
1228
- endif;
1229
- ?>
1230
-
1231
- <?php
1232
- $defaultWpIcons = array(
1233
- 'generic', 'dashboard', 'post', 'media', 'links', 'page', 'comments',
1234
- 'appearance', 'plugins', 'users', 'tools', 'settings', 'site',
1235
- );
1236
- foreach($defaultWpIcons as $icon) {
1237
- printf(
1238
- '<div class="ws_icon_option" title="%1$s" data-icon-class="menu-icon-%2$s">
1239
- <div class="ws_icon_image icon16 icon-%2$s"><br></div>
1240
- </div>',
1241
- esc_attr(ucwords($icon)),
1242
- $icon
1243
- );
1244
  }
1245
 
1246
- $defaultIconImages = array(
1247
- 'images/generic.png',
1248
- );
1249
- foreach($defaultIconImages as $icon) {
1250
- printf(
1251
- '<div class="ws_icon_option" data-icon-url="%1$s">
1252
- <img src="%1$s">
1253
- </div>',
1254
- esc_attr($icon)
1255
  );
1256
- }
1257
- ?>
1258
- <div class="ws_icon_option ws_custom_image_icon" title="Custom image" style="display: none;">
1259
- <img src="<?php echo esc_attr(admin_url('images/loading.gif')); ?>" alt="Custom image">
1260
- </div>
1261
- <div class="clear"></div>
1262
- </div>
1263
 
1264
- <span id="ws-ame-screen-meta-contents" style="display:none;">
1265
- <label for="ws-hide-advanced-settings">
1266
- <input type="checkbox" id="ws-hide-advanced-settings"<?php
1267
- if ( $this->options['hide_advanced_settings'] ){
1268
- echo ' checked="checked"';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1269
  }
1270
- ?> /> Hide advanced options
1271
- </label>
1272
- </span>
1273
 
1274
- <script type='text/javascript'>
 
 
 
 
1275
 
1276
- var defaultMenu = <?php echo $default_menu_js; ?>;
1277
- var customMenu = <?php echo $custom_menu_js; ?>;
 
 
1278
 
1279
- var imagesUrl = "<?php echo esc_js($images_url); ?>";
 
1280
 
1281
- var adminAjaxUrl = "<?php echo esc_js(admin_url('admin-ajax.php')); ?>";
 
1282
 
1283
- var hideAdvancedSettings = <?php echo $this->options['hide_advanced_settings']?'true':'false'; ?>;
1284
- var hideAdvancedSettingsNonce = '<?php echo esc_js(wp_create_nonce('ws_ame_save_screen_options')); ?>';
 
 
 
 
 
 
1285
 
1286
- var captionShowAdvanced = 'Show advanced options';
1287
- var captionHideAdvanced = 'Hide advanced options';
1288
 
1289
- window.wsMenuEditorPro = false; //Will be overwritten if extras are loaded
 
 
 
 
 
 
 
1290
 
1291
- </script>
 
 
 
 
 
 
 
 
 
1292
 
1293
- <?php
1294
-
1295
- //Let the Pro version script output it's extra HTML & scripts.
1296
- do_action('admin_menu_editor_footer');
 
 
 
 
 
1297
  }
1298
 
1299
- /**
1300
- * Retrieve a list of all known capabilities of all roles
1301
- *
1302
- * @return array Associative array with capability names as keys
1303
- */
1304
- function get_all_capabilities(){
1305
- /** @var WP_Roles $wp_roles */
1306
- global $wp_roles;
1307
-
1308
- $capabilities = array();
1309
-
1310
- if ( !isset($wp_roles) || !isset($wp_roles->roles) ){
1311
- return $capabilities;
 
 
1312
  }
1313
-
1314
- //Iterate over all known roles and collect their capabilities
1315
- foreach($wp_roles->roles as $role){
1316
- if ( !empty($role['capabilities']) && is_array($role['capabilities']) ){ //Being defensive here
1317
- $capabilities = array_merge($capabilities, $role['capabilities']);
1318
- }
1319
  }
1320
-
1321
- //Add multisite-specific capabilities (not listed in any roles in WP 3.0)
1322
- $multisite_caps = array(
1323
- 'manage_sites' => 1,
1324
- 'manage_network' => 1,
1325
- 'manage_network_users' => 1,
1326
- 'manage_network_themes' => 1,
1327
- 'manage_network_options' => 1,
1328
- 'manage_network_plugins' => 1,
1329
- );
1330
- $capabilities = array_merge($capabilities, $multisite_caps);
1331
-
1332
- return $capabilities;
1333
  }
1334
-
1335
- /**
1336
- * Retrieve a list of all known roles
1337
- *
1338
- * @return array Associative array with role IDs as keys and role display names as values
1339
- */
1340
- function get_all_roles(){
1341
- /** @var WP_Roles $wp_roles */
1342
- global $wp_roles;
1343
- $roles = array();
1344
-
1345
- if ( !isset($wp_roles) || !isset($wp_roles->roles) ){
1346
- return $roles;
 
1347
  }
1348
-
1349
- foreach($wp_roles->roles as $role_id => $role){
1350
- $roles[$role_id] = $role['name'];
1351
  }
1352
-
1353
- return $roles;
1354
  }
1355
 
1356
  /**
@@ -1371,25 +1455,6 @@ window.wsMenuEditorPro = false; //Will be overwritten if extras are loaded
1371
  return $allcaps;
1372
  }
1373
 
1374
- /**
1375
- * Output the "Upgrade to Pro" message
1376
- *
1377
- * @return void
1378
- */
1379
- function print_upgrade_notice(){
1380
- ?>
1381
- <script type="text/javascript">
1382
- (function($){
1383
- $('#screen-meta-links').append(
1384
- '<div id="ws-pro-version-notice">' +
1385
- '<a href="http://adminmenueditor.com/?utm_source=Admin%2BMenu%2BEditor%2Bfree&utm_medium=text_link&utm_content=top_upgrade_link&utm_campaign=Plugins" id="ws-pro-version-notice-link" class="show-settings" target="_blank" title="View Pro version details">Upgrade to Pro</a>' +
1386
- '</div>'
1387
- );
1388
- })(jQuery);
1389
- </script>
1390
- <?php
1391
- }
1392
-
1393
  /**
1394
  * AJAX callback for saving screen options (whether to show or to hide advanced menu options).
1395
  *
@@ -1399,7 +1464,7 @@ window.wsMenuEditorPro = false; //Will be overwritten if extras are loaded
1399
  * @return void
1400
  */
1401
  function ajax_save_screen_options(){
1402
- if (!current_user_can('manage_options') || !check_ajax_referer('ws_ame_save_screen_options', false, false)){
1403
  die( $this->json_encode( array(
1404
  'error' => "You're not allowed to do that!"
1405
  )));
@@ -1431,7 +1496,7 @@ window.wsMenuEditorPro = false; //Will be overwritten if extras are loaded
1431
 
1432
  $defaults = array(
1433
  'ws_sidebar_pro_ad' => true,
1434
- //'ws_whats_new_120' => true, //Set upon activation, default not needed.
1435
  'ws_hint_menu_permissions' => true,
1436
  );
1437
 
@@ -1448,22 +1513,216 @@ window.wsMenuEditorPro = false; //Will be overwritten if extras are loaded
1448
  * would not be highlighted properly when the user visits them.
1449
  */
1450
  public function enqueue_menu_fix_script() {
1451
- wp_enqueue_script(
1452
  'ame-menu-fix',
1453
  plugins_url('js/menu-highlight-fix.js', $this->plugin_file),
1454
  array('jquery'),
1455
- '20120915',
1456
  true
1457
  );
1458
  }
1459
-
1460
  /**
1461
- * A callback for the stub meta box added to the plugin's page. Does nothing.
1462
- *
1463
- * @return void
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1464
  */
1465
- function noop(){
1466
- //nihil
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1467
  }
1468
 
1469
  public function display_survey_notice() {
@@ -1480,8 +1739,15 @@ window.wsMenuEditorPro = false; //Will be overwritten if extras are loaded
1480
  $display_notice = $display_notice && ((time() - $this->options['first_install_time']) > $minimum_usage_period);
1481
  }
1482
 
1483
- //Only display the notice on the Menu Editor page.
1484
  $display_notice = $display_notice && isset($this->get['page']) && ($this->get['page'] == 'menu_editor');
 
 
 
 
 
 
 
1485
 
1486
  if ( $display_notice ) {
1487
  $free_survey_url = 'https://docs.google.com/spreadsheet/viewform?formkey=dERyeDk0OWhlbkxYcEY4QTNaMnlTQUE6MQ';
@@ -1497,8 +1763,8 @@ window.wsMenuEditorPro = false; //Will be overwritten if extras are loaded
1497
  printf(
1498
  '<div class="updated">
1499
  <p><strong>Help improve Admin Menu Editor - take the user survey!</strong></p>
1500
- <p><a href="%s" target="_blank" title="Opens in a new window">Take the survey</a></p>
1501
- <p><a href="%s">Hide this notice</a></p>
1502
  </div>',
1503
  esc_attr($survey_url),
1504
  esc_attr($hide_url)
@@ -1522,8 +1788,168 @@ window.wsMenuEditorPro = false; //Will be overwritten if extras are loaded
1522
  }
1523
  }
1524
 
1525
- } //class
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1526
 
1527
- endif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1528
 
1529
- ?>
8
  );
9
  }
10
 
 
11
  $thisDirectory = dirname(__FILE__);
12
  require $thisDirectory . '/shadow_plugin_framework.php';
13
+ require $thisDirectory . '/role-utils.php';
14
  require $thisDirectory . '/menu-item.php';
15
+ require $thisDirectory . '/menu.php';
16
+ require $thisDirectory . '/auto-versioning.php';
17
 
18
  class WPMenuEditor extends MenuEd_ShadowPluginFramework {
19
+ const WPML_CONTEXT = 'admin-menu-editor menu texts';
20
+
21
+ private $plugin_db_version = 140;
22
 
23
+ /** @var array The default WordPress menu, before display-specific filtering. */
24
+ protected $default_wp_menu;
25
+ /** @var array The default WordPress submenu. */
26
+ protected $default_wp_submenu;
27
+
28
+ /**
29
+ * We also keep track of the final, ready-for-display version of the default WP menu
30
+ * and submenu. These values are captured *just* before the admin menu HTML is output
31
+ * by _wp_menu_output() in /wp-admin/menu-header.php, and are restored afterwards.
32
+ */
33
+ private $old_wp_menu;
34
+ private $old_wp_submenu;
35
 
36
+ private $title_lookups = array(); //A list of page titles indexed by $item['file']. Used to
37
  //fix the titles of moved plugin pages.
38
+ private $reverse_item_lookup = array(); //Contains the final (merged & filtered) list of admin menu items,
39
+ //indexed by URL.
40
+
41
+ /**
42
+ * @var array List of per-URL capabilities, indexed by priority. Used while merging and
43
+ * building the final admin menu.
44
+ */
45
+ private $page_access_lookup = array();
46
+
47
+ /**
48
+ * @var array A log of menu access checks.
49
+ */
50
+ private $security_log = array();
51
+
52
+ /**
53
+ * @var array The current custom menu with defaults merged in.
54
+ */
55
+ private $merged_custom_menu = null;
56
+
57
+ /**
58
+ * @var array The custom menu in WP-compatible format (top-level).
59
+ */
60
+ private $custom_wp_menu = null;
61
+
62
+ /**
63
+ * @var array The custom menu in WP-compatible format (sub-menu).
64
+ */
65
+ private $custom_wp_submenu = null;
66
+
67
+ private $item_templates = array(); //A lookup list of default menu items, used as templates for the custom menu.
68
+
69
+ private $cached_custom_menu = null; //Cached, non-merged version of the custom menu. Used by load_custom_menu().
70
+ private $cached_virtual_caps = null;//List of virtual caps. Used by get_virtual_caps().
71
 
72
  //Our personal copy of the request vars, without any "magic quotes".
73
  private $post = array();
74
  private $get = array();
75
 
76
  function init(){
77
+ $this->sitewide_options = true;
78
+
 
 
 
 
79
  //Set some plugin-specific options
80
  if ( empty($this->option_name) ){
81
  $this->option_name = 'ws_menu_editor';
82
  }
83
  $this->defaults = array(
84
  'hide_advanced_settings' => true,
85
+ 'custom_menu' => null,
 
86
  'first_install_time' => null,
87
+ 'display_survey_notice' => true,
88
+ 'plugin_db_version' => 0,
89
+ 'security_logging_enabled' => false,
90
+
91
+ 'menu_config_scope' => ($this->is_super_plugin() || !is_multisite()) ? 'global' : 'site',
92
+
93
+ //super_admin, specific_user, or a capability.
94
+ 'plugin_access' => $this->is_super_plugin() ? 'super_admin' : 'manage_options',
95
+ //The ID of the user who is allowed to use this plugin. Only used when plugin_access == specific_user.
96
+ 'allowed_user_id' => null,
97
+ //The user who can see this plugin on the "Plugins" page. By default all admins can see it.
98
+ 'plugins_page_allowed_user_id' => null,
99
  );
100
  $this->serialize_with_json = false; //(Don't) store the options in JSON format
101
 
104
  $this->magic_hooks = true;
105
  $this->magic_hook_priority = 99999;
106
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  //AJAXify screen options
108
+ add_action('wp_ajax_ws_ame_save_screen_options', array(&$this,'ajax_save_screen_options'));
109
 
110
  //AJAXify hints
111
  add_action('wp_ajax_ws_ame_hide_hint', array($this, 'ajax_hide_hint'));
113
  //Make sure we have access to the original, un-mangled request data.
114
  //This is necessary because WordPress will stupidly apply "magic quotes"
115
  //to the request vars even if this PHP misfeature is disabled.
116
+ $this->capture_request_vars();
117
 
118
  add_action('admin_enqueue_scripts', array($this, 'enqueue_menu_fix_script'));
119
 
120
+ //Enqueue miscellaneous helper scripts and styles.
121
+ add_action('admin_enqueue_scripts', array($this, 'enqueue_helper_scripts'));
122
+ add_action('admin_print_styles', array($this, 'enqueue_helper_styles'));
123
+
124
  //User survey
125
  add_action('admin_notices', array($this, 'display_survey_notice'));
126
  }
127
+
128
  function init_finish() {
129
  parent::init_finish();
130
+ $should_save_options = false;
131
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  //If we have no stored settings for this version of the plugin, try importing them
133
  //from other versions (i.e. the free or the Pro version).
134
  if ( !$this->load_options() ){
135
  $this->import_settings();
136
+ $should_save_options = true;
137
+ }
138
+
139
+ //Track first install time.
140
+ if ( !isset($this->options['first_install_time']) ) {
141
+ $this->options['first_install_time'] = time();
142
+ $should_save_options = true;
143
+ }
144
+
145
+ if ( $this->options['plugin_db_version'] < $this->plugin_db_version ) {
146
+ /* Put any activation code here. */
147
+
148
+ $this->options['plugin_db_version'] = $this->plugin_db_version;
149
+ $should_save_options = true;
150
+ }
151
+
152
+ if ( $should_save_options ) {
153
+ $this->save_options();
154
  }
155
 
156
+ //This is here and not in init() because it relies on $options being initialized.
157
+ if ( $this->options['security_logging_enabled'] ) {
158
+ add_action('admin_notices', array($this, 'display_security_log'));
159
+ }
160
  }
161
+
162
  /**
163
  * Import settings from a different version of the plugin.
164
  *
171
  return true;
172
  }
173
  }
 
174
  return false;
175
  }
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  /**
178
  * Create a configuration page and load the custom menu
179
  *
181
  */
182
  function hook_admin_menu(){
183
  global $menu, $submenu;
184
+
185
  //Menu reset (for emergencies). Executed by accessing http://example.com/wp-admin/?reset_admin_menu=1
186
  $reset_requested = isset($this->get['reset_admin_menu']) && $this->get['reset_admin_menu'];
187
  if ( $reset_requested && $this->current_user_can_edit_menu() ){
188
+ $this->set_custom_menu(null);
 
189
  }
190
 
191
  //The menu editor is only visible to users with the manage_options privilege.
192
  //Or, if the plugin is installed in mu-plugins, only to the site administrator(s).
193
  if ( $this->current_user_can_edit_menu() ){
194
+ $this->log_security_note('Current user can edit the admin menu.');
195
+
196
  $page = add_options_page(
197
  apply_filters('admin_menu_editor-self_page_title', 'Menu Editor'),
198
  apply_filters('admin_menu_editor-self_menu_title', 'Menu Editor'),
199
+ apply_filters('admin_menu_editor_capability', 'manage_options'),
200
  'menu_editor',
201
  array(&$this, 'page_menu_editor')
202
  );
203
  //Output our JS & CSS on that page only
204
  add_action("admin_print_scripts-$page", array(&$this, 'enqueue_scripts'));
205
  add_action("admin_print_styles-$page", array(&$this, 'enqueue_styles'));
206
+
207
+ //Compatibility fix for All In One Event Calendar; see the callback for details.
208
+ add_action("admin_print_scripts-$page", array($this, 'dequeue_ai1ec_scripts'));
209
+
210
  //Compatibility fix for Participants Database.
211
  add_action("admin_print_scripts-$page", array($this, 'dequeue_pd_scripts'));
212
+
213
+ //Experimental compatibility fix for Ultimate TinyMCE
214
+ add_action("admin_print_scripts-$page", array($this, 'remove_ultimate_tinymce_qtags'));
215
+
216
  //Make a placeholder for our screen options (hacky)
217
+ add_meta_box("ws-ame-screen-options", "[AME placeholder]", '__return_false', $page);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  }
219
 
220
  //Store the "original" menus for later use in the editor
221
  $this->default_wp_menu = $menu;
222
  $this->default_wp_submenu = $submenu;
223
 
224
+ //Generate item templates from the default menu.
225
+ $this->item_templates = $this->build_templates($this->default_wp_menu, $this->default_wp_submenu);
226
+
227
  //Is there a custom menu to use?
228
+ $custom_menu = $this->load_custom_menu();
229
+ if ( $custom_menu !== null ){
 
 
 
 
 
230
  //Merge in data from the default menu
231
+ $custom_menu['tree'] = $this->menu_merge($custom_menu['tree']);
232
+
233
+ //Save the merged menu for later - the editor page will need it
234
+ $this->merged_custom_menu = $custom_menu;
235
+
236
+ //Convert our custom menu to the $menu + $submenu structure used by WP.
237
+ //Note: This method sets up multiple internal fields and may cause side-effects.
238
+ $this->build_custom_wp_menu($this->merged_custom_menu['tree']);
239
+
240
+ if ( !$this->user_can_access_current_page() ) {
241
+ $this->log_security_note('DENY access.');
242
+ $message = 'You do not have sufficient permissions to access this admin page.';
243
+ if ( $this->options['security_logging_enabled'] ) {
244
+ $message .= '<p><strong>Admin Menu Editor security log</strong></p>';
245
+ $message .= $this->get_formatted_security_log();
246
+ }
247
+ wp_die($message);
248
+ } else {
249
+ $this->log_security_note('ALLOW access.');
250
+ }
251
+
252
+ //Replace the admin menu just before it is displayed and restore it afterwards.
253
+ //The fact that replace_wp_menu() is attached to the 'parent_file' hook is incidental;
254
+ //there just wasn't any other, more suitable hook available.
255
+ add_filter('parent_file', array($this, 'replace_wp_menu'));
256
+ add_action('adminmenu', array($this, 'restore_wp_menu'));
257
+
258
+ //A compatibility hack for Ozh's Admin Drop Down Menu. Make sure it also sees the modified menu.
259
+ $ozh_adminmenu_priority = has_action('in_admin_header', 'wp_ozh_adminmenu');
260
+ if ( $ozh_adminmenu_priority !== false ) {
261
+ add_action('in_admin_header', array($this, 'replace_wp_menu'), $ozh_adminmenu_priority - 1);
262
+ add_action('in_admin_header', array($this, 'restore_wp_menu'), $ozh_adminmenu_priority + 1);
263
+ }
264
  }
265
  }
266
 
267
  /**
268
+ * Replace the current WP menu with our custom one.
269
  *
270
+ * @param string $parent_file Ignored. Required because this method is a hook for the 'parent_file' filter.
271
+ * @return string Returns the $parent_file argument.
272
  */
273
+ public function replace_wp_menu($parent_file = '') {
274
+ global $menu, $submenu;
275
+
276
+ $this->old_wp_menu = $menu;
277
+ $this->old_wp_submenu = $submenu;
278
+
279
+ $menu = $this->custom_wp_menu;
280
+ $submenu = $this->custom_wp_submenu;
281
+ list($menu, $submenu) = $this->filter_menu($menu, $submenu);
282
+
283
+ return $parent_file;
284
  }
285
 
286
  /**
287
+ * Restore the default WordPress menu that was replaced using replace_wp_menu().
288
  *
289
+ * @return void
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  */
291
+ public function restore_wp_menu() {
292
+ global $menu, $submenu;
293
+ $menu = $this->old_wp_menu;
294
+ $submenu = $this->old_wp_submenu;
 
295
  }
296
+
297
  /**
298
+ * Filter a menu so that it can be handed to _wp_menu_output(). This method basically
299
+ * emulates the filtering that WordPress does in /wp-admin/includes/menu.php, with a few
300
+ * additions of our own.
301
+ *
302
+ * - Removes inaccessible items and superfluous separators.
303
+ *
304
+ * - Sets accessible items to a capability that the user is guaranteed to have to prevent
305
+ * _wp_menu_output() from choking on plugin-specific capabilities like "cap1,cap2+not:cap3".
306
+ *
307
+ * - Adds position-dependent CSS classes.
308
+ *
309
+ * @param array $menu
310
+ * @param array $submenu
311
+ * @return array An array with two items - the filtered menu and submenu.
312
  */
313
+ private function filter_menu($menu, $submenu) {
314
+ global $_wp_menu_nopriv; //Caution: Modifying this array could lead to unexpected consequences.
315
+
316
+ //Remove sub-menus which the user shouldn't be able to access,
317
+ //and ensure the rest are visible.
318
+ foreach ($submenu as $parent => $items) {
319
+ foreach ($items as $index => $data) {
320
+ if ( ! $this->current_user_can($data[1]) ) {
321
+ unset($submenu[$parent][$index]);
322
+ $_wp_submenu_nopriv[$parent][$data[2]] = true;
323
+ } else {
324
+ //The menu might be set to some kind of special capability that is only valid
325
+ //within this plugin and not WP in general. Ensure WP doesn't choke on it.
326
+ //(This is safe - we'll double-check the caps when the user tries to access a page.)
327
+ $submenu[$parent][$index][1] = 'exist'; //All users have the 'exist' cap.
328
+ }
329
+ }
330
+
331
+ if ( empty($submenu[$parent]) ) {
332
+ unset($submenu[$parent]);
333
  }
334
  }
 
 
 
 
335
 
336
+ //Remove menus that have no accessible sub-menus and require privileges that the user does not have.
337
+ //Ensure the rest are visible. Run re-parent loop again.
338
+ foreach ( $menu as $id => $data ) {
339
+ if ( ! $this->current_user_can($data[1]) ) {
340
+ $_wp_menu_nopriv[$data[2]] = true;
341
+ } else {
342
+ $menu[$id][1] = 'exist';
343
+ }
344
+
345
+ //If there is only one submenu and it is has same destination as the parent,
346
+ //remove the submenu.
347
+ if ( ! empty( $submenu[$data[2]] ) && 1 == count ( $submenu[$data[2]] ) ) {
348
+ $subs = $submenu[$data[2]];
349
+ $first_sub = array_shift($subs);
350
+ if ( $data[2] == $first_sub[2] ) {
351
+ unset( $submenu[$data[2]] );
352
  }
353
+ }
354
 
355
+ //If submenu is empty...
356
+ if ( empty($submenu[$data[2]]) ) {
357
+ // And user doesn't have privs, remove menu.
358
+ if ( isset( $_wp_menu_nopriv[$data[2]] ) ) {
359
+ unset($menu[$id]);
360
+ }
361
  }
362
  }
363
+ unset($id, $data, $subs, $first_sub);
364
+
365
+ //Remove any duplicated separators
366
+ $separator_found = false;
367
+ foreach ( $menu as $id => $data ) {
368
+ if ( 0 == strcmp('wp-menu-separator', $data[4] ) ) {
369
+ if ($separator_found) {
370
+ unset($menu[$id]);
371
+ }
372
+ $separator_found = true;
373
+ } else {
374
+ $separator_found = false;
375
+ }
376
+ }
377
+ unset($id, $data);
378
 
379
+ //Remove the last menu item if it is a separator.
380
+ $last_menu_key = array_keys( $menu );
381
+ $last_menu_key = array_pop( $last_menu_key );
382
+ if (!empty($menu) && 'wp-menu-separator' == $menu[$last_menu_key][4]) {
383
+ unset($menu[$last_menu_key]);
384
+ }
385
+ unset( $last_menu_key );
 
 
386
 
387
+ //Add display-specific classes like "menu-top-first" and others.
388
+ $menu = add_menu_classes($menu);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
 
390
+ return array($menu, $submenu);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  }
392
 
393
+
394
+ /**
395
+ * Add the JS required by the editor to the page header
396
+ *
397
+ * @return void
398
+ */
399
+ function enqueue_scripts(){
400
+ //jQuery JSON plugin
401
+ wp_register_auto_versioned_script('jquery-json', plugins_url('js/jquery.json.js', $this->plugin_file), array('jquery'));
402
+ //jQuery sort plugin
403
+ wp_register_auto_versioned_script('jquery-sort', plugins_url('js/jquery.sort.js', $this->plugin_file), array('jquery'));
404
+ //qTip2 - jQuery tooltip plugin
405
+ wp_register_auto_versioned_script('jquery-qtip', plugins_url('js/jquery.qtip.min.js', $this->plugin_file), array('jquery'));
406
+ //jQuery Form plugin. This is a more recent version than the one included with WP.
407
+ wp_register_auto_versioned_script('ame-jquery-form', plugins_url('js/jquery.form.js', $this->plugin_file), array('jquery'));
408
+
409
+ //Editor's scripts
410
+ wp_register_auto_versioned_script(
411
+ 'menu-editor',
412
+ plugins_url('js/menu-editor.js', $this->plugin_file),
413
+ array(
414
+ 'jquery', 'jquery-ui-sortable', 'jquery-ui-dialog',
415
+ 'ame-jquery-form', 'jquery-ui-droppable', 'jquery-qtip',
416
+ 'jquery-sort', 'jquery-json'
417
+ )
418
+ );
419
+
420
+ //Add scripts to our editor page, but not the settings sub-section
421
+ //that shares the same page slug. Some of the scripts would crash otherwise.
422
+ if ( !$this->is_editor_page() ) {
423
+ return;
424
  }
425
 
426
+ wp_enqueue_script('menu-editor');
427
+
428
+ //We use WordPress media uploader to let the user upload custom menu icons (WP 3.5+).
429
+ if ( function_exists('wp_enqueue_media') ) {
430
+ wp_enqueue_media();
 
 
 
 
431
  }
432
 
433
+ //Remove the default jQuery Form plugin to prevent conflicts with our custom version.
434
+ wp_dequeue_script('jquery-form');
435
 
436
+ //Actors (roles and users) are used in the permissions UI, so we need to pass them along.
437
+ $actors = array();
438
+ $roles = array();
 
 
 
 
 
 
 
 
439
 
440
+ $wp_roles = ameRoleUtils::get_roles();
441
+ foreach($wp_roles->roles as $role_id => $role) {
442
+ $actors['role:' . $role_id] = $role['name'];
443
+ $role['capabilities'] = $this->castValuesToBool($role['capabilities']);
444
+ $roles[$role_id] = $role;
445
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
 
447
+ if ( is_multisite() && is_super_admin() ) {
448
+ $actors['special:super_admin'] = 'Super Admin';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  }
450
 
451
+ //Known users. Right now, this is limited to the current user only.
452
+ $users = array();
 
453
 
454
+ $current_user = wp_get_current_user();
455
+ $users[$current_user->get('user_login')] = array(
456
+ 'user_login' => $current_user->get('user_login'),
457
+ 'id' => $current_user->ID,
458
+ 'roles' => array_values($current_user->roles),
459
+ 'capabilities' => $this->castValuesToBool($current_user->caps),
460
+ 'is_super_admin' => is_multisite() && is_super_admin(),
461
+ );
462
 
463
+ $actors['user:' . $current_user->get('user_login')] = sprintf(
464
+ 'Current user (%s)',
465
+ $current_user->get('user_login')
466
+ );
467
+ //Note: Users do NOT get added to the actor list because that feature
468
+ //is not fully implemented.
469
 
470
+ //The editor will need access to some of the plugin data and WP data.
471
+ wp_localize_script(
472
+ 'menu-editor',
473
+ 'wsEditorData',
474
+ array(
475
+ 'imagesUrl' => plugins_url('images', $this->plugin_file),
476
+ 'adminAjaxUrl' => admin_url('admin-ajax.php'),
477
+ 'hideAdvancedSettings' => (boolean)$this->options['hide_advanced_settings'],
478
+ 'hideAdvancedSettingsNonce' => wp_create_nonce('ws_ame_save_screen_options'),
479
+ 'captionShowAdvanced' => 'Show advanced options',
480
+ 'captionHideAdvanced' => 'Hide advanced options',
481
+ 'wsMenuEditorPro' => false, //Will be overwritten if extras are loaded
482
+ 'menuFormatName' => ameMenu::format_name,
483
+ 'menuFormatVersion' => ameMenu::format_version,
484
+
485
+ 'blankMenuItem' => ameMenuItem::blank_menu(),
486
+ 'itemTemplates' => $this->item_templates,
487
+ 'customItemTemplate' => array(
488
+ 'name' => '< Custom >',
489
+ 'defaults' => ameMenuItem::custom_item_defaults(),
490
+ ),
491
+
492
+ 'actors' => $actors,
493
+ 'roles' => $roles,
494
+ 'users' => $users,
495
+ 'currentUserLogin' => $current_user->get('user_login'),
496
+ 'selectedActor' => isset($this->get['selected_actor']) ? strval($this->get['selected_actor']) : null,
497
 
498
+ 'showHints' => $this->get_hint_visibility(),
499
+
500
+ 'isDemoMode' => defined('IS_DEMO_MODE'),
501
+ 'isMasterMode' => defined('IS_MASTER_MODE'),
502
+ )
503
+ );
504
+ }
505
+
506
+ /**
507
+ * Compatibility workaround for All In One Event Calendar 1.8.3-premium.
508
+ *
509
+ * The event calendar plugin is known to crash Admin Menu Editor Pro 1.40. The exact cause
510
+ * of the crash is unknown, but we can prevent it by removing AIOEC scripts from the menu
511
+ * editor page.
512
+ *
513
+ * This should not affect the functionality of the event calendar plugin. The scripts
514
+ * in question don't seem to do anything on pages not related to the event calendar. AIOEC
515
+ * just loads them indiscriminately on all pages.
516
+ */
517
+ public function dequeue_ai1ec_scripts() {
518
+ wp_dequeue_script('ai1ec_requirejs');
519
+ wp_dequeue_script('ai1ec_common_backend');
520
+ wp_dequeue_script('ai1ec_add_new_event_require');
521
+ }
522
+
523
+ /**
524
+ * Compatibility workaround for Participants Database 1.4.5.2.
525
+ *
526
+ * Participants Database loads its settings JavaScript on every page in the "Settings" menu,
527
+ * not just its own. It doesn't bother to also load the script's dependencies, though, so
528
+ * the script crashes *and* it breaks the menu editor by way of collateral damage.
529
+ *
530
+ * Fix by forcibly removing the offending script from the queue.
531
+ */
532
+ public function dequeue_pd_scripts() {
533
+ if ( is_plugin_active('participants-database/participants-database.php') ) {
534
+ wp_dequeue_script('settings_script');
535
  }
536
+ }
537
 
538
+ public function remove_ultimate_tinymce_qtags() {
539
+ remove_action('admin_print_footer_scripts', 'jwl_ult_quicktags');
540
+ }
541
 
542
+ /**
543
+ * Add the editor's CSS file to the page header
544
+ *
545
+ * @return void
546
+ */
547
+ function enqueue_styles(){
548
+ wp_enqueue_auto_versioned_style('jquery-qtip-syle', plugins_url('css/jquery.qtip.min.css', $this->plugin_file), array());
549
+
550
+ wp_register_auto_versioned_style('menu-editor-base-style', plugins_url('css/menu-editor.css', $this->plugin_file));
551
+ wp_register_auto_versioned_style(
552
+ 'menu-editor-colours-classic',
553
+ plugins_url('css/style-classic.css', $this->plugin_file),
554
+ array('menu-editor-base-style')
555
+ );
556
+ wp_register_auto_versioned_style(
557
+ 'menu-editor-colours-wp-gray',
558
+ plugins_url('css/style-wp-gray.css', $this->plugin_file),
559
+ array('menu-editor-base-style')
560
+ );
561
+
562
+ wp_enqueue_style('menu-editor-colours-classic');
563
  }
564
+
565
+ /**
566
+ * Set and save a new custom menu for the current site.
567
+ *
568
+ * @param array|null $custom_menu
569
+ */
570
+ function set_custom_menu($custom_menu) {
571
+ $previous_custom_menu = $this->load_custom_menu();
572
+ $this->update_wpml_strings($previous_custom_menu, $custom_menu);
573
+
574
+ if ( $this->should_use_site_specific_menu() ) {
575
+ $site_specific_options = get_option($this->option_name);
576
+ if ( !is_array($site_specific_options) ) {
577
+ $site_specific_options = array();
578
  }
579
+ $site_specific_options['custom_menu'] = $custom_menu;
580
+ update_option($this->option_name, $site_specific_options);
 
 
 
581
  } else {
582
+ $this->options['custom_menu'] = $custom_menu;
583
+ $this->save_options();
584
  }
585
+
586
+ $this->cached_custom_menu = null;
587
+ $this->cached_virtual_caps = null;
588
  }
589
 
590
+ /**
591
+ * Load the current custom menu for this site, if any.
592
+ *
593
+ * @return array|null Either a menu in the internal format, or NULL if there is no custom menu available.
594
+ */
595
+ function load_custom_menu() {
596
+ if ( $this->cached_custom_menu !== null ) {
597
+ return $this->cached_custom_menu;
598
+ }
599
+
600
+ if ( $this->should_use_site_specific_menu() ) {
601
+ $site_specific_options = get_option($this->option_name, null);
602
+ if ( is_array($site_specific_options) && isset($site_specific_options['custom_menu']) ) {
603
+ $this->cached_custom_menu = ameMenu::load_array($site_specific_options['custom_menu']);
 
 
 
 
 
604
  }
605
+ } else {
606
+ if ( empty($this->options['custom_menu']) ) {
607
+ return null;
 
 
 
 
 
 
 
608
  }
609
+ $this->cached_custom_menu = ameMenu::load_array($this->options['custom_menu']);
 
610
  }
611
 
612
+ return $this->cached_custom_menu;
613
+ }
614
 
615
+ /**
616
+ * Determine if we should use a site-specific admin menu configuration
617
+ * for the current site, or fall back to the global config.
618
+ *
619
+ * @return bool True = use the site-specific config (if any), false = use the global config.
620
+ */
621
+ protected function should_use_site_specific_menu() {
622
+ if ( !is_multisite() ) {
623
+ //If this is a single-site WP installation then there's really
624
+ //no difference between "site-specific" and "global".
625
+ return false;
626
+ }
627
+ return ($this->options['menu_config_scope'] === 'site');
628
  }
629
 
630
+ /**
631
+ * Determine if the current user may use the menu editor.
632
+ *
633
+ * @return bool
634
+ */
635
+ public function current_user_can_edit_menu(){
636
+ $access = $this->options['plugin_access'];
637
+
638
+ if ( $access === 'super_admin' ) {
639
+ return is_super_admin();
640
+ } else if ( $access === 'specific_user' ) {
641
+ return get_current_user_id() == $this->options['allowed_user_id'];
642
+ } else {
643
+ $capability = apply_filters('admin_menu_editor-capability', $access);
644
+ return current_user_can($capability);
645
  }
 
646
  }
647
 
648
+ /**
649
+ * Apply the custom page title, if any.
650
+ *
651
+ * This is a callback for the "admin_title" filter. It will change the browser window/tab
652
+ * title (i.e. <title>), but not the title displayed on the admin page itself.
653
+ *
654
+ * @param string $admin_title The current admin title (full).
655
+ * @param string $title The current page title.
656
+ * @return string New admin title.
657
+ */
658
+ function hook_admin_title($admin_title, $title){
659
+ $item = $this->get_current_menu_item();
660
+ if ( $item === null ) {
661
+ return $admin_title;
 
 
 
 
 
662
  }
663
+
664
+ //Check if the we have a custom title for this page.
665
+ $default_title = isset($item['defaults']['page_title']) ? $item['defaults']['page_title'] : '';
666
+ if ( !empty($item['page_title']) && $item['page_title'] != $default_title ) {
667
+ if ( empty($title) ) {
668
+ $admin_title = $item['page_title'] . $admin_title;
669
+ } else {
670
+ //Replace the first occurrence of the default title with the custom one.
671
+ $title_pos = strpos($admin_title, $title);
672
+ $admin_title = substr_replace($admin_title, $item['page_title'], $title_pos, strlen($title));
673
+ }
674
  }
675
+
676
+ return $admin_title;
677
  }
678
 
679
  /**
680
+ * Populate a lookup array with default values (templates) from $menu and $submenu.
681
+ * Used later to merge a custom menu with the native WordPress menu structure.
 
682
  *
683
+ * @param array $menu
684
+ * @param array $submenu
685
+ * @return array An array of menu templates and their default values.
 
686
  */
687
+ function build_templates($menu, $submenu){
688
+ $templates = array();
689
+
690
+ $name_lookup = array();
691
+ foreach($menu as $pos => $item){
692
+ $item = ameMenuItem::fromWpItem($item, $pos);
693
+ if ($item['separator']) {
694
+ continue;
695
+ }
696
+
697
+ $name = $this->sanitize_menu_title($item['menu_title']);
698
+ $name_lookup[$item['file']] = $name;
699
+
700
+ $templates[ameMenuItem::template_id($item)] = array(
701
+ 'name' => $name,
702
+ 'used' => false,
703
+ 'defaults' => $item
704
+ );
705
+ }
706
+
707
+ foreach($submenu as $parent => $items){
708
+ //Skip sub-menus attached to non-existent parents. This should theoretically never happen,
709
+ //but a buggy plugin can cause such a situation.
710
+ if ( !isset($name_lookup[$parent]) ) {
711
+ continue;
712
+ }
713
+
714
+ foreach($items as $pos => $item){
715
+ $item = ameMenuItem::fromWpItem($item, $pos, $parent);
716
+ $templates[ameMenuItem::template_id($item)] = array(
717
+ 'name' => $name_lookup[$parent] . ' -> ' . $this->sanitize_menu_title($item['menu_title']),
718
+ 'used' => false,
719
+ 'defaults' => $item
720
+ );
721
  }
722
  }
723
+
724
+ return $templates;
725
+ }
726
+
727
+ /**
728
+ * Sanitize a menu title for display.
729
+ * Removes HTML tags and update notification bubbles.
730
+ *
731
+ * @param string $title
732
+ * @return string
733
+ */
734
+ private function sanitize_menu_title($title) {
735
+ return strip_tags( preg_replace('@<span[^>]*>.*</span>@i', '', $title) );
736
  }
737
 
738
  /**
739
+ * Merge a custom menu with the current default WordPress menu. Adds/replaces defaults,
740
+ * inserts new items and removes missing items.
741
  *
742
+ * @uses self::$item_templates
743
+ *
744
+ * @param array $tree A menu in plugin's internal form
745
+ * @return array Updated menu tree
746
  */
747
+ function menu_merge($tree){
748
+ //Iterate over all menus and submenus and look up default values
749
+ foreach ($tree as &$topmenu){
750
+
751
+ if ( !ameMenuItem::get($topmenu, 'custom') ) {
752
+ $template_id = ameMenuItem::template_id($topmenu);
753
+ //Is this menu present in the default WP menu?
754
+ if (isset($this->item_templates[$template_id])){
755
+ //Yes, load defaults from that item
756
+ $topmenu['defaults'] = $this->item_templates[$template_id]['defaults'];
757
+ //Note that the original item was used
758
+ $this->item_templates[$template_id]['used'] = true;
759
+ } else {
760
+ //Record the menu as missing, unless it's a menu separator
761
+ if ( empty($topmenu['separator']) ){
762
+ $topmenu['missing'] = true;
763
+
764
+ $temp = ameMenuItem::apply_defaults($topmenu);
765
+ $temp = $this->set_final_menu_capability($temp);
766
+ $this->add_access_lookup($temp, 'menu', true);
767
+ }
768
+ }
769
+ }
770
+
771
+ if (is_array($topmenu['items'])) {
772
+ //Iterate over submenu items
773
+ foreach ($topmenu['items'] as &$item){
774
+ if ( !ameMenuItem::get($item, 'custom') ) {
775
+ $template_id = ameMenuItem::template_id($item);
776
+
777
+ //Is this item present in the default WP menu?
778
+ if (isset($this->item_templates[$template_id])){
779
+ //Yes, load defaults from that item
780
+ $item['defaults'] = $this->item_templates[$template_id]['defaults'];
781
+ $this->item_templates[$template_id]['used'] = true;
782
+ } else if ( empty($item['separator']) ) {
783
+ //Record as missing, unless it's a menu separator
784
+ $item['missing'] = true;
785
+
786
+ $temp = ameMenuItem::apply_defaults($item);
787
+ $temp = $this->set_final_menu_capability($temp);
788
+ $this->add_access_lookup($temp, 'submenu', true);
789
+ }
790
+ }
791
+ }
792
  }
793
  }
794
 
795
+ //If we don't unset these they will fuck up the next two loops where the same names are used.
796
+ unset($topmenu);
797
+ unset($item);
798
+
799
+ //Now we have some items marked as missing, and some items in lookup arrays
800
+ //that are not marked as used. Lets remove the missing items from the tree.
801
+ $filteredTree = array();
802
+ foreach($tree as $file => $topmenu) {
803
+ if ( $topmenu['missing'] ) {
804
+ continue;
805
+ }
806
+ $filteredSubmenu = array();
807
+ if (is_array($topmenu['items'])) {
808
+ foreach($topmenu['items'] as $index => $item) {
809
+ if ( !$item['missing'] ) {
810
+ $filteredSubmenu[$index] = $item;
811
+ }
812
+ }
813
+
814
+ }
815
+ $topmenu['items'] = $filteredSubmenu;
816
+ $filteredTree[$file] = $topmenu;
817
+ }
818
+
819
+ $tree = $filteredTree;
820
+
821
+ //Lets merge in the unused items.
822
+ foreach ($this->item_templates as $template_id => $template){
823
+ //Skip used menus and separators
824
+ if ( !empty($template['used']) || !empty($template['defaults']['separator'])) {
825
+ continue;
826
+ }
827
+
828
+ //Found an unused item. Build the tree entry.
829
+ $entry = ameMenuItem::blank_menu();
830
+ $entry['template_id'] = $template_id;
831
+ $entry['defaults'] = $template['defaults'];
832
+ $entry['unused'] = true; //Note that this item is unused
833
+
834
+ //Add the new entry to the menu tree
835
+ if ( !empty($template['defaults']['parent']) ) {
836
+ if (isset($tree[$template['defaults']['parent']])) {
837
+ //Okay, insert the item.
838
+ $tree[$template['defaults']['parent']]['items'][] = $entry;
839
+ } else {
840
+ //This can happen if the original parent menu has been moved to a submenu.
841
+ //Todo: Handle this unusual situation.
842
+ }
843
  } else {
844
+ $tree[$template['defaults']['file']] = $entry;
845
  }
846
  }
847
 
 
 
 
 
 
 
 
 
 
 
848
  //Resort the tree to ensure the found items are in the right spots
849
+ $tree = ameMenu::sort_menu_tree($tree);
850
+
851
+ return $tree;
852
+ }
853
+
854
+ /**
855
+ * Add a page and its required capability to the page access lookup.
856
+ *
857
+ * The lookup array is indexed by priority. Priorities (highest to lowest):
858
+ * - Has custom permissions and a known template.
859
+ * - Has custom permissions, template missing or can't be determined correctly.
860
+ * - Default permissions.
861
+ * - Everything else.
862
+ * Additionally, submenu items have slightly higher priority that top level menus.
863
+ * The desired end result is for menu items with custom permissions to override
864
+ * default menus.
865
+ *
866
+ * Note to self: If we were to keep items with an unknown template instead of throwing
867
+ * them away during the merge phase, we could simplify this considerably.
868
+ *
869
+ * @param array $item Menu item (with defaults already applied).
870
+ * @param string $item_type 'menu' or 'submenu'.
871
+ * @param bool $missing Whether the item template is missing or unknown.
872
+ */
873
+ private function add_access_lookup($item, $item_type = 'menu', $missing = false) {
874
+ if ( empty($item['url']) ) {
875
+ return;
876
+ }
877
+
878
+ $has_custom_settings = !empty($item['grant_access']) || !empty($item['extra_capability']);
879
+ $priority = 6;
880
+ if ( $missing ) {
881
+ if ( $has_custom_settings ) {
882
+ $priority = 4;
883
+ } else {
884
+ return; //Don't even consider missing menus without custom access settings.
885
  }
886
+ } else if ( $has_custom_settings ) {
887
+ $priority = 2;
888
  }
889
+
890
+ if ( $item_type == 'submenu' ) {
891
+ $priority--;
892
+ }
893
+
894
+ $this->page_access_lookup[$item['url']][$priority] = $item['access_level'];
895
  }
896
 
897
  /**
898
+ * Generate WP-compatible $menu and $submenu arrays from a custom menu tree.
899
  *
900
+ * Side-effects: This function executes several filters that may modify global state.
901
+ * Specifically, IFrame-handling callbacks in 'extras.php' will add add new hooks
902
+ * and other menu-related structures.
903
+ *
904
+ * @uses WPMenuEditor::$custom_wp_menu Stores the generated top-level menu here.
905
+ * @uses WPMenuEditor::$custom_wp_submenu Stores the generated sub-menu here.
906
  *
907
+ * @uses WPMenuEditor::$title_lookups Generates a lookup list of page titles.
908
+ * @uses WPMenuEditor::$reverse_item_lookup Generates a lookup list of url => menu item relationships.
909
+ *
910
+ * @param array $tree The new menu, in the internal tree format.
911
+ * @return void
912
  */
913
+ function build_custom_wp_menu($tree){
914
+ $new_tree = array();
915
+ $new_menu = array();
916
+ $new_submenu = array();
917
+ $this->title_lookups = array();
918
 
919
  //Sort the menu by position
920
+ uasort($tree, 'ameMenuItem::compare_position');
921
 
922
  //Prepare the top menu
923
  $first_nonseparator_found = false;
924
  foreach ($tree as $topmenu){
925
+
926
+ //Skip missing and hidden menus.
927
+ if ( !empty($topmenu['missing']) || !empty($topmenu['hidden']) ) {
 
928
  continue;
929
+ }
930
 
931
  //Skip leading menu separators. Fixes a superfluous separator showing up
932
  //in WP 3.0 (multisite mode) when there's a custom menu and the current user
933
  //can't access its first item ("Super Admin").
934
+ if ( !empty($topmenu['separator']) && !$first_nonseparator_found ) {
935
+ continue;
936
+ }
937
  $first_nonseparator_found = true;
938
 
939
+ $topmenu = $this->prepare_for_output($topmenu, 'menu');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
940
 
941
+ if ( empty($topmenu['separator']) ) {
942
+ $this->title_lookups[$topmenu['file']] = !empty($topmenu['page_title']) ? $topmenu['page_title'] : $topmenu['menu_title'];
943
+ }
 
 
 
 
 
 
 
 
 
 
944
 
945
  //Prepare the submenu of this menu
946
+ $new_items = array();
947
  if( !empty($topmenu['items']) ){
948
  $items = $topmenu['items'];
949
  //Sort by position
950
+ uasort($items, 'ameMenuItem::compare_position');
951
 
952
  foreach ($items as $item) {
953
+ //Skip missing and hidden items
954
+ if ( !empty($item['missing']) || !empty($item['hidden']) ) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
955
  continue;
956
  }
957
+
958
+ $item = $this->prepare_for_output($item, 'submenu', $topmenu['file']);
959
+ $new_items[] = $item;
960
+
961
+ //Make a note of the page's correct title so we can fix it later if necessary.
962
+ $this->title_lookups[$item['file']] = !empty($item['page_title']) ? $item['page_title'] : $item['menu_title'];
 
 
 
 
 
963
  }
964
  }
965
+
966
+ $topmenu['items'] = $new_items;
967
+ $new_tree[] = $topmenu;
968
+ }
969
+
970
+ //Use only the highest-priority capability for each URL.
971
+ foreach($this->page_access_lookup as $url => $capabilities) {
972
+ ksort($capabilities);
973
+ $this->page_access_lookup[$url] = reset($capabilities);
974
  }
975
+
976
+ //Convert the prepared tree to the internal WordPress format.
977
+ foreach($new_tree as $topmenu) {
978
+ $trueAccess = isset($this->page_access_lookup[$topmenu['url']]) ? $this->page_access_lookup[$topmenu['url']] : null;
979
+ if ( $trueAccess === 'do_not_allow' ) {
980
+ $topmenu['access_level'] = $trueAccess;
981
+ }
982
+ if ( !isset($this->reverse_item_lookup[$topmenu['url']]) ) { //Prefer sub-menus.
983
+ $this->reverse_item_lookup[$topmenu['url']] = $topmenu;
984
+ }
985
+
986
+ $new_menu[] = $this->convert_to_wp_format($topmenu);
987
+
988
+ foreach($topmenu['items'] as $item) {
989
+ $trueAccess = isset($this->page_access_lookup[$item['url']]) ? $this->page_access_lookup[$item['url']] : null;
990
+ if ( $trueAccess === 'do_not_allow' ) {
991
+ $item['access_level'] = $trueAccess;
992
+ }
993
+ $this->reverse_item_lookup[$item['url']] = $item;
994
+ $new_submenu[$topmenu['file']][] = $this->convert_to_wp_format($item);
995
+ }
996
+ }
997
+
998
+ $this->custom_wp_menu = $new_menu;
999
+ $this->custom_wp_submenu = $new_submenu;
1000
  }
1001
+
1002
+ /**
1003
+ * Convert a menu item from the internal format used by this plugin to the format
1004
+ * used by WP. The menu should be prepared using the prepare... function beforehand.
1005
+ *
1006
+ * @see self::prepare_for_output()
1007
+ *
1008
+ * @param array $item
1009
+ * @return array
1010
+ */
1011
+ private function convert_to_wp_format($item) {
1012
+ //Build the menu structure that WP expects
1013
+ $wp_item = array(
1014
+ $item['menu_title'],
1015
+ $item['access_level'],
1016
+ $item['file'],
1017
+ $item['page_title'],
1018
+ $item['css_class'],
1019
+ $item['hookname'], //ID
1020
+ $item['icon_url']
1021
+ );
1022
+
1023
+ return $wp_item;
1024
+ }
1025
+
1026
+ /**
1027
+ * Prepare a menu item to be converted to the WordPress format and added to the current
1028
+ * WordPress admin menu. This function applies menu defaults and templates, calls filters
1029
+ * that allow other components to tweak the menu, decides on what capability/-ies to use,
1030
+ * and so on.
1031
+ *
1032
+ * Caution: The filters called by this function may cause side-effects. Specifically, the Pro-only feature
1033
+ * for displaying menu pages in a frame does this. See wsMenuEditorExtras::create_framed_menu().
1034
+ * Therefore, it is not safe to call this function more than once for the same item.
1035
+ *
1036
+ * @param array $item Menu item in the internal format.
1037
+ * @param string $item_type Either 'menu' or 'submenu'.
1038
+ * @param string $parent Optional. The parent of this sub-menu item. An empty string for top-level menus.
1039
+ * @return array Menu item in the internal format.
1040
+ */
1041
+ private function prepare_for_output($item, $item_type = 'menu', $parent = '') {
1042
+ // Special case : plugin pages that have been moved from a sub-menu to a different
1043
+ // menu or the top level. We'll need to adjust the file field to point to the correct URL.
1044
+ // This is required because WP identifies plugin pages using *both* the plugin file
1045
+ // and the parent file.
1046
+ if ( $item['template_id'] !== '' && !$item['separator'] ) {
1047
+ $template = $this->item_templates[$item['template_id']];
1048
+ if ( $template['defaults']['is_plugin_page'] ) {
1049
+ $default_parent = $template['defaults']['parent'];
1050
+ if ( $parent != $default_parent ){
1051
+ $item['file'] = $template['defaults']['url'];
1052
+ }
1053
  }
1054
  }
1055
+
1056
+ //Menus that have both a custom icon URL and a "menu-icon-*" class will get two overlapping icons.
1057
+ //Fix this by automatically removing the class. The user can set a custom class attr. to override.
1058
+ if (
1059
+ ameMenuItem::is_default($item, 'css_class')
1060
+ && !ameMenuItem::is_default($item, 'icon_url')
1061
+ && !in_array($item['icon_url'], array('', 'none', 'div')) //Skip "no custom icon" icons.
1062
+ ) {
1063
+ $new_classes = preg_replace('@\bmenu-icon-[^\s]+\b@', '', $item['defaults']['css_class']);
1064
+ if ( $new_classes !== $item['defaults']['css_class'] ) {
1065
+ $item['css_class'] = $new_classes;
1066
+ }
1067
+ }
1068
+
1069
+ //Apply defaults & filters
1070
+ $item = ameMenuItem::apply_defaults($item);
1071
+ $item = ameMenuItem::apply_filters($item, $item_type, $parent); //may cause side-effects
1072
+
1073
+ $item = $this->set_final_menu_capability($item);
1074
+ if ( !$this->options['security_logging_enabled'] ) {
1075
+ unset($item['access_check_log']); //Throw away the log to conserve memory.
1076
+ }
1077
+ $this->add_access_lookup($item, $item_type);
1078
+
1079
+ //Menus without a custom icon image should have it set to "none" (or "div" in older WP versions).
1080
+ //See /wp-admin/menu-header.php for details on how this works.
1081
+ if ( $item['icon_url'] === '' ) {
1082
+ $item['icon_url'] = 'none';
1083
+ }
1084
+
1085
+ //Used later to determine the current page based on URL.
1086
+ $item['url'] = ameMenuItem::generate_url($item['file'], $parent);
1087
+
1088
+ //Convert relative URls to fully qualified ones. This prevents problems with WordPress
1089
+ //incorrectly converting "index.php?page=xyz" to, say, "tools.php?page=index.php?page=xyz"
1090
+ //if the menu item was moved from "Dashboard" to "Tools".
1091
+ $itemFile = ameMenuItem::remove_query_from($item['file']);
1092
+ $shouldMakeAbsolute =
1093
+ (strpos($item['file'], '://') === false)
1094
+ && (substr($item['file'], 0, 1) != '/')
1095
+ && ($itemFile == 'index.php')
1096
+ && (strpos($item['file'], '?') !== false);
1097
+
1098
+ if ( $shouldMakeAbsolute ) {
1099
+ $item['file'] = admin_url($item['url']);
1100
+ }
1101
+
1102
+ //WPML support: Use translated menu titles where available.
1103
+ if ( !$item['separator'] && function_exists('icl_t') ) {
1104
+ $item['menu_title'] = icl_t(
1105
+ self::WPML_CONTEXT,
1106
+ $this->get_wpml_name_for($item, 'menu_title'),
1107
+ $item['menu_title']
1108
+ );
1109
+ }
1110
+
1111
+ return $item;
1112
+ }
1113
+
1114
+ /**
1115
+ * Figure out if the current user can access a menu item and what capability they would need.
1116
+ *
1117
+ * This method takes into account the default capability set by WordPress as well as any
1118
+ * custom role and capability settings specified by the user. It will set "access_level"
1119
+ * to the required capability, or set it to 'do_not_allow' if the current user can't access
1120
+ * this menu.
1121
+ *
1122
+ * @param array $item Menu item (with defaults applied).
1123
+ * @return array
1124
+ */
1125
+ private function set_final_menu_capability($item) {
1126
+ $item['access_check_log'] = array(
1127
+ str_repeat('=', 79),
1128
+ 'Figuring out what capability the user will need to access this item...'
1129
+ );
1130
+
1131
+ $item = apply_filters('custom_admin_menu_capability', $item);
1132
+
1133
+ $item['access_check_log'][] = '-----';
1134
+
1135
+ //Check if the current user can access this menu.
1136
+ $user_has_access = true;
1137
+ $cap_to_use = '';
1138
+ if ( !empty($item['access_level']) ) {
1139
+ $user_has_cap = $this->current_user_can($item['access_level']);
1140
+ $user_has_access = $user_has_access && $user_has_cap;
1141
+ $cap_to_use = $item['access_level'];
1142
+
1143
+ $item['access_check_log'][] = sprintf(
1144
+ 'Required capability: %1$s. User %2$s this capability.',
1145
+ htmlentities($cap_to_use),
1146
+ $user_has_cap ? 'HAS' : 'DOES NOT have'
1147
+ );
1148
+ } else {
1149
+ $item['access_check_log'][] = '- No required capability set.';
1150
+ }
1151
+
1152
+ if ( !empty($item['extra_capability']) ) {
1153
+ $user_has_cap = $this->current_user_can($item['extra_capability']);
1154
+ $user_has_access = $user_has_access && $user_has_cap;
1155
+ $cap_to_use = $item['extra_capability'];
1156
+
1157
+ $item['access_check_log'][] = sprintf(
1158
+ 'Extra capability: %1$s. User %2$s this capability.',
1159
+ htmlentities($cap_to_use),
1160
+ $user_has_cap ? 'HAS' : 'DOES NOT have'
1161
+ );
1162
+ } else {
1163
+ $item['access_check_log'][] = 'No "extra capability" set.';
1164
+ }
1165
+
1166
+ $capability = $user_has_access ? $cap_to_use : 'do_not_allow';
1167
+ $item['access_check_log'][] = 'Final capability setting: ' . $capability;
1168
+ $item['access_check_log'][] = str_repeat('=', 79);
1169
+
1170
+ $item['access_level'] = $capability;
1171
+ return $item;
1172
  }
1173
 
1174
  /**
1177
  * @return void
1178
  */
1179
  function page_menu_editor(){
 
 
 
1180
  if ( !$this->current_user_can_edit_menu() ){
1181
+ wp_die(sprintf(
1182
+ 'You do not have sufficient permissions to use Admin Menu Editor. Required: <code>%s</code>.',
1183
+ htmlentities($this->options['plugin_access'])
1184
+ ));
1185
  }
1186
 
1187
  $action = isset($this->post['action']) ? $this->post['action'] : (isset($this->get['action']) ? $this->get['action'] : '');
1188
  do_action('admin_menu_editor_header', $action);
 
 
 
 
 
 
 
 
 
 
 
1189
 
1190
+ if ( !empty($action) ) {
1191
+ $this->handle_form_submission($this->post, $action);
1192
+ }
1193
+
1194
+ $sub_section = isset($this->get['sub_section']) ? $this->get['sub_section'] : null;
1195
+ if ( $sub_section === 'settings' ) {
1196
+ $this->display_plugin_settings_ui();
1197
+ } else {
1198
+ $this->display_editor_ui();
1199
+ }
1200
+ }
1201
+
1202
+ private function handle_form_submission($post, $action = '') {
1203
+ if ( $action == 'save_menu' ) {
1204
+ //Save the admin menu configuration.
1205
+ if ( isset($post['data']) ){
1206
+ check_admin_referer('menu-editor-form');
1207
+
1208
+ //Try to decode a menu tree encoded as JSON
1209
+ $url = remove_query_arg(array('noheader'));
1210
+ try {
1211
+ $menu = ameMenu::load_json($post['data'], true);
1212
+ } catch (InvalidMenuException $ex) {
1213
+ //Or redirect & display the error message
1214
+ wp_redirect( add_query_arg('message', 2, $url) );
1215
+ die();
1216
  }
1217
 
1218
  //Save the custom menu
1219
+ $this->set_custom_menu($menu);
1220
+
1221
+ //Redirect back to the editor and display the success message.
1222
+ //Also, automatically select the last selected actor (convenience feature).
1223
+ $query = array('message' => 1);
1224
+ if ( isset($post['selected_actor']) && !empty($post['selected_actor']) ) {
1225
+ $query['selected_actor'] = strval($post['selected_actor']);
1226
+ }
1227
+ wp_redirect( add_query_arg($query, $url) );
1228
+ die();
1229
  } else {
1230
+ $message = "Failed to save the menu. ";
1231
+ if ( isset($this->post['data_length']) && is_numeric($this->post['data_length']) ) {
1232
+ $message .= sprintf(
1233
+ 'Expected to receive %d bytes of menu data in $_POST[\'data\'], but got nothing.',
1234
+ intval($this->post['data_length'])
1235
+ );
1236
+ }
1237
+ wp_die($message);
1238
  }
 
 
1239
 
1240
+ } else if ( $action == 'save_settings' ) {
 
 
 
 
 
 
 
 
1241
 
1242
+ //Save overall plugin configuration (permissions, etc).
1243
+ check_admin_referer('save_settings');
1244
+
1245
+ //Plugin access setting.
1246
+ $valid_access_settings = array('super_admin', 'manage_options');
1247
+ //On Multisite only Super Admins can choose the "Only the current user" option.
1248
+ if ( !is_multisite() || is_super_admin() ) {
1249
+ $valid_access_settings[] = 'specific_user';
1250
+ }
1251
+ if ( isset($this->post['plugin_access']) && in_array($this->post['plugin_access'], $valid_access_settings) ) {
1252
+ $this->options['plugin_access'] = $this->post['plugin_access'];
1253
+
1254
+ if ( $this->options['plugin_access'] === 'specific_user' ) {
1255
+ $this->options['allowed_user_id'] = get_current_user_id();
1256
+ } else {
1257
+ $this->options['allowed_user_id'] = null;
1258
+ }
1259
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1260
 
1261
+ //Whether to hide the plugin on the "Plugins" admin page.
1262
+ if ( !is_multisite() || is_super_admin() ) {
1263
+ if ( !empty($this->post['hide_plugin_from_others']) ) {
1264
+ $this->options['plugins_page_allowed_user_id'] = get_current_user_id();
1265
+ } else {
1266
+ $this->options['plugins_page_allowed_user_id'] = null;
1267
+ }
1268
+ }
1269
 
1270
+ //Configuration scope. The Super Admin is the only one who can change it since it affects all sites.
1271
+ if ( is_multisite() && is_super_admin() ) {
1272
+ $valid_scopes = array('global', 'site');
1273
+ if ( isset($this->post['menu_config_scope']) && in_array($this->post['menu_config_scope'], $valid_scopes) ) {
1274
+ $this->options['menu_config_scope'] = $this->post['menu_config_scope'];
1275
+ }
1276
+ }
 
 
 
 
 
 
 
 
1277
 
1278
+ //Security logging.
1279
+ $this->options['security_logging_enabled'] = !empty($this->post['security_logging_enabled']);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1280
 
1281
+ //Hide some menu options by default.
1282
+ $this->options['hide_advanced_settings'] = !empty($this->post['hide_advanced_settings']);
1283
 
1284
+ $this->save_options();
1285
+ wp_redirect(add_query_arg('updated', 1, $this->get_settings_page_url()));
1286
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1287
  }
1288
 
1289
+ private function display_editor_ui() {
1290
+ //Prepare a bunch of parameters for the editor.
1291
+ $editor_data = array(
1292
+ 'message' => isset($this->get['message']) ? intval($this->get['message']) : null,
1293
+ 'images_url' => plugins_url('images', $this->plugin_file),
1294
+ 'hide_advanced_settings' => $this->options['hide_advanced_settings'],
1295
+ 'settings_page_url' => $this->get_settings_page_url(),
 
 
1296
  );
 
 
 
 
 
 
 
1297
 
1298
+ //Build a tree struct. for the default menu
1299
+ $default_tree = ameMenu::wp2tree($this->default_wp_menu, $this->default_wp_submenu);
1300
+ $default_menu = ameMenu::load_array($default_tree);
1301
+
1302
+ //Is there a custom menu?
1303
+ if (!empty($this->merged_custom_menu)){
1304
+ $custom_menu = $this->merged_custom_menu;
1305
+ } else {
1306
+ //Start out with the default menu if there is no user-created one
1307
+ $custom_menu = $default_menu;
1308
+ }
1309
+
1310
+ //Encode both menus as JSON
1311
+ $editor_data['default_menu_js'] = ameMenu::to_json($default_menu);
1312
+ $editor_data['custom_menu_js'] = ameMenu::to_json($custom_menu);
1313
+
1314
+ //Create a list of all known capabilities and roles. Used for the drop-down list on the access field.
1315
+ $all_capabilities = ameRoleUtils::get_all_capabilities();
1316
+ //"level_X" capabilities are deprecated so we don't want people using them.
1317
+ //This would look better with array_filter() and an anonymous function as a callback.
1318
+ for($level = 0; $level <= 10; $level++){
1319
+ $cap = 'level_' . $level;
1320
+ if ( isset($all_capabilities[$cap]) ){
1321
+ unset($all_capabilities[$cap]);
1322
+ }
1323
  }
1324
+ $all_capabilities = array_keys($all_capabilities);
1325
+ natcasesort($all_capabilities);
 
1326
 
1327
+ //Multi-site installs also get the virtual "Super Admin" cap, but only the Super Admin sees it.
1328
+ if ( is_multisite() && !isset($all_capabilities['super_admin']) && is_super_admin() ){
1329
+ array_unshift($all_capabilities, 'super_admin');
1330
+ }
1331
+ $editor_data['all_capabilities'] = $all_capabilities;
1332
 
1333
+ //Create a list of all roles, too.
1334
+ $all_roles = ameRoleUtils::get_role_names();
1335
+ asort($all_roles);
1336
+ $editor_data['all_roles'] = $all_roles;
1337
 
1338
+ //Include hint visibility settings
1339
+ $editor_data['show_hints'] = $this->get_hint_visibility();
1340
 
1341
+ require dirname(__FILE__) . '/editor-page.php';
1342
+ }
1343
 
1344
+ /**
1345
+ * Display the plugin settings page.
1346
+ */
1347
+ private function display_plugin_settings_ui() {
1348
+ //These variables are used by settings-page.php.
1349
+ $settings = $this->options;
1350
+ $settings_page_url = $this->get_settings_page_url();
1351
+ $editor_page_url = admin_url($this->settings_link);
1352
 
1353
+ require dirname(__FILE__) . '/settings-page.php';
1354
+ }
1355
 
1356
+ /**
1357
+ * Get the fully qualified URL of the "Settings" sub-section of our plugin page.
1358
+ *
1359
+ * @return string
1360
+ */
1361
+ private function get_settings_page_url() {
1362
+ return add_query_arg('sub_section', 'settings', admin_url($this->settings_link));
1363
+ }
1364
 
1365
+ /**
1366
+ * Check if the current page is the "Menu Editor" admin page.
1367
+ *
1368
+ * @return bool
1369
+ */
1370
+ protected function is_editor_page() {
1371
+ return is_admin()
1372
+ && isset($this->get['page']) && ($this->get['page'] == 'menu_editor')
1373
+ && ( !isset($this->get['sub_section']) || empty($this->get['sub_section']) );
1374
+ }
1375
 
1376
+ /**
1377
+ * Check if the current page is the "Settings" sub-section of our admin page.
1378
+ *
1379
+ * @return bool
1380
+ */
1381
+ protected function is_settings_page() {
1382
+ return is_admin()
1383
+ && isset($this->get['sub_section']) && ($this->get['sub_section'] == 'settings')
1384
+ && isset($this->get['page']) && ($this->get['page'] == 'menu_editor');
1385
  }
1386
 
1387
+ /**
1388
+ * Generate a list of "virtual" capabilities that should be granted to certain roles.
1389
+ *
1390
+ * This is based on grant_access settings for the current custom menu and enables
1391
+ * selected roles and users to access menu items that they ordinarily would not
1392
+ * be able to.
1393
+ *
1394
+ * @uses self::get_virtual_caps_for() to actually generate the caps.
1395
+ * @uses self::$cached_virtual_caps to cache the generated list of caps.
1396
+ *
1397
+ * @return array A list of capability => [role1 => true, ... roleN => true] assignments.
1398
+ */
1399
+ function get_virtual_caps() {
1400
+ if ( $this->cached_virtual_caps !== null ) {
1401
+ return $this->cached_virtual_caps;
1402
  }
1403
+
1404
+ $caps = array();
1405
+ $custom_menu = $this->load_custom_menu();
1406
+ if ( $custom_menu === null ){
1407
+ return $caps;
 
1408
  }
1409
+
1410
+ foreach($custom_menu['tree'] as $item) {
1411
+ $caps = array_merge_recursive($caps, $this->get_virtual_caps_for($item));
1412
+ }
1413
+
1414
+ $this->cached_virtual_caps = $caps;
1415
+ return $caps;
 
 
 
 
 
 
1416
  }
1417
+
1418
+ private function get_virtual_caps_for($item) {
1419
+ $caps = array();
1420
+
1421
+ if ( $item['template_id'] !== '' ) {
1422
+ $required_cap = ameMenuItem::get($item, 'access_level');
1423
+ foreach ($item['grant_access'] as $grant => $has_access) {
1424
+ if ( $has_access ) {
1425
+ if ( !isset($caps[$grant]) ) {
1426
+ $caps[$grant] = array();
1427
+ }
1428
+ $caps[$grant][$required_cap] = true;
1429
+ }
1430
+ }
1431
  }
1432
+
1433
+ foreach($item['items'] as $sub_item) {
1434
+ $caps = array_merge_recursive($caps, $this->get_virtual_caps_for($sub_item));
1435
  }
1436
+
1437
+ return $caps;
1438
  }
1439
 
1440
  /**
1455
  return $allcaps;
1456
  }
1457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1458
  /**
1459
  * AJAX callback for saving screen options (whether to show or to hide advanced menu options).
1460
  *
1464
  * @return void
1465
  */
1466
  function ajax_save_screen_options(){
1467
+ if (!$this->current_user_can_edit_menu() || !check_ajax_referer('ws_ame_save_screen_options', false, false)){
1468
  die( $this->json_encode( array(
1469
  'error' => "You're not allowed to do that!"
1470
  )));
1496
 
1497
  $defaults = array(
1498
  'ws_sidebar_pro_ad' => true,
1499
+ 'ws_whats_new_120' => false,
1500
  'ws_hint_menu_permissions' => true,
1501
  );
1502
 
1513
  * would not be highlighted properly when the user visits them.
1514
  */
1515
  public function enqueue_menu_fix_script() {
1516
+ wp_enqueue_auto_versioned_script(
1517
  'ame-menu-fix',
1518
  plugins_url('js/menu-highlight-fix.js', $this->plugin_file),
1519
  array('jquery'),
 
1520
  true
1521
  );
1522
  }
1523
+
1524
  /**
1525
+ * Check if the current user can access the current admin menu page.
1526
+ *
1527
+ * @return bool
1528
+ */
1529
+ private function user_can_access_current_page() {
1530
+ $current_item = $this->get_current_menu_item();
1531
+ if ( $current_item === null ) {
1532
+ $this->log_security_note('Could not determine the current menu item. We won\'t do any custom permission checks.');
1533
+ return true; //Let WordPress handle it.
1534
+ }
1535
+
1536
+ $this->log_security_note(sprintf(
1537
+ 'The current menu item is "%s", menu template ID: "%s"',
1538
+ htmlentities($current_item['menu_title']),
1539
+ htmlentities(ameMenuItem::get($current_item, 'template_id', 'N/A'))
1540
+ ));
1541
+ if ( isset($current_item['access_check_log']) ) {
1542
+ $this->log_security_note($current_item['access_check_log']);
1543
+ }
1544
+
1545
+ //Note: Per-role and per-user virtual caps will be applied by has_cap filters.
1546
+ $allow = $this->current_user_can($current_item['access_level']);
1547
+ $this->log_security_note(sprintf(
1548
+ 'The current user %1$s the "%2$s" capability.',
1549
+ $allow ? 'has' : 'does not have',
1550
+ htmlentities($current_item['access_level'])
1551
+ ));
1552
+
1553
+ return $allow;
1554
+ }
1555
+
1556
+ /**
1557
+ * Check if the current user has the specified capability.
1558
+ * If the Pro version installed, you can use special syntax to perform complex capability checks.
1559
+ *
1560
+ * @param string $capability
1561
+ * @return bool
1562
+ */
1563
+ private function current_user_can($capability) {
1564
+ return apply_filters('admin_menu_editor-current_user_can', current_user_can($capability), $capability);
1565
+ }
1566
+
1567
+ /**
1568
+ * Determine which menu item matches the currently open admin page.
1569
+ *
1570
+ * @uses self::$reverse_item_lookup
1571
+ * @return array|null Menu item in the internal format, or NULL if no matching item can be found.
1572
+ */
1573
+ private function get_current_menu_item() {
1574
+ if ( !is_admin() || empty($this->reverse_item_lookup)) {
1575
+ if ( !is_admin() ) {
1576
+ $this->log_security_note('This is not an admin page. is_admin() returns false.');
1577
+ } else if ( empty($this->reverse_item_lookup) ) {
1578
+ $this->log_security_note('Warning: reverse_item_lookup is empty!');
1579
+ }
1580
+ return null;
1581
+ }
1582
+
1583
+ //The current menu item doesn't change during a request, so we can cache it
1584
+ //and avoid searching the entire menu every time.
1585
+ static $cached_item = null;
1586
+ if ( $cached_item !== null ) {
1587
+ return $cached_item;
1588
+ }
1589
+
1590
+ //Find an item where *all* query params match the current ones, with as few extraneous params as possible,
1591
+ //preferring sub-menu items. This is intentionally more strict than what we do in menu-highlight-fix.js,
1592
+ //since this function is used to check menu access.
1593
+ //TODO: Use get_current_screen() to determine the current post type and taxonomy.
1594
+
1595
+ $best_item = null;
1596
+ $best_extra_params = PHP_INT_MAX;
1597
+
1598
+ $base_site_url = get_site_url();
1599
+ if ( preg_match('@(^\w+://[^/]+)@', $base_site_url, $matches) ) { //Extract scheme + hostname.
1600
+ $base_site_url = $matches[1];
1601
+ }
1602
+
1603
+ $current_url = $base_site_url . remove_query_arg('___ame_dummy_param___');
1604
+ $this->log_security_note(sprintf('Current URL: "%s"', htmlentities($current_url)));
1605
+
1606
+ $current_url = $this->parse_url($current_url);
1607
+
1608
+ foreach($this->reverse_item_lookup as $url => $item) {
1609
+ $item_url = $url;
1610
+ //Convert to absolute URL. Caution: directory traversal (../, etc) is not handled.
1611
+ if (strpos($item_url, '://') === false) {
1612
+ if ( substr($item_url, 0, 1) == '/' ) {
1613
+ $item_url = $base_site_url . $item_url;
1614
+ } else {
1615
+ $item_url = admin_url($item_url);
1616
+ }
1617
+ }
1618
+ $item_url = $this->parse_url($item_url);
1619
+
1620
+ //Must match scheme, host, port, user, pass and path.
1621
+ $components = array('scheme', 'host', 'port', 'user', 'pass');
1622
+ $is_close_match = $this->urlPathsMatch($current_url['path'], $item_url['path']);
1623
+ foreach($components as $component) {
1624
+ $is_close_match = $is_close_match && ($current_url[$component] == $item_url[$component]);
1625
+ if ( !$is_close_match ) {
1626
+ break;
1627
+ }
1628
+ }
1629
+
1630
+ //The current URL must match all query parameters of the item URL.
1631
+ $different_params = array_diff_assoc($item_url['params'], $current_url['params']);
1632
+
1633
+ //The current URL must have as few extra parameters as possible.
1634
+ $extra_params = array_diff_assoc($current_url['params'], $item_url['params']);
1635
+
1636
+ if ( $is_close_match && (count($different_params) == 0) && (count($extra_params) < $best_extra_params) ) {
1637
+ $best_item = $item;
1638
+ $best_extra_params = count($extra_params);
1639
+ }
1640
+ }
1641
+
1642
+ $cached_item = $best_item;
1643
+ return $best_item;
1644
+ }
1645
+
1646
+ /**
1647
+ * Parse a URL and return its components.
1648
+ *
1649
+ * Returns an array that contains all of these components: 'scheme', 'host', 'port', 'user', 'pass',
1650
+ * 'path', 'query', 'fragment' and 'params'. All entries are strings, except 'params' which is
1651
+ * an associative array of query parameters and their values.
1652
+ *
1653
+ * @param string $url
1654
+ * @return array
1655
+ */
1656
+ private function parse_url($url) {
1657
+ $url_defaults = array_fill_keys(array('scheme', 'host', 'port', 'user', 'pass', 'path', 'query', 'fragment'), '');
1658
+ $url_defaults['port'] = '80';
1659
+
1660
+ $parsed = @parse_url($url);
1661
+ if ( !is_array($parsed) ) {
1662
+ $parsed = array();
1663
+ }
1664
+ $parsed = array_merge($url_defaults, $parsed);
1665
+
1666
+ $params = array();
1667
+ if (!empty($parsed['query'])) {
1668
+ wp_parse_str($parsed['query'], $params);
1669
+ };
1670
+ $parsed['params'] = $params;
1671
+
1672
+ return $parsed;
1673
+ }
1674
+
1675
+ /**
1676
+ * Check if two paths match. Intended for comparing WP admin URLs.
1677
+ *
1678
+ * @param string $path1
1679
+ * @param string $path2
1680
+ * @return bool
1681
+ */
1682
+ private function urlPathsMatch($path1, $path2) {
1683
+ if ( $path1 == $path2 ) {
1684
+ return true;
1685
+ }
1686
+
1687
+ // "/wp-admin/index.php" should match "/wp-admin/".
1688
+ if (
1689
+ ($this->endsWith($path1, '/wp-admin/index.php') && $this->endsWith($path2, '/wp-admin/'))
1690
+ || ($this->endsWith($path2, '/wp-admin/index.php') && $this->endsWith($path1, '/wp-admin/'))
1691
+ ) {
1692
+ return true;
1693
+ }
1694
+
1695
+ return false;
1696
+ }
1697
+
1698
+ /**
1699
+ * Determine if the input $string ends with the specified $suffix.
1700
+ *
1701
+ * @param string $string
1702
+ * @param string $suffix
1703
+ * @return bool
1704
  */
1705
+ private function endsWith($string, $suffix) {
1706
+ $len = strlen($suffix);
1707
+ if ( $len == 0 ) {
1708
+ return true;
1709
+ }
1710
+ return substr($string, -$len) === $suffix;
1711
+ }
1712
+
1713
+ private function castValuesToBool($capabilities) {
1714
+ if ( !is_array($capabilities) ) {
1715
+ if ( empty($capabilities) ) {
1716
+ $capabilities = array();
1717
+ } else {
1718
+ trigger_error("Unexpected capability array: " . print_r($capabilities, true), E_USER_WARNING);
1719
+ return array();
1720
+ }
1721
+ }
1722
+ foreach($capabilities as $capability => $value) {
1723
+ $capabilities[$capability] = (bool)$value;
1724
+ }
1725
+ return $capabilities;
1726
  }
1727
 
1728
  public function display_survey_notice() {
1739
  $display_notice = $display_notice && ((time() - $this->options['first_install_time']) > $minimum_usage_period);
1740
  }
1741
 
1742
+ //Only display the notice on the Menu Editor (Pro) page.
1743
  $display_notice = $display_notice && isset($this->get['page']) && ($this->get['page'] == 'menu_editor');
1744
+
1745
+ //Let the user override this completely (useful for client sites).
1746
+ if ( $display_notice && file_exists(dirname($this->plugin_file) . '/never-display-surveys.txt') ) {
1747
+ $display_notice = false;
1748
+ $this->options['display_survey_notice'] = false;
1749
+ $this->save_options();
1750
+ }
1751
 
1752
  if ( $display_notice ) {
1753
  $free_survey_url = 'https://docs.google.com/spreadsheet/viewform?formkey=dERyeDk0OWhlbkxYcEY4QTNaMnlTQUE6MQ';
1763
  printf(
1764
  '<div class="updated">
1765
  <p><strong>Help improve Admin Menu Editor - take the user survey!</strong></p>
1766
+ <p><!--suppress HtmlUnknownTarget --><a href="%s" target="_blank" title="Opens in a new window">Take the survey</a></p>
1767
+ <p><!--suppress HtmlUnknownTarget --><a href="%s">Hide this notice</a></p>
1768
  </div>',
1769
  esc_attr($survey_url),
1770
  esc_attr($hide_url)
1788
  }
1789
  }
1790
 
1791
+ public function enqueue_helper_scripts() {
1792
+ wp_enqueue_script(
1793
+ 'ame-helper-script',
1794
+ plugins_url('js/admin-helpers.js', $this->plugin_file),
1795
+ array('jquery'),
1796
+ '20121121'
1797
+ );
1798
+ }
1799
+
1800
+ public function enqueue_helper_styles() {
1801
+ wp_enqueue_style(
1802
+ 'ame-helper-style',
1803
+ plugins_url('css/admin.css', $this->plugin_file),
1804
+ array(),
1805
+ '20130211'
1806
+ );
1807
+ }
1808
+
1809
+ /**
1810
+ * Get one of the plugin configuration values.
1811
+ *
1812
+ * @param string $name Option name.
1813
+ * @return mixed
1814
+ */
1815
+ public function get_plugin_option($name) {
1816
+ if ( array_key_exists($name, $this->options) ) {
1817
+ return $this->options[$name];
1818
+ }
1819
+ return null;
1820
+ }
1821
+
1822
+
1823
+ /**
1824
+ * Log a security-related message.
1825
+ *
1826
+ * @param string|array $message The message to add tot he log, or an array of messages.
1827
+ */
1828
+ private function log_security_note($message) {
1829
+ if ( !$this->options['security_logging_enabled'] ) {
1830
+ return;
1831
+ }
1832
+ if ( is_array($message) ) {
1833
+ $this->security_log = array_merge($this->security_log, $message);
1834
+ } else {
1835
+ $this->security_log[] = $message;
1836
+ }
1837
+ }
1838
+
1839
+ /**
1840
+ * Callback for "admin_notices".
1841
+ */
1842
+ public function display_security_log() {
1843
+ ?>
1844
+ <div class="updated">
1845
+ <h3>Admin Menu Editor security log</h3>
1846
+ <?php echo $this->get_formatted_security_log(); ?>
1847
+ </div>
1848
+ <?php
1849
+ }
1850
+
1851
+ /**
1852
+ * Get the security log in HTML format.
1853
+ *
1854
+ * @return string
1855
+ */
1856
+ private function get_formatted_security_log() {
1857
+ $log = '<div style="font: 12px/16.8px Consolas, monospace; margin-bottom: 1em;">';
1858
+ $log .= implode("<br>\n", $this->security_log);
1859
+ $log .= '</div>';
1860
+ return $log;
1861
+ }
1862
+
1863
+ /**
1864
+ * WPML support: Update strings that need translation.
1865
+ *
1866
+ * @param array $old_menu The old custom menu, if any.
1867
+ * @param array $custom_menu The new custom menu.
1868
+ */
1869
+ private function update_wpml_strings($old_menu, $custom_menu) {
1870
+ if ( !function_exists('icl_register_string') ) {
1871
+ return;
1872
+ }
1873
+
1874
+ $previous_strings = $this->get_wpml_strings($old_menu);
1875
+ $new_strings = $this->get_wpml_strings($custom_menu);
1876
+
1877
+ //Delete strings that are no longer valid.
1878
+ if ( function_exists('icl_unregister_string') ) {
1879
+ $removed_strings = array_diff_key($previous_strings, $new_strings);
1880
+ foreach($removed_strings as $name => $value) {
1881
+ icl_unregister_string(self::WPML_CONTEXT, $name);
1882
+ }
1883
+ }
1884
+
1885
+ //Register/update the new menu strings.
1886
+ foreach($new_strings as $name => $value) {
1887
+ icl_register_string(self::WPML_CONTEXT, $name, $value);
1888
+ }
1889
+ }
1890
+
1891
+ /**
1892
+ * Prepare WPML translation strings for all menu and page titles
1893
+ * in the specified menu.
1894
+ *
1895
+ * @param array $custom_menu
1896
+ * @return array Associative array of strings that can be translated, indexed by unique name.
1897
+ */
1898
+ private function get_wpml_strings($custom_menu) {
1899
+ if ( empty($custom_menu) ) {
1900
+ return array();
1901
+ }
1902
+
1903
+ $strings = array();
1904
+ $translatable_fields = array('menu_title', 'page_title');
1905
+ foreach($custom_menu['tree'] as $top_menu) {
1906
+ if ( $top_menu['separator'] ) {
1907
+ continue;
1908
+ }
1909
+
1910
+ foreach($translatable_fields as $field) {
1911
+ if ( isset($top_menu[$field]) ) {
1912
+ $name = $this->get_wpml_name_for($top_menu, $field);
1913
+ $strings[$name] = ameMenuItem::get($top_menu, $field);
1914
+ }
1915
+ }
1916
+
1917
+ if ( isset($top_menu['items']) && !empty($top_menu['items']) ) {
1918
+ foreach($top_menu['items'] as $item) {
1919
+ if ( $item['separator'] ) {
1920
+ continue;
1921
+ }
1922
+
1923
+ foreach($translatable_fields as $field) {
1924
+ if ( isset($item[$field]) ) {
1925
+ $name = $this->get_wpml_name_for($item, $field);
1926
+ $strings[$name] = ameMenuItem::get($item, $field);
1927
+ }
1928
+ }
1929
+ }
1930
+ }
1931
+ }
1932
+
1933
+ return $strings;
1934
+ }
1935
 
1936
+ /**
1937
+ * Create a unique name for a specific field of a specific menu item.
1938
+ * Intended for use with the icl_register_string() function.
1939
+ *
1940
+ * @param array $item Admin menu item in the internal format.
1941
+ * @param string $field Field name.
1942
+ * @return string
1943
+ */
1944
+ private function get_wpml_name_for($item, $field = '') {
1945
+ $name = ameMenuItem::get($item, 'template_id');
1946
+ if ( empty($name) ) {
1947
+ $name = 'custom: ' . ameMenuItem::get($item, 'file');
1948
+ }
1949
+ if ( !empty($field) ) {
1950
+ $name = $name . '[' . $field. ']';
1951
+ }
1952
+ return $name;
1953
+ }
1954
 
1955
+ } //class
includes/menu-item.php CHANGED
@@ -1,408 +1,408 @@
1
- <?php
2
-
3
- /**
4
- * This class contains a number of static methods for working with individual menu items.
5
- *
6
- * Note: This class is not fully self-contained. Some of the methods will query global state.
7
- * This is necessary because the interpretation of certain menu fields depends on things like
8
- * currently registered hooks and the presence of specific files in admin/plugin folders.
9
- */
10
- abstract class ameMenuItem {
11
- /**
12
- * Convert a WP menu structure to an associative array.
13
- *
14
- * @param array $item An menu item.
15
- * @param int $position The position (index) of the the menu item.
16
- * @param string $parent The slug of the parent menu that owns this item. Blank for top level menus.
17
- * @return array
18
- */
19
- public static function fromWpItem($item, $position = 0, $parent = '') {
20
- static $separator_count = 0;
21
- $item = array(
22
- 'menu_title' => $item[0],
23
- 'access_level' => $item[1], //= required capability
24
- 'file' => $item[2],
25
- 'page_title' => (isset($item[3]) ? $item[3] : ''),
26
- 'css_class' => (isset($item[4]) ? $item[4] : 'menu-top'),
27
- 'hookname' => (isset($item[5]) ? $item[5] : ''), //Used as the ID attr. of the generated HTML tag.
28
- 'icon_url' => (isset($item[6]) ? $item[6] : 'images/generic.png'),
29
- 'position' => $position,
30
- 'parent' => $parent,
31
- );
32
-
33
- if ( is_numeric($item['access_level']) ) {
34
- $dummyUser = new WP_User;
35
- $item['access_level'] = $dummyUser->translate_level_to_cap($item['access_level']);
36
- }
37
-
38
- if ( empty($parent) ) {
39
- $item['separator'] = empty($item['file']) || empty($item['menu_title']) || (strpos($item['css_class'], 'wp-menu-separator') !== false);
40
- //WP 3.0 in multisite mode has two separators with the same filename. Fix by reindexing separators.
41
- if ( $item['separator'] ) {
42
- $item['file'] = 'separator_' . ($separator_count++);
43
- }
44
- } else {
45
- //Submenus can't contain separators.
46
- $item['separator'] = false;
47
- }
48
-
49
- //Flag plugin pages
50
- $item['is_plugin_page'] = (get_plugin_page_hook($item['file'], $parent) != null);
51
-
52
- if ( !$item['separator'] ) {
53
- $item['url'] = self::generate_url($item['file'], $parent);
54
- }
55
-
56
- $item['template_id'] = self::template_id($item, $parent);
57
-
58
- return array_merge(self::basic_defaults(), $item);
59
- }
60
-
61
- public static function basic_defaults() {
62
- static $basic_defaults = null;
63
- if ( $basic_defaults !== null ) {
64
- return $basic_defaults;
65
- }
66
-
67
- $basic_defaults = array(
68
- //Fields that apply to all menu items.
69
- 'page_title' => '',
70
- 'menu_title' => '',
71
- 'access_level' => 'read',
72
- 'extra_capability' => '',
73
- 'file' => '',
74
- 'position' => 0,
75
- 'parent' => '',
76
-
77
- //Fields that apply only to top level menus.
78
- 'css_class' => 'menu-top',
79
- 'hookname' => '',
80
- 'icon_url' => 'images/generic.png',
81
- 'separator' => false,
82
-
83
- //Internal fields that may not map directly to WP menu structures.
84
- 'open_in' => 'same_window', //'new_window', 'iframe' or 'same_window' (the default)
85
- 'template_id' => '', //The default menu item that this item is based on.
86
- 'is_plugin_page' => false,
87
- 'custom' => false,
88
- 'url' => '',
89
- );
90
-
91
- return $basic_defaults;
92
- }
93
-
94
- public static function blank_menu() {
95
- static $blank_menu = null;
96
- if ( $blank_menu !== null ) {
97
- return $blank_menu;
98
- }
99
-
100
- //Template for a basic menu item.
101
- $blank_menu = array_fill_keys(array_keys(self::basic_defaults()), null);
102
- $blank_menu = array_merge($blank_menu, array(
103
- 'items' => array(), //List of sub-menu items.
104
- 'grant_access' => array(), //Per-role and per-user access. Supersedes role_access.
105
- 'role_access' => array(), //Per-role access settings.
106
-
107
- 'custom' => false, //True if item is made-from-scratch and has no template.
108
- 'missing' => false, //True if our template is no longer present in the default admin menu. Note: Stored values will be ignored. Set upon merging.
109
- 'unused' => false, //True if this item was generated from an unused default menu. Note: Stored values will be ignored. Set upon merging.
110
- 'hidden' => false, //Hide/show the item. Hiding is purely cosmetic, the item remains accessible.
111
-
112
- 'defaults' => self::basic_defaults(),
113
- ));
114
- return $blank_menu;
115
- }
116
-
117
- public static function custom_item_defaults() {
118
- return array(
119
- 'menu_title' => 'Custom Menu',
120
- 'access_level' => 'read',
121
- 'page_title' => '',
122
- 'css_class' => 'menu-top',
123
- 'hookname' => '',
124
- 'icon_url' => 'images/generic.png',
125
- 'open_in' => 'same_window',
126
- 'is_plugin_page' => false,
127
- );
128
- }
129
-
130
- /**
131
- * Get the value of a menu/submenu field.
132
- * Will return the corresponding value from the 'defaults' entry of $item if the
133
- * specified field is not set in the item itself.
134
- *
135
- * @param array $item
136
- * @param string $field_name
137
- * @param mixed $default Returned if the requested field is not set and is not listed in $item['defaults']. Defaults to null.
138
- * @return mixed Field value.
139
- */
140
- public static function get($item, $field_name, $default = null){
141
- if ( isset($item[$field_name]) ){
142
- return $item[$field_name];
143
- } else {
144
- if ( isset($item['defaults'], $item['defaults'][$field_name]) ){
145
- return $item['defaults'][$field_name];
146
- } else {
147
- return $default;
148
- }
149
- }
150
- }
151
-
152
- /**
153
- * Generate or retrieve an ID that semi-uniquely identifies the template
154
- * of the given menu item.
155
- *
156
- * Note that custom items (i.e. those that do not point to any of the default
157
- * admin menu pages) have no template IDs.
158
- *
159
- * The ID is generated from the item's and its parent's file attributes.
160
- * Since WordPress technically allows two copies of the same menu to exist
161
- * in the same sub-menu, this combination is not necessarily unique.
162
- *
163
- * @param array|string $item The menu item in question.
164
- * @param string $parent_file The parent menu. If omitted, $item['defaults']['parent'] will be used.
165
- * @return string Template ID, or an empty string if this is a custom item.
166
- */
167
- public static function template_id($item, $parent_file = ''){
168
- if (is_string($item)) {
169
- return $parent_file . '>' . $item;
170
- }
171
-
172
- if ( self::get($item, 'custom') ) {
173
- return '';
174
- }
175
-
176
- //Maybe it already has an ID?
177
- $template_id = self::get($item, 'template_id');
178
- if ( !empty($template_id) ) {
179
- return $template_id;
180
- }
181
-
182
- if ( isset($item['defaults']['file']) ) {
183
- $item_file = $item['defaults']['file'];
184
- } else {
185
- $item_file = self::get($item, 'file');
186
- }
187
-
188
- if ( empty($parent_file) ) {
189
- if ( isset($item['defaults']['parent']) ) {
190
- $parent_file = $item['defaults']['parent'];
191
- } else {
192
- $parent_file = self::get($item, 'parent');
193
- }
194
- }
195
-
196
- return $parent_file . '>' . $item_file;
197
- }
198
-
199
- /**
200
- * Set all undefined menu fields to the default value.
201
- *
202
- * @param array $item Menu item in the plugin's internal form
203
- * @return array
204
- */
205
- public static function apply_defaults($item){
206
- foreach($item as $key => $value){
207
- //Is the field set?
208
- if ($value === null){
209
- //Use default, if available
210
- if (isset($item['defaults'], $item['defaults'][$key])){
211
- $item[$key] = $item['defaults'][$key];
212
- }
213
- }
214
- }
215
- return $item;
216
- }
217
-
218
- /**
219
- * Apply custom menu filters to an item of the custom menu.
220
- *
221
- * Calls two types of filters :
222
- * 'custom_admin_$item_type' with the entire $item passed as the argument.
223
- * 'custom_admin_$item_type-$field' with the value of a single field of $item as the argument.
224
- *
225
- * Used when converting the current custom menu to a WP-format menu.
226
- *
227
- * @param array $item Associative array representing one menu item (either top-level or submenu).
228
- * @param string $item_type 'menu' or 'submenu'
229
- * @param mixed $extra Optional extra data to pass to hooks.
230
- * @return array Filtered menu item.
231
- */
232
- public static function apply_filters($item, $item_type, $extra = null){
233
- $item = apply_filters("custom_admin_{$item_type}", $item, $extra);
234
- foreach($item as $field => $value){
235
- $item[$field] = apply_filters("custom_admin_{$item_type}-$field", $value, $extra);
236
- }
237
-
238
- return $item;
239
- }
240
-
241
- /**
242
- * Recursively normalize a menu item and all of its sub-items.
243
- *
244
- * This will also ensure that the item has all the required fields.
245
- *
246
- * @static
247
- * @param array $item
248
- * @return array
249
- */
250
- public static function normalize($item) {
251
- if ( isset($item['defaults']) ) {
252
- $item['defaults'] = array_merge(self::basic_defaults(), $item['defaults']);
253
- }
254
- $item = array_merge(self::blank_menu(), $item);
255
-
256
- $item['unused'] = false;
257
- $item['missing'] = false;
258
- $item['template_id'] = self::template_id($item);
259
-
260
- //Items pointing to a default page can't have a custom file/URL.
261
- if ( ($item['template_id'] !== '') && ($item['file'] !== null) ) {
262
- if ( $item['file'] == $item['defaults']['file'] ) {
263
- //Identical to default, so just set it to use that.
264
- $item['file'] = null;
265
- } else {
266
- //Different file = convert to a custom item. Need to call fix_defaults()
267
- //to fix other fields that are currently set to defaults custom items don't have.
268
- $item['template_id'] = '';
269
- }
270
- }
271
-
272
- $item['custom'] = $item['custom'] || ($item['template_id'] == '');
273
- $item = self::fix_defaults($item);
274
-
275
- //Older versions would allow the user to set the required capability directly.
276
- //This was incorrect since for default menu items the default cap was *always*
277
- //applied anyway, and the new cap was applied on top of that. We make that explicit
278
- //by storing the custom cap in a separate field - extra_capability - and keeping
279
- //access_level (required cap) at the default value.
280
- if ( isset($item['defaults']) && $item['access_level'] !== null ) {
281
- if ( empty($item['extra_capability']) ) {
282
- $item['extra_capability'] = $item['access_level'];
283
- }
284
- $item['access_level'] = null;
285
- }
286
-
287
- //Convert per-role access settings to the more general grant_access format.
288
- if ( isset($item['role_access']) ) {
289
- foreach($item['role_access'] as $role_id => $has_access) {
290
- $item['grant_access']['role:' . $role_id] = $has_access;
291
- }
292
- $item['role_access'] = array();
293
- }
294
-
295
- if ( isset($item['items']) ) {
296
- foreach($item['items'] as $index => $sub_item) {
297
- $item['items'][$index] = self::normalize($sub_item);
298
- }
299
- }
300
-
301
- return $item;
302
- }
303
-
304
- /**
305
- * Fix obsolete default values on custom items.
306
- *
307
- * In older versions of the plugin, each custom item had its own set of defaults.
308
- * It was also possible to create a pseudo-custom item from a default item by
309
- * freely overwriting its fields with custom values.
310
- *
311
- * The current version uses the same defaults for all custom items. To avoid data
312
- * loss, we'll check for any mismatches and make such defaults explicit.
313
- *
314
- * @static
315
- * @param array $item
316
- * @return array
317
- */
318
- private static function fix_defaults($item) {
319
- if ( $item['custom'] && isset($item['defaults']) ) {
320
- $new_defaults = self::custom_item_defaults();
321
- foreach($item as $field => $value) {
322
- $is_mismatch = is_null($value)
323
- && array_key_exists($field, $item['defaults'])
324
- && (
325
- !array_key_exists($field, $new_defaults) //No default.
326
- || ($item['defaults'][$field] != $new_defaults[$field]) //Different default.
327
- );
328
-
329
- if ( $is_mismatch ) {
330
- $item[$field] = $item['defaults'][$field];
331
- }
332
- }
333
- $item['defaults'] = $new_defaults;
334
- }
335
- return $item;
336
- }
337
-
338
- /**
339
- * Custom comparison function that compares menu items based on their position in the menu.
340
- *
341
- * @param array $a
342
- * @param array $b
343
- * @return int
344
- */
345
- public static function compare_position($a, $b){
346
- return self::get($a, 'position', 0) - self::get($b, 'position', 0);
347
- }
348
-
349
- /**
350
- * Generate a URL for a menu item.
351
- *
352
- * @param string $item_slug
353
- * @param string $parent_slug
354
- * @return string An URL relative to the /wp-admin/ directory.
355
- */
356
- public static function generate_url($item_slug, $parent_slug = '') {
357
- $menu_url = is_array($item_slug) ? self::get($item_slug, 'file') : $item_slug;
358
- $parent_url = !empty($parent_slug) ? $parent_slug : 'admin.php';
359
-
360
- if ( strpos($menu_url, '://') !== false ) {
361
- return $menu_url;
362
- }
363
-
364
- if ( self::is_hook_or_plugin_page($menu_url, $parent_url) ) {
365
- $base_file = self::is_hook_or_plugin_page($parent_url) ? 'admin.php' : $parent_url;
366
- $url = add_query_arg(array('page' => $menu_url), $base_file);
367
- } else {
368
- $url = $menu_url;
369
- }
370
- return $url;
371
- }
372
-
373
- private static function is_hook_or_plugin_page($page_url, $parent_page_url = '') {
374
- if ( empty($parent_page_url) ) {
375
- $parent_page_url = 'admin.php';
376
- }
377
- $pageFile = self::remove_query_from($page_url);
378
-
379
- $hasHook = (get_plugin_page_hook($page_url, $parent_page_url) !== null);
380
- $adminFileExists = is_file(ABSPATH . '/wp-admin/' . $pageFile);
381
- $pluginFileExists = ($page_url != 'index.php') && is_file(WP_PLUGIN_DIR . '/' . $pageFile);
382
-
383
- return !$adminFileExists && ($hasHook || $pluginFileExists);
384
- }
385
-
386
- /**
387
- * Check if a field is currently set to its default value.
388
- *
389
- * @param array $item
390
- * @param string $field_name
391
- * @return bool
392
- */
393
- public static function is_default($item, $field_name) {
394
- if ( isset($item[$field_name]) ){
395
- return false;
396
- } else {
397
- return isset($item['defaults'], $item['defaults'][$field_name]);
398
- }
399
- }
400
-
401
- public static function remove_query_from($url) {
402
- $pos = strpos($url, '?');
403
- if ( $pos !== false ) {
404
- return substr($url, 0, $pos);
405
- }
406
- return $url;
407
- }
408
  }
1
+ <?php
2
+
3
+ /**
4
+ * This class contains a number of static methods for working with individual menu items.
5
+ *
6
+ * Note: This class is not fully self-contained. Some of the methods will query global state.
7
+ * This is necessary because the interpretation of certain menu fields depends on things like
8
+ * currently registered hooks and the presence of specific files in admin/plugin folders.
9
+ */
10
+ abstract class ameMenuItem {
11
+ /**
12
+ * Convert a WP menu structure to an associative array.
13
+ *
14
+ * @param array $item An menu item.
15
+ * @param int $position The position (index) of the the menu item.
16
+ * @param string $parent The slug of the parent menu that owns this item. Blank for top level menus.
17
+ * @return array
18
+ */
19
+ public static function fromWpItem($item, $position = 0, $parent = '') {
20
+ static $separator_count = 0;
21
+ $item = array(
22
+ 'menu_title' => $item[0],
23
+ 'access_level' => $item[1], //= required capability
24
+ 'file' => $item[2],
25
+ 'page_title' => (isset($item[3]) ? $item[3] : ''),
26
+ 'css_class' => (isset($item[4]) ? $item[4] : 'menu-top'),
27
+ 'hookname' => (isset($item[5]) ? $item[5] : ''), //Used as the ID attr. of the generated HTML tag.
28
+ 'icon_url' => (isset($item[6]) ? $item[6] : 'images/generic.png'),
29
+ 'position' => $position,
30
+ 'parent' => $parent,
31
+ );
32
+
33
+ if ( is_numeric($item['access_level']) ) {
34
+ $dummyUser = new WP_User;
35
+ $item['access_level'] = $dummyUser->translate_level_to_cap($item['access_level']);
36
+ }
37
+
38
+ if ( empty($parent) ) {
39
+ $item['separator'] = empty($item['file']) || empty($item['menu_title']) || (strpos($item['css_class'], 'wp-menu-separator') !== false);
40
+ //WP 3.0 in multisite mode has two separators with the same filename. Fix by reindexing separators.
41
+ if ( $item['separator'] ) {
42
+ $item['file'] = 'separator_' . ($separator_count++);
43
+ }
44
+ } else {
45
+ //Submenus can't contain separators.
46
+ $item['separator'] = false;
47
+ }
48
+
49
+ //Flag plugin pages
50
+ $item['is_plugin_page'] = (get_plugin_page_hook($item['file'], $parent) != null);
51
+
52
+ if ( !$item['separator'] ) {
53
+ $item['url'] = self::generate_url($item['file'], $parent);
54
+ }
55
+
56
+ $item['template_id'] = self::template_id($item, $parent);
57
+
58
+ return array_merge(self::basic_defaults(), $item);
59
+ }
60
+
61
+ public static function basic_defaults() {
62
+ static $basic_defaults = null;
63
+ if ( $basic_defaults !== null ) {
64
+ return $basic_defaults;
65
+ }
66
+
67
+ $basic_defaults = array(
68
+ //Fields that apply to all menu items.
69
+ 'page_title' => '',
70
+ 'menu_title' => '',
71
+ 'access_level' => 'read',
72
+ 'extra_capability' => '',
73
+ 'file' => '',
74
+ 'position' => 0,
75
+ 'parent' => '',
76
+
77
+ //Fields that apply only to top level menus.
78
+ 'css_class' => 'menu-top',
79
+ 'hookname' => '',
80
+ 'icon_url' => 'images/generic.png',
81
+ 'separator' => false,
82
+
83
+ //Internal fields that may not map directly to WP menu structures.
84
+ 'open_in' => 'same_window', //'new_window', 'iframe' or 'same_window' (the default)
85
+ 'template_id' => '', //The default menu item that this item is based on.
86
+ 'is_plugin_page' => false,
87
+ 'custom' => false,
88
+ 'url' => '',
89
+ );
90
+
91
+ return $basic_defaults;
92
+ }
93
+
94
+ public static function blank_menu() {
95
+ static $blank_menu = null;
96
+ if ( $blank_menu !== null ) {
97
+ return $blank_menu;
98
+ }
99
+
100
+ //Template for a basic menu item.
101
+ $blank_menu = array_fill_keys(array_keys(self::basic_defaults()), null);
102
+ $blank_menu = array_merge($blank_menu, array(
103
+ 'items' => array(), //List of sub-menu items.
104
+ 'grant_access' => array(), //Per-role and per-user access. Supersedes role_access.
105
+ 'role_access' => array(), //Per-role access settings.
106
+
107
+ 'custom' => false, //True if item is made-from-scratch and has no template.
108
+ 'missing' => false, //True if our template is no longer present in the default admin menu. Note: Stored values will be ignored. Set upon merging.
109
+ 'unused' => false, //True if this item was generated from an unused default menu. Note: Stored values will be ignored. Set upon merging.
110
+ 'hidden' => false, //Hide/show the item. Hiding is purely cosmetic, the item remains accessible.
111
+
112
+ 'defaults' => self::basic_defaults(),
113
+ ));
114
+ return $blank_menu;
115
+ }
116
+
117
+ public static function custom_item_defaults() {
118
+ return array(
119
+ 'menu_title' => 'Custom Menu',
120
+ 'access_level' => 'read',
121
+ 'page_title' => '',
122
+ 'css_class' => 'menu-top',
123
+ 'hookname' => '',
124
+ 'icon_url' => 'images/generic.png',
125
+ 'open_in' => 'same_window',
126
+ 'is_plugin_page' => false,
127
+ );
128
+ }
129
+
130
+ /**
131
+ * Get the value of a menu/submenu field.
132
+ * Will return the corresponding value from the 'defaults' entry of $item if the
133
+ * specified field is not set in the item itself.
134
+ *
135
+ * @param array $item
136
+ * @param string $field_name
137
+ * @param mixed $default Returned if the requested field is not set and is not listed in $item['defaults']. Defaults to null.
138
+ * @return mixed Field value.
139
+ */
140
+ public static function get($item, $field_name, $default = null){
141
+ if ( isset($item[$field_name]) ){
142
+ return $item[$field_name];
143
+ } else {
144
+ if ( isset($item['defaults'], $item['defaults'][$field_name]) ){
145
+ return $item['defaults'][$field_name];
146
+ } else {
147
+ return $default;
148
+ }
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Generate or retrieve an ID that semi-uniquely identifies the template
154
+ * of the given menu item.
155
+ *
156
+ * Note that custom items (i.e. those that do not point to any of the default
157
+ * admin menu pages) have no template IDs.
158
+ *
159
+ * The ID is generated from the item's and its parent's file attributes.
160
+ * Since WordPress technically allows two copies of the same menu to exist
161
+ * in the same sub-menu, this combination is not necessarily unique.
162
+ *
163
+ * @param array|string $item The menu item in question.
164
+ * @param string $parent_file The parent menu. If omitted, $item['defaults']['parent'] will be used.
165
+ * @return string Template ID, or an empty string if this is a custom item.
166
+ */
167
+ public static function template_id($item, $parent_file = ''){
168
+ if (is_string($item)) {
169
+ return $parent_file . '>' . $item;
170
+ }
171
+
172
+ if ( self::get($item, 'custom') ) {
173
+ return '';
174
+ }
175
+
176
+ //Maybe it already has an ID?
177
+ $template_id = self::get($item, 'template_id');
178
+ if ( !empty($template_id) ) {
179
+ return $template_id;
180
+ }
181
+
182
+ if ( isset($item['defaults']['file']) ) {
183
+ $item_file = $item['defaults']['file'];
184
+ } else {
185
+ $item_file = self::get($item, 'file');
186
+ }
187
+
188
+ if ( empty($parent_file) ) {
189
+ if ( isset($item['defaults']['parent']) ) {
190
+ $parent_file = $item['defaults']['parent'];
191
+ } else {
192
+ $parent_file = self::get($item, 'parent');
193
+ }
194
+ }
195
+
196
+ return $parent_file . '>' . $item_file;
197
+ }
198
+
199
+ /**
200
+ * Set all undefined menu fields to the default value.
201
+ *
202
+ * @param array $item Menu item in the plugin's internal form
203
+ * @return array
204
+ */
205
+ public static function apply_defaults($item){
206
+ foreach($item as $key => $value){
207
+ //Is the field set?
208
+ if ($value === null){
209
+ //Use default, if available
210
+ if (isset($item['defaults'], $item['defaults'][$key])){
211
+ $item[$key] = $item['defaults'][$key];
212
+ }
213
+ }
214
+ }
215
+ return $item;
216
+ }
217
+
218
+ /**
219
+ * Apply custom menu filters to an item of the custom menu.
220
+ *
221
+ * Calls two types of filters :
222
+ * 'custom_admin_$item_type' with the entire $item passed as the argument.
223
+ * 'custom_admin_$item_type-$field' with the value of a single field of $item as the argument.
224
+ *
225
+ * Used when converting the current custom menu to a WP-format menu.
226
+ *
227
+ * @param array $item Associative array representing one menu item (either top-level or submenu).
228
+ * @param string $item_type 'menu' or 'submenu'
229
+ * @param mixed $extra Optional extra data to pass to hooks.
230
+ * @return array Filtered menu item.
231
+ */
232
+ public static function apply_filters($item, $item_type, $extra = null){
233
+ $item = apply_filters("custom_admin_{$item_type}", $item, $extra);
234
+ foreach($item as $field => $value){
235
+ $item[$field] = apply_filters("custom_admin_{$item_type}-$field", $value, $extra);
236
+ }
237
+
238
+ return $item;
239
+ }
240
+
241
+ /**
242
+ * Recursively normalize a menu item and all of its sub-items.
243
+ *
244
+ * This will also ensure that the item has all the required fields.
245
+ *
246
+ * @static
247
+ * @param array $item
248
+ * @return array
249
+ */
250
+ public static function normalize($item) {
251
+ if ( isset($item['defaults']) ) {
252
+ $item['defaults'] = array_merge(self::basic_defaults(), $item['defaults']);
253
+ }
254
+ $item = array_merge(self::blank_menu(), $item);
255
+
256
+ $item['unused'] = false;
257
+ $item['missing'] = false;
258
+ $item['template_id'] = self::template_id($item);
259
+
260
+ //Items pointing to a default page can't have a custom file/URL.
261
+ if ( ($item['template_id'] !== '') && ($item['file'] !== null) ) {
262
+ if ( $item['file'] == $item['defaults']['file'] ) {
263
+ //Identical to default, so just set it to use that.
264
+ $item['file'] = null;
265
+ } else {
266
+ //Different file = convert to a custom item. Need to call fix_defaults()
267
+ //to fix other fields that are currently set to defaults custom items don't have.
268
+ $item['template_id'] = '';
269
+ }
270
+ }
271
+
272
+ $item['custom'] = $item['custom'] || ($item['template_id'] == '');
273
+ $item = self::fix_defaults($item);
274
+
275
+ //Older versions would allow the user to set the required capability directly.
276
+ //This was incorrect since for default menu items the default cap was *always*
277
+ //applied anyway, and the new cap was applied on top of that. We make that explicit
278
+ //by storing the custom cap in a separate field - extra_capability - and keeping
279
+ //access_level (required cap) at the default value.
280
+ if ( isset($item['defaults']) && $item['access_level'] !== null ) {
281
+ if ( empty($item['extra_capability']) ) {
282
+ $item['extra_capability'] = $item['access_level'];
283
+ }
284
+ $item['access_level'] = null;
285
+ }
286
+
287
+ //Convert per-role access settings to the more general grant_access format.
288
+ if ( isset($item['role_access']) ) {
289
+ foreach($item['role_access'] as $role_id => $has_access) {
290
+ $item['grant_access']['role:' . $role_id] = $has_access;
291
+ }
292
+ $item['role_access'] = array();
293
+ }
294
+
295
+ if ( isset($item['items']) ) {
296
+ foreach($item['items'] as $index => $sub_item) {
297
+ $item['items'][$index] = self::normalize($sub_item);
298
+ }
299
+ }
300
+
301
+ return $item;
302
+ }
303
+
304
+ /**
305
+ * Fix obsolete default values on custom items.
306
+ *
307
+ * In older versions of the plugin, each custom item had its own set of defaults.
308
+ * It was also possible to create a pseudo-custom item from a default item by
309
+ * freely overwriting its fields with custom values.
310
+ *
311
+ * The current version uses the same defaults for all custom items. To avoid data
312
+ * loss, we'll check for any mismatches and make such defaults explicit.
313
+ *
314
+ * @static
315
+ * @param array $item
316
+ * @return array
317
+ */
318
+ private static function fix_defaults($item) {
319
+ if ( $item['custom'] && isset($item['defaults']) ) {
320
+ $new_defaults = self::custom_item_defaults();
321
+ foreach($item as $field => $value) {
322
+ $is_mismatch = is_null($value)
323
+ && array_key_exists($field, $item['defaults'])
324
+ && (
325
+ !array_key_exists($field, $new_defaults) //No default.
326
+ || ($item['defaults'][$field] != $new_defaults[$field]) //Different default.
327
+ );
328
+
329
+ if ( $is_mismatch ) {
330
+ $item[$field] = $item['defaults'][$field];
331
+ }
332
+ }
333
+ $item['defaults'] = $new_defaults;
334
+ }
335
+ return $item;
336
+ }
337
+
338
+ /**
339
+ * Custom comparison function that compares menu items based on their position in the menu.
340
+ *
341
+ * @param array $a
342
+ * @param array $b
343
+ * @return int
344
+ */
345
+ public static function compare_position($a, $b){
346
+ return self::get($a, 'position', 0) - self::get($b, 'position', 0);
347
+ }
348
+
349
+ /**
350
+ * Generate a URL for a menu item.
351
+ *
352
+ * @param string $item_slug
353
+ * @param string $parent_slug
354
+ * @return string An URL relative to the /wp-admin/ directory.
355
+ */
356
+ public static function generate_url($item_slug, $parent_slug = '') {
357
+ $menu_url = is_array($item_slug) ? self::get($item_slug, 'file') : $item_slug;
358
+ $parent_url = !empty($parent_slug) ? $parent_slug : 'admin.php';
359
+
360
+ if ( strpos($menu_url, '://') !== false ) {
361
+ return $menu_url;
362
+ }
363
+
364
+ if ( self::is_hook_or_plugin_page($menu_url, $parent_url) ) {
365
+ $base_file = self::is_hook_or_plugin_page($parent_url) ? 'admin.php' : $parent_url;
366
+ $url = add_query_arg(array('page' => $menu_url), $base_file);
367
+ } else {
368
+ $url = $menu_url;
369
+ }
370
+ return $url;
371
+ }
372
+
373
+ private static function is_hook_or_plugin_page($page_url, $parent_page_url = '') {
374
+ if ( empty($parent_page_url) ) {
375
+ $parent_page_url = 'admin.php';
376
+ }
377
+ $pageFile = self::remove_query_from($page_url);
378
+
379
+ $hasHook = (get_plugin_page_hook($page_url, $parent_page_url) !== null);
380
+ $adminFileExists = is_file(ABSPATH . '/wp-admin/' . $pageFile);
381
+ $pluginFileExists = ($page_url != 'index.php') && is_file(WP_PLUGIN_DIR . '/' . $pageFile);
382
+
383
+ return !$adminFileExists && ($hasHook || $pluginFileExists);
384
+ }
385
+
386
+ /**
387
+ * Check if a field is currently set to its default value.
388
+ *
389
+ * @param array $item
390
+ * @param string $field_name
391
+ * @return bool
392
+ */
393
+ public static function is_default($item, $field_name) {
394
+ if ( isset($item[$field_name]) ){
395
+ return false;
396
+ } else {
397
+ return isset($item['defaults'], $item['defaults'][$field_name]);
398
+ }
399
+ }
400
+
401
+ public static function remove_query_from($url) {
402
+ $pos = strpos($url, '?');
403
+ if ( $pos !== false ) {
404
+ return substr($url, 0, $pos);
405
+ }
406
+ return $url;
407
+ }
408
  }
includes/menu.php ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ abstract class ameMenu {
3
+ const format_name = 'Admin Menu Editor menu';
4
+ const format_version = '5.0';
5
+
6
+ /**
7
+ * Load an admin menu from a JSON string.
8
+ *
9
+ * @static
10
+ * @throws InvalidMenuException when the supplied input is not a valid menu.
11
+ *
12
+ * @param string $json A JSON-encoded menu structure.
13
+ * @param bool $assume_correct_format Skip the format header check and assume everything is fine. Defaults to false.
14
+ * @return array
15
+ */
16
+ public static function load_json($json, $assume_correct_format = false) {
17
+ $arr = json_decode($json, true);
18
+ if ( !is_array($arr) ) {
19
+ throw new InvalidMenuException('The input is not a valid JSON-encoded admin menu.');
20
+ }
21
+ return self::load_array($arr, $assume_correct_format);
22
+ }
23
+
24
+ /**
25
+ * Load an admin menu structure from an associative array.
26
+ *
27
+ * @static
28
+ * @throws InvalidMenuException when the supplied input is not a valid menu.
29
+ *
30
+ * @param array $arr
31
+ * @param bool $assume_correct_format
32
+ * @return array
33
+ */
34
+ public static function load_array($arr, $assume_correct_format = false){
35
+ if ( !$assume_correct_format ) {
36
+ if ( isset($arr['format']) && ($arr['format']['name'] == self::format_name) ) {
37
+ if ( !version_compare($arr['format']['version'], self::format_version, '<=') ) {
38
+ throw new InvalidMenuException("Can't load a menu created by a newer version of the plugin.");
39
+ }
40
+ } else {
41
+ return self::load_menu_40($arr);
42
+ }
43
+ }
44
+
45
+ if ( !(isset($arr['tree']) && is_array($arr['tree'])) ) {
46
+ throw new InvalidMenuException("Failed to load a menu - the menu tree is missing.");
47
+ }
48
+
49
+ $menu = array('tree' => array());
50
+ $menu = self::add_format_header($menu);
51
+
52
+ foreach($arr['tree'] as $file => $item) {
53
+ $menu['tree'][$file] = ameMenuItem::normalize($item);
54
+ }
55
+
56
+ return $menu;
57
+ }
58
+
59
+ /**
60
+ * "Pre-load" an old menu structure.
61
+ *
62
+ * In older versions of the plugin, the entire menu consisted of
63
+ * just the menu tree and nothing else. This was internally known as
64
+ * menu format "4".
65
+ *
66
+ * To improve portability and forward-compatibility, newer versions
67
+ * use a simple dictionary-based container instead, with the menu tree
68
+ * being one of the possible entries.
69
+ *
70
+ * @static
71
+ * @param array $arr
72
+ * @return array
73
+ */
74
+ private static function load_menu_40($arr) {
75
+ //This is *very* basic and might need to be improved.
76
+ $menu = array('tree' => $arr);
77
+ return self::load_array($menu, true);
78
+ }
79
+
80
+ private static function add_format_header($menu) {
81
+ $menu['format'] = array(
82
+ 'name' => self::format_name,
83
+ 'version' => self::format_version,
84
+ );
85
+ return $menu;
86
+ }
87
+
88
+ /**
89
+ * Serialize an admin menu as JSON.
90
+ *
91
+ * @static
92
+ * @param array $menu
93
+ * @return string
94
+ */
95
+ public static function to_json($menu) {
96
+ $menu = self::add_format_header($menu);
97
+ return json_encode($menu);
98
+ }
99
+
100
+ /**
101
+ * Sort the menus and menu items of a given menu according to their positions
102
+ *
103
+ * @param array $tree A menu structure in the internal format (just the tree).
104
+ * @return array Sorted menu in the internal format
105
+ */
106
+ public static function sort_menu_tree($tree){
107
+ //Resort the tree to ensure the found items are in the right spots
108
+ uasort($tree, 'ameMenuItem::compare_position');
109
+ //Resort all submenus as well
110
+ foreach ($tree as &$topmenu){
111
+ if (!empty($topmenu['items'])){
112
+ uasort($topmenu['items'], 'ameMenuItem::compare_position');
113
+ }
114
+ }
115
+
116
+ return $tree;
117
+ }
118
+
119
+ /**
120
+ * Convert the WP menu structure to the internal representation. All properties set as defaults.
121
+ *
122
+ * @param array $menu
123
+ * @param array $submenu
124
+ * @return array Menu in the internal tree format.
125
+ */
126
+ public static function wp2tree($menu, $submenu){
127
+ $tree = array();
128
+ foreach ($menu as $pos => $item){
129
+
130
+ $tree_item = ameMenuItem::blank_menu();
131
+ $tree_item['defaults'] = ameMenuItem::fromWpItem($item, $pos);
132
+ $tree_item['separator'] = $tree_item['defaults']['separator'];
133
+
134
+ //Attach sub-menu items
135
+ $parent = $tree_item['defaults']['file'];
136
+ if ( isset($submenu[$parent]) ){
137
+ foreach($submenu[$parent] as $position => $subitem){
138
+ $tree_item['items'][$subitem[2]] = array_merge(
139
+ ameMenuItem::blank_menu(),
140
+ array('defaults' => ameMenuItem::fromWpItem($subitem, $position, $parent))
141
+ );
142
+ }
143
+ }
144
+
145
+ $tree[$parent] = $tree_item;
146
+ }
147
+
148
+ $tree = self::sort_menu_tree($tree);
149
+
150
+ return $tree;
151
+ }
152
+ }
153
+
154
+
155
+ class InvalidMenuException extends Exception {}
includes/role-utils.php ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ class ameRoleUtils {
3
+ /**
4
+ * Retrieve a list of all known capabilities of all roles
5
+ *
6
+ * @return array Associative array with capability names as keys
7
+ */
8
+ public static function get_all_capabilities(){
9
+ $wp_roles = self::get_roles();
10
+ $capabilities = array();
11
+
12
+ //Iterate over all known roles and collect their capabilities
13
+ foreach($wp_roles->roles as $role){
14
+ if ( !empty($role['capabilities']) && is_array($role['capabilities']) ){ //Being defensive here
15
+ $capabilities = array_merge($capabilities, $role['capabilities']);
16
+ }
17
+ }
18
+
19
+ //Add multisite-specific capabilities (not listed in any roles in WP 3.0)
20
+ $multisite_caps = array(
21
+ 'manage_sites' => 1,
22
+ 'manage_network' => 1,
23
+ 'manage_network_users' => 1,
24
+ 'manage_network_themes' => 1,
25
+ 'manage_network_options' => 1,
26
+ 'manage_network_plugins' => 1,
27
+ );
28
+ $capabilities = array_merge($capabilities, $multisite_caps);
29
+
30
+ return $capabilities;
31
+ }
32
+
33
+ /**
34
+ * Retrieve a list of all known roles and their names.
35
+ *
36
+ * @return array Associative array with role IDs as keys and role display names as values
37
+ */
38
+ public static function get_role_names(){
39
+ $wp_roles = self::get_roles();
40
+ $roles = array();
41
+
42
+ foreach($wp_roles->roles as $role_id => $role){
43
+ $roles[$role_id] = $role['name'];
44
+ }
45
+
46
+ return $roles;
47
+ }
48
+
49
+ /**
50
+ * Get all defined WordPress roles.
51
+ *
52
+ * @global WP_Roles $wp_roles
53
+ * @return WP_Roles
54
+ */
55
+ public static function get_roles() {
56
+ global $wp_roles;
57
+ if ( !isset($wp_roles) ) {
58
+ $wp_roles = new WP_Roles();
59
+ }
60
+ //TODO: Do something about Super Admin
61
+ return $wp_roles;
62
+ }
63
+ }
includes/settings-page.php ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * This is the HTML template for the plugin settings page.
4
+ *
5
+ * These variables are provided by the plugin:
6
+ * @var array $settings Plugin settings.
7
+ * @var string $editor_page_url A fully qualified URL of the admin menu editor page.
8
+ * @var string $settings_page_url
9
+ */
10
+
11
+ $currentUser = wp_get_current_user();
12
+ $isMultisite = is_multisite();
13
+ $isSuperAdmin = is_super_admin();
14
+ $formActionUrl = add_query_arg('noheader', 1, $settings_page_url);
15
+ $isProVersion = apply_filters('admin_menu_editor_is_pro', false);
16
+ ?>
17
+
18
+ <div class="wrap">
19
+ <?php screen_icon(); ?>
20
+ <h2>
21
+ <?php echo apply_filters('admin_menu_editor-self_page_title', 'Menu Editor'); ?> Settings
22
+ <a href="<?php echo esc_attr($editor_page_url); ?>" class="add-new-h2"
23
+ title="Back to Admin Menu Editor">Editor</a>
24
+ </h2>
25
+
26
+ <form method="post" action="<?php echo esc_attr($formActionUrl); ?>" id="ws_plugin_settings_form">
27
+
28
+ <table class="form-table">
29
+ <tbody>
30
+ <tr>
31
+ <th scope="row">
32
+ Who can access this plugin
33
+ </th>
34
+ <td>
35
+ <fieldset>
36
+ <p>
37
+ <label>
38
+ <input type="radio" name="plugin_access" value="super_admin"
39
+ <?php checked('super_admin', $settings['plugin_access']); ?>
40
+ <?php disabled( !$isSuperAdmin ); ?>>
41
+ Super Admin
42
+
43
+ <?php if ( !$isMultisite ) : ?>
44
+ <br><span class="description">
45
+ On a single site installation this is usually
46
+ the same as the Administrator role.
47
+ </span>
48
+ <?php endif; ?>
49
+ </label>
50
+ </p>
51
+
52
+ <p>
53
+ <label>
54
+ <input type="radio" name="plugin_access" value="manage_options"
55
+ <?php checked('manage_options', $settings['plugin_access']); ?>
56
+ <?php disabled( !current_user_can('manage_options') ); ?>>
57
+ Anyone with the "manage_options" capability
58
+
59
+ <br><span class="description">
60
+ By default only Administrators have this capability.
61
+ </span>
62
+ </label>
63
+ </p>
64
+
65
+ <p>
66
+ <label>
67
+ <input type="radio" name="plugin_access" value="specific_user"
68
+ <?php checked('specific_user', $settings['plugin_access']); ?>
69
+ <?php disabled( $isMultisite && !$isSuperAdmin ); ?>>
70
+ Only the current user
71
+
72
+ <br>
73
+ <span class="description">
74
+ Login: <?php echo $currentUser->user_login; ?>,
75
+ user ID: <?php echo get_current_user_id(); ?>
76
+ </span>
77
+ </label>
78
+ </p>
79
+ </fieldset>
80
+
81
+ <p>
82
+ <label>
83
+ <input type="checkbox" name="hide_plugin_from_others" value="1"
84
+ <?php checked( $settings['plugins_page_allowed_user_id'] !== null ); ?>
85
+ <?php disabled( !$isProVersion || ($isMultisite && !is_super_admin()) ); ?>
86
+ >
87
+ Hide the "Admin Menu Editor<?php if ( $isProVersion ) { echo ' Pro'; } ?>" entry on the "Plugins" page from other users
88
+ <?php if ( !$isProVersion ) {
89
+ echo '(Pro version only)';
90
+ } ?>
91
+ </label>
92
+ </p>
93
+ </td>
94
+ </tr>
95
+
96
+ <tr>
97
+ <th scope="row">
98
+ Multisite settings
99
+ </th>
100
+ <td>
101
+ <fieldset id="ame-menu-scope-settings">
102
+ <p>
103
+ <label>
104
+ <input type="radio" name="menu_config_scope" value="global"
105
+ id="ame-menu-config-scope-global"
106
+ <?php checked('global', $settings['menu_config_scope']); ?>
107
+ <?php disabled(!$isMultisite || !$isSuperAdmin); ?>>
108
+ Global &mdash;
109
+ Use the same admin menu settings for all network sites.
110
+ </label><br>
111
+ </p>
112
+
113
+
114
+ <label>
115
+ <input type="radio" name="menu_config_scope" value="site"
116
+ <?php checked('site', $settings['menu_config_scope']); ?>
117
+ <?php disabled(!$isMultisite || !$isSuperAdmin); ?>>
118
+ Per-site &mdash;
119
+ Use different admin menu settings for each site.
120
+ </label>
121
+ </fieldset>
122
+ </td>
123
+ </tr>
124
+
125
+ <tr>
126
+ <th scope="row">Interface</th>
127
+ <td>
128
+ <label>
129
+ <input type="checkbox" name="hide_advanced_settings"
130
+ <?php checked($this->options['hide_advanced_settings']); ?>>
131
+ Hide advanced menu options by default
132
+ </label>
133
+ </td>
134
+ </tr>
135
+
136
+ <tr>
137
+ <th scope="row">Debugging</th>
138
+ <td>
139
+ <label>
140
+ <input type="checkbox" name="security_logging_enabled"
141
+ <?php checked($this->options['security_logging_enabled']); ?>>
142
+ Show menu access checks performed by the plugin on every admin page
143
+ </label>
144
+ <br><span class="description">
145
+ This can help track down configuration problems and figure out why
146
+ your menu permissions don't work the way they should.
147
+
148
+ Note: It's not recommended to use this option on a live site as
149
+ it can reveal information about your menu configuration.
150
+ </span>
151
+ </td>
152
+ </tr>
153
+ </tbody>
154
+ </table>
155
+
156
+ <input type="hidden" name="action" value="save_settings">
157
+ <?php
158
+ wp_nonce_field('save_settings');
159
+ submit_button();
160
+ ?>
161
+ </form>
162
+
163
+ </div>
includes/shadow_plugin_framework.php CHANGED
@@ -2,7 +2,7 @@
2
 
3
  /**
4
  * @author W-Shadow
5
- * @copyright 2008-2011
6
  */
7
 
8
  //Load JSON functions for PHP < 5.2
@@ -141,7 +141,7 @@ class MenuEd_ShadowPluginFramework {
141
  * ShadowPluginFramework::save_options()
142
  * Saves the $options array to the database.
143
  *
144
- * @return void
145
  */
146
  function save_options(){
147
  if ($this->option_name) {
@@ -151,20 +151,21 @@ class MenuEd_ShadowPluginFramework {
151
  }
152
 
153
  if ( $this->sitewide_options ) {
154
- update_site_option($this->option_name, $stored_options);
155
  } else {
156
- update_option($this->option_name, $stored_options);
157
  }
158
  }
 
159
  }
160
 
161
 
162
  /**
163
- * Backwards fompatible json_decode.
164
  *
165
  * @param string $data
166
  * @param bool $assoc Decode objects as associative arrays.
167
- * @return string
168
  */
169
  function json_decode($data, $assoc=false){
170
  if ( function_exists('json_decode') ){
@@ -179,11 +180,12 @@ class MenuEd_ShadowPluginFramework {
179
  return $json->decode($data);
180
  } else {
181
  trigger_error('No JSON parser available', E_USER_ERROR);
 
182
  }
183
  }
184
 
185
  /**
186
- * Backwards fompatible json_encode.
187
  *
188
  * @param mixed $data
189
  * @return string
@@ -200,6 +202,7 @@ class MenuEd_ShadowPluginFramework {
200
  return $json->encode($data);
201
  } else {
202
  trigger_error('No JSON parser available', E_USER_ERROR);
 
203
  }
204
  }
205
 
@@ -214,7 +217,7 @@ class MenuEd_ShadowPluginFramework {
214
  $class = new ReflectionClass(get_class($this));
215
  $methods = $class->getMethods();
216
 
217
- foreach ($methods as $method){
218
  //Check if the method name starts with "hook_"
219
  if (strpos($method->name, 'hook_') === 0){
220
  //Get the hook's tag from the method name
2
 
3
  /**
4
  * @author W-Shadow
5
+ * @copyright 2008-2012
6
  */
7
 
8
  //Load JSON functions for PHP < 5.2
141
  * ShadowPluginFramework::save_options()
142
  * Saves the $options array to the database.
143
  *
144
+ * @return bool
145
  */
146
  function save_options(){
147
  if ($this->option_name) {
151
  }
152
 
153
  if ( $this->sitewide_options ) {
154
+ return update_site_option($this->option_name, $stored_options);
155
  } else {
156
+ return update_option($this->option_name, $stored_options);
157
  }
158
  }
159
+ return false;
160
  }
161
 
162
 
163
  /**
164
+ * Backwards compatible json_decode.
165
  *
166
  * @param string $data
167
  * @param bool $assoc Decode objects as associative arrays.
168
+ * @return mixed
169
  */
170
  function json_decode($data, $assoc=false){
171
  if ( function_exists('json_decode') ){
180
  return $json->decode($data);
181
  } else {
182
  trigger_error('No JSON parser available', E_USER_ERROR);
183
+ return null;
184
  }
185
  }
186
 
187
  /**
188
+ * Backwards compatible json_encode.
189
  *
190
  * @param mixed $data
191
  * @return string
202
  return $json->encode($data);
203
  } else {
204
  trigger_error('No JSON parser available', E_USER_ERROR);
205
+ return '';
206
  }
207
  }
208
 
217
  $class = new ReflectionClass(get_class($this));
218
  $methods = $class->getMethods();
219
 
220
+ foreach ($methods as $method){ /** @var ReflectionMethod $method */
221
  //Check if the method name starts with "hook_"
222
  if (strpos($method->name, 'hook_') === 0){
223
  //Get the hook's tag from the method name
includes/version-conflict-check.php ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ //It's not possible to have two versions of this plugin active at the same time. Abort plugin load
3
+ //and display an error if we detect that another version has already been loaded.
4
+ if ( class_exists('WPMenuEditor') ) {
5
+
6
+ function ws_ame_activation_conflict() {
7
+ if ( !current_user_can('activate_plugins') ) {
8
+ return; //The current user can't do anything about the problem.
9
+ }
10
+ ?>
11
+ <div class="error fade">
12
+ <p>
13
+ <strong>Error: Another version of Admin Menu Editor is already active.</strong><br>
14
+ Please deactivate the older version. It is not possible to run two different versions
15
+ of this plugin at the same time.
16
+ </p>
17
+ </div>
18
+ <?php
19
+ }
20
+
21
+ add_action('admin_notices', 'ws_ame_activation_conflict');
22
+ return true; //Conflict detected.
23
+ }
24
+
25
+ return false; //No conflict.
js/admin-helpers.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
1
+ jQuery(function($) {
2
+ //Menu separators shouldn't be clickable and should have a custom class.
3
+ $('#adminmenu')
4
+ .find('.ws-submenu-separator')
5
+ .closest('a').click(function() {
6
+ return false;
7
+ })
8
+ .closest('li').addClass('ws-submenu-separator-wrap');
9
+ });
js/jquery.form.js CHANGED
@@ -1,372 +1,713 @@
1
  /*!
2
  * jQuery Form Plugin
3
- * version: 2.43 (12-MAR-2010)
4
- * @requires jQuery v1.3.2 or later
5
  *
6
  * Examples and documentation at: http://malsup.com/jquery/form/
 
7
  * Dual licensed under the MIT and GPL licenses:
8
- * http://www.opensource.org/licenses/mit-license.php
9
- * http://www.gnu.org/licenses/gpl.html
10
  */
 
11
  ;(function($) {
 
12
 
13
  /*
14
- Usage Note:
15
- -----------
16
- Do not use both ajaxSubmit and ajaxForm on the same form. These
17
- functions are intended to be exclusive. Use ajaxSubmit if you want
18
- to bind your own submit handler to the form. For example,
19
-
20
- $(document).ready(function() {
21
- $('#myForm').bind('submit', function() {
22
- $(this).ajaxSubmit({
23
- target: '#output'
24
- });
25
- return false; // <-- important!
26
- });
27
- });
28
-
29
- Use ajaxForm when you want the plugin to manage all the event binding
30
- for you. For example,
31
-
32
- $(document).ready(function() {
33
- $('#myForm').ajaxForm({
34
- target: '#output'
35
- });
36
- });
37
-
38
- When using ajaxForm, the ajaxSubmit function will be invoked for you
39
- at the appropriate time.
 
 
 
 
 
 
 
 
40
  */
41
 
 
 
 
 
 
 
 
42
  /**
43
  * ajaxSubmit() provides a mechanism for immediately submitting
44
  * an HTML form using AJAX.
45
  */
46
  $.fn.ajaxSubmit = function(options) {
47
- // fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
48
- if (!this.length) {
49
- log('ajaxSubmit: skipping submit process - no element selected');
50
- return this;
51
- }
52
-
53
- if (typeof options == 'function')
54
- options = { success: options };
55
-
56
- var url = $.trim(this.attr('action'));
57
- if (url) {
58
- // clean url (don't include hash vaue)
59
- url = (url.match(/^([^#]+)/)||[])[1];
60
- }
61
- url = url || window.location.href || '';
62
-
63
- options = $.extend({
64
- url: url,
65
- type: this.attr('method') || 'GET',
66
- iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank'
67
- }, options || {});
68
-
69
- // hook for manipulating the form data before it is extracted;
70
- // convenient for use with rich editors like tinyMCE or FCKEditor
71
- var veto = {};
72
- this.trigger('form-pre-serialize', [this, options, veto]);
73
- if (veto.veto) {
74
- log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
75
- return this;
76
- }
77
-
78
- // provide opportunity to alter form data before it is serialized
79
- if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
80
- log('ajaxSubmit: submit aborted via beforeSerialize callback');
81
- return this;
82
- }
83
-
84
- var a = this.formToArray(options.semantic);
85
- if (options.data) {
86
- options.extraData = options.data;
87
- for (var n in options.data) {
88
- if(options.data[n] instanceof Array) {
89
- for (var k in options.data[n])
90
- a.push( { name: n, value: options.data[n][k] } );
91
- }
92
- else
93
- a.push( { name: n, value: options.data[n] } );
94
- }
95
- }
96
-
97
- // give pre-submit callback an opportunity to abort the submit
98
- if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
99
- log('ajaxSubmit: submit aborted via beforeSubmit callback');
100
- return this;
101
- }
102
-
103
- // fire vetoable 'validate' event
104
- this.trigger('form-submit-validate', [a, this, options, veto]);
105
- if (veto.veto) {
106
- log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
107
- return this;
108
- }
109
-
110
- var q = $.param(a);
111
-
112
- if (options.type.toUpperCase() == 'GET') {
113
- options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
114
- options.data = null; // data is null for 'get'
115
- }
116
- else
117
- options.data = q; // data is the query string for 'post'
118
-
119
- var $form = this, callbacks = [];
120
- if (options.resetForm) callbacks.push(function() { $form.resetForm(); });
121
- if (options.clearForm) callbacks.push(function() { $form.clearForm(); });
122
-
123
- // perform a load on the target only if dataType is not provided
124
- if (!options.dataType && options.target) {
125
- var oldSuccess = options.success || function(){};
126
- callbacks.push(function(data) {
127
- var fn = options.replaceTarget ? 'replaceWith' : 'html';
128
- $(options.target)[fn](data).each(oldSuccess, arguments);
129
- });
130
- }
131
- else if (options.success)
132
- callbacks.push(options.success);
133
-
134
- options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg
135
- for (var i=0, max=callbacks.length; i < max; i++)
136
- callbacks[i].apply(options, [data, status, xhr || $form, $form]);
137
- };
138
-
139
- // are there files to upload?
140
- var files = $('input:file', this).fieldValue();
141
- var found = false;
142
- for (var j=0; j < files.length; j++)
143
- if (files[j])
144
- found = true;
145
-
146
- var multipart = false;
147
- // var mp = 'multipart/form-data';
148
- // multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);
149
-
150
- // options.iframe allows user to force iframe mode
151
- // 06-NOV-09: now defaulting to iframe mode if file input is detected
152
- if ((files.length && options.iframe !== false) || options.iframe || found || multipart) {
153
- // hack to fix Safari hang (thanks to Tim Molendijk for this)
154
- // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
155
- if (options.closeKeepAlive)
156
- $.get(options.closeKeepAlive, fileUpload);
157
- else
158
- fileUpload();
159
- }
160
- else
161
- $.ajax(options);
162
-
163
- // fire 'notify' event
164
- this.trigger('form-submit-notify', [this, options]);
165
- return this;
166
-
167
-
168
- // private function for handling file uploads (hat tip to YAHOO!)
169
- function fileUpload() {
170
- var form = $form[0];
171
-
172
- if ($(':input[name=submit]', form).length) {
173
- alert('Error: Form elements must not be named "submit".');
174
- return;
175
- }
176
-
177
- var opts = $.extend({}, $.ajaxSettings, options);
178
- var s = $.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts);
179
-
180
- var id = 'jqFormIO' + (new Date().getTime());
181
- var $io = $('<iframe id="' + id + '" name="' + id + '" src="'+ opts.iframeSrc +'" onload="(jQuery(this).data(\'form-plugin-onload\'))()" />');
182
- var io = $io[0];
183
-
184
- $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
185
-
186
- var xhr = { // mock object
187
- aborted: 0,
188
- responseText: null,
189
- responseXML: null,
190
- status: 0,
191
- statusText: 'n/a',
192
- getAllResponseHeaders: function() {},
193
- getResponseHeader: function() {},
194
- setRequestHeader: function() {},
195
- abort: function() {
196
- this.aborted = 1;
197
- $io.attr('src', opts.iframeSrc); // abort op in progress
198
- }
199
- };
200
-
201
- var g = opts.global;
202
- // trigger ajax global events so that activity/block indicators work like normal
203
- if (g && ! $.active++) $.event.trigger("ajaxStart");
204
- if (g) $.event.trigger("ajaxSend", [xhr, opts]);
205
-
206
- if (s.beforeSend && s.beforeSend(xhr, s) === false) {
207
- s.global && $.active--;
208
- return;
209
- }
210
- if (xhr.aborted)
211
- return;
212
-
213
- var cbInvoked = false;
214
- var timedOut = 0;
215
-
216
- // add submitting element to data if we know it
217
- var sub = form.clk;
218
- if (sub) {
219
- var n = sub.name;
220
- if (n && !sub.disabled) {
221
- opts.extraData = opts.extraData || {};
222
- opts.extraData[n] = sub.value;
223
- if (sub.type == "image") {
224
- opts.extraData[n+'.x'] = form.clk_x;
225
- opts.extraData[n+'.y'] = form.clk_y;
226
- }
227
- }
228
- }
229
-
230
- // take a breath so that pending repaints get some cpu time before the upload starts
231
- function doSubmit() {
232
- // make sure form attrs are set
233
- var t = $form.attr('target'), a = $form.attr('action');
234
-
235
- // update form attrs in IE friendly way
236
- form.setAttribute('target',id);
237
- if (form.getAttribute('method') != 'POST')
238
- form.setAttribute('method', 'POST');
239
- if (form.getAttribute('action') != opts.url)
240
- form.setAttribute('action', opts.url);
241
-
242
- // ie borks in some cases when setting encoding
243
- if (! opts.skipEncodingOverride) {
244
- $form.attr({
245
- encoding: 'multipart/form-data',
246
- enctype: 'multipart/form-data'
247
- });
248
- }
249
-
250
- // support timout
251
- if (opts.timeout)
252
- setTimeout(function() { timedOut = true; cb(); }, opts.timeout);
253
-
254
- // add "extra" data to form if provided in options
255
- var extraInputs = [];
256
- try {
257
- if (opts.extraData)
258
- for (var n in opts.extraData)
259
- extraInputs.push(
260
- $('<input type="hidden" name="'+n+'" value="'+opts.extraData[n]+'" />')
261
- .appendTo(form)[0]);
262
-
263
- // add iframe to doc and submit the form
264
- $io.appendTo('body');
265
- $io.data('form-plugin-onload', cb);
266
- form.submit();
267
- }
268
- finally {
269
- // reset attrs and remove "extra" input elements
270
- form.setAttribute('action',a);
271
- t ? form.setAttribute('target', t) : $form.removeAttr('target');
272
- $(extraInputs).remove();
273
- }
274
- };
275
-
276
- if (opts.forceSync)
277
- doSubmit();
278
- else
279
- setTimeout(doSubmit, 10); // this lets dom updates render
280
-
281
- var domCheckCount = 100;
282
-
283
- function cb() {
284
- if (cbInvoked)
285
- return;
286
-
287
- var ok = true;
288
- try {
289
- if (timedOut) throw 'timeout';
290
- // extract the server response from the iframe
291
- var data, doc;
292
-
293
- doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document;
294
-
295
- var isXml = opts.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
296
- log('isXml='+isXml);
297
- if (!isXml && (doc.body == null || doc.body.innerHTML == '')) {
298
- if (--domCheckCount) {
299
- // in some browsers (Opera) the iframe DOM is not always traversable when
300
- // the onload callback fires, so we loop a bit to accommodate
301
- log('requeing onLoad callback, DOM not available');
302
- setTimeout(cb, 250);
303
- return;
304
- }
305
- log('Could not access iframe DOM after 100 tries.');
306
- return;
307
- }
308
-
309
- log('response detected');
310
- cbInvoked = true;
311
- xhr.responseText = doc.body ? doc.body.innerHTML : null;
312
- xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
313
- xhr.getResponseHeader = function(header){
314
- var headers = {'content-type': opts.dataType};
315
- return headers[header];
316
- };
317
-
318
- if (opts.dataType == 'json' || opts.dataType == 'script') {
319
- // see if user embedded response in textarea
320
- var ta = doc.getElementsByTagName('textarea')[0];
321
- if (ta)
322
- xhr.responseText = ta.value;
323
- else {
324
- // account for browsers injecting pre around json response
325
- var pre = doc.getElementsByTagName('pre')[0];
326
- if (pre)
327
- xhr.responseText = pre.innerHTML;
328
- }
329
- }
330
- else if (opts.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) {
331
- xhr.responseXML = toXml(xhr.responseText);
332
- }
333
- data = $.httpData(xhr, opts.dataType);
334
- }
335
- catch(e){
336
- log('error caught:',e);
337
- ok = false;
338
- xhr.error = e;
339
- $.handleError(opts, xhr, 'error', e);
340
- }
341
-
342
- // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
343
- if (ok) {
344
- opts.success(data, 'success');
345
- if (g) $.event.trigger("ajaxSuccess", [xhr, opts]);
346
- }
347
- if (g) $.event.trigger("ajaxComplete", [xhr, opts]);
348
- if (g && ! --$.active) $.event.trigger("ajaxStop");
349
- if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error');
350
-
351
- // clean up
352
- setTimeout(function() {
353
- $io.removeData('form-plugin-onload');
354
- $io.remove();
355
- xhr.responseXML = null;
356
- }, 100);
357
- };
358
-
359
- function toXml(s, doc) {
360
- if (window.ActiveXObject) {
361
- doc = new ActiveXObject('Microsoft.XMLDOM');
362
- doc.async = 'false';
363
- doc.loadXML(s);
364
- }
365
- else
366
- doc = (new DOMParser()).parseFromString(s, 'text/xml');
367
- return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;
368
- };
369
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  };
371
 
372
  /**
@@ -375,9 +716,9 @@ $.fn.ajaxSubmit = function(options) {
375
  * The advantages of using this method instead of ajaxSubmit() are:
376
  *
377
  * 1: This method will include coordinates for <input type="image" /> elements (if the element
378
- * is used to submit the form).
379
  * 2. This method will include the submit element's name/value data (for the element that was
380
- * used to submit the form).
381
  * 3. This method binds the submit() method to the form for you.
382
  *
383
  * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely
@@ -385,42 +726,83 @@ $.fn.ajaxSubmit = function(options) {
385
  * the form itself.
386
  */
387
  $.fn.ajaxForm = function(options) {
388
- return this.ajaxFormUnbind().bind('submit.form-plugin', function(e) {
389
- e.preventDefault();
390
- $(this).ajaxSubmit(options);
391
- }).bind('click.form-plugin', function(e) {
392
- var target = e.target;
393
- var $el = $(target);
394
- if (!($el.is(":submit,input:image"))) {
395
- // is this a child element of the submit el? (ex: a span within a button)
396
- var t = $el.closest(':submit');
397
- if (t.length == 0)
398
- return;
399
- target = t[0];
400
- }
401
- var form = this;
402
- form.clk = target;
403
- if (target.type == 'image') {
404
- if (e.offsetX != undefined) {
405
- form.clk_x = e.offsetX;
406
- form.clk_y = e.offsetY;
407
- } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
408
- var offset = $el.offset();
409
- form.clk_x = e.pageX - offset.left;
410
- form.clk_y = e.pageY - offset.top;
411
- } else {
412
- form.clk_x = e.pageX - target.offsetLeft;
413
- form.clk_y = e.pageY - target.offsetTop;
414
- }
415
- }
416
- // clear form vars
417
- setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100);
418
- });
419
  };
420
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  // ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
422
  $.fn.ajaxFormUnbind = function() {
423
- return this.unbind('submit.form-plugin click.form-plugin');
424
  };
425
 
426
  /**
@@ -434,45 +816,74 @@ $.fn.ajaxFormUnbind = function() {
434
  * It is this array that is passed to pre-submit callback functions provided to the
435
  * ajaxSubmit() and ajaxForm() methods.
436
  */
437
- $.fn.formToArray = function(semantic) {
438
- var a = [];
439
- if (this.length == 0) return a;
440
-
441
- var form = this[0];
442
- var els = semantic ? form.getElementsByTagName('*') : form.elements;
443
- if (!els) return a;
444
- for(var i=0, max=els.length; i < max; i++) {
445
- var el = els[i];
446
- var n = el.name;
447
- if (!n) continue;
448
-
449
- if (semantic && form.clk && el.type == "image") {
450
- // handle image inputs on the fly when semantic == true
451
- if(!el.disabled && form.clk == el) {
452
- a.push({name: n, value: $(el).val()});
453
- a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
454
- }
455
- continue;
456
- }
457
-
458
- var v = $.fieldValue(el, true);
459
- if (v && v.constructor == Array) {
460
- for(var j=0, jmax=v.length; j < jmax; j++)
461
- a.push({name: n, value: v[j]});
462
- }
463
- else if (v !== null && typeof v != 'undefined')
464
- a.push({name: n, value: v});
465
- }
466
-
467
- if (!semantic && form.clk) {
468
- // input type=='image' are not found in elements array! handle it here
469
- var $input = $(form.clk), input = $input[0], n = input.name;
470
- if (n && !input.disabled && input.type == 'image') {
471
- a.push({name: n, value: $input.val()});
472
- a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
473
- }
474
- }
475
- return a;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
  };
477
 
478
  /**
@@ -480,8 +891,8 @@ $.fn.formToArray = function(semantic) {
480
  * in the format: name1=value1&amp;name2=value2
481
  */
482
  $.fn.formSerialize = function(semantic) {
483
- //hand off to jQuery.param for proper encoding
484
- return $.param(this.formToArray(semantic));
485
  };
486
 
487
  /**
@@ -489,47 +900,51 @@ $.fn.formSerialize = function(semantic) {
489
  * This method will return a string in the format: name1=value1&amp;name2=value2
490
  */
491
  $.fn.fieldSerialize = function(successful) {
492
- var a = [];
493
- this.each(function() {
494
- var n = this.name;
495
- if (!n) return;
496
- var v = $.fieldValue(this, successful);
497
- if (v && v.constructor == Array) {
498
- for (var i=0,max=v.length; i < max; i++)
499
- a.push({name: n, value: v[i]});
500
- }
501
- else if (v !== null && typeof v != 'undefined')
502
- a.push({name: this.name, value: v});
503
- });
504
- //hand off to jQuery.param for proper encoding
505
- return $.param(a);
 
 
 
 
506
  };
507
 
508
  /**
509
  * Returns the value(s) of the element in the matched set. For example, consider the following form:
510
  *
511
  * <form><fieldset>
512
- * <input name="A" type="text" />
513
- * <input name="A" type="text" />
514
- * <input name="B" type="checkbox" value="B1" />
515
- * <input name="B" type="checkbox" value="B2"/>
516
- * <input name="C" type="radio" value="C1" />
517
- * <input name="C" type="radio" value="C2" />
518
  * </fieldset></form>
519
  *
520
- * var v = $(':text').fieldValue();
521
  * // if no values are entered into the text inputs
522
  * v == ['','']
523
  * // if values entered into the text inputs are 'foo' and 'bar'
524
  * v == ['foo','bar']
525
  *
526
- * var v = $(':checkbox').fieldValue();
527
  * // if neither checkbox is checked
528
  * v === undefined
529
  * // if both checkboxes are checked
530
  * v == ['B1', 'B2']
531
  *
532
- * var v = $(':radio').fieldValue();
533
  * // if neither radio is checked
534
  * v === undefined
535
  * // if first radio is checked
@@ -541,51 +956,63 @@ $.fn.fieldSerialize = function(successful) {
541
  * for each element is returned.
542
  *
543
  * Note: This method *always* returns an array. If no valid value can be determined the
544
- * array will be empty, otherwise it will contain one or more values.
545
  */
546
  $.fn.fieldValue = function(successful) {
547
- for (var val=[], i=0, max=this.length; i < max; i++) {
548
- var el = this[i];
549
- var v = $.fieldValue(el, successful);
550
- if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length))
551
- continue;
552
- v.constructor == Array ? $.merge(val, v) : val.push(v);
553
- }
554
- return val;
 
 
 
 
555
  };
556
 
557
  /**
558
  * Returns the value of the field element.
559
  */
560
  $.fieldValue = function(el, successful) {
561
- var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
562
- if (typeof successful == 'undefined') successful = true;
563
-
564
- if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
565
- (t == 'checkbox' || t == 'radio') && !el.checked ||
566
- (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
567
- tag == 'select' && el.selectedIndex == -1))
568
- return null;
569
-
570
- if (tag == 'select') {
571
- var index = el.selectedIndex;
572
- if (index < 0) return null;
573
- var a = [], ops = el.options;
574
- var one = (t == 'select-one');
575
- var max = (one ? index+1 : ops.length);
576
- for(var i=(one ? index : 0); i < max; i++) {
577
- var op = ops[i];
578
- if (op.selected) {
579
- var v = op.value;
580
- if (!v) // extra pain for IE...
581
- v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
582
- if (one) return v;
583
- a.push(v);
584
- }
585
- }
586
- return a;
587
- }
588
- return el.value;
 
 
 
 
 
 
 
 
589
  };
590
 
591
  /**
@@ -596,47 +1023,70 @@ $.fieldValue = function(el, successful) {
596
  * - inputs of type submit, button, reset, and hidden will *not* be effected
597
  * - button elements will *not* be effected
598
  */
599
- $.fn.clearForm = function() {
600
- return this.each(function() {
601
- $('input,select,textarea', this).clearFields();
602
- });
603
  };
604
 
605
  /**
606
  * Clears the selected form elements.
607
  */
608
- $.fn.clearFields = $.fn.clearInputs = function() {
609
- return this.each(function() {
610
- var t = this.type, tag = this.tagName.toLowerCase();
611
- if (t == 'text' || t == 'password' || tag == 'textarea')
612
- this.value = '';
613
- else if (t == 'checkbox' || t == 'radio')
614
- this.checked = false;
615
- else if (tag == 'select')
616
- this.selectedIndex = -1;
617
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
618
  };
619
 
620
  /**
621
  * Resets the form data. Causes all form elements to be reset to their original value.
622
  */
623
  $.fn.resetForm = function() {
624
- return this.each(function() {
625
- // guard against an input with the name of 'reset'
626
- // note that IE reports the reset function as an 'object'
627
- if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType))
628
- this.reset();
629
- });
 
630
  };
631
 
632
  /**
633
  * Enables or disables any matching elements.
634
  */
635
  $.fn.enable = function(b) {
636
- if (b == undefined) b = true;
637
- return this.each(function() {
638
- this.disabled = !b;
639
- });
 
 
640
  };
641
 
642
  /**
@@ -644,32 +1094,39 @@ $.fn.enable = function(b) {
644
  * selects/deselects and matching option elements.
645
  */
646
  $.fn.selected = function(select) {
647
- if (select == undefined) select = true;
648
- return this.each(function() {
649
- var t = this.type;
650
- if (t == 'checkbox' || t == 'radio')
651
- this.checked = select;
652
- else if (this.tagName.toLowerCase() == 'option') {
653
- var $sel = $(this).parent('select');
654
- if (select && $sel[0] && $sel[0].type == 'select-one') {
655
- // deselect all other options
656
- $sel.find('option').selected(false);
657
- }
658
- this.selected = select;
659
- }
660
- });
 
 
 
661
  };
662
 
 
 
 
663
  // helper fn for console logging
664
- // set $.fn.ajaxSubmit.debug to true to enable debug logging
665
  function log() {
666
- if ($.fn.ajaxSubmit.debug) {
667
- var msg = '[jquery.form] ' + Array.prototype.join.call(arguments,'');
668
- if (window.console && window.console.log)
669
- window.console.log(msg);
670
- else if (window.opera && window.opera.postError)
671
- window.opera.postError(msg);
672
- }
673
- };
 
 
674
 
675
  })(jQuery);
1
  /*!
2
  * jQuery Form Plugin
3
+ * version: 3.24 (26-DEC-2012)
4
+ * @requires jQuery v1.5 or later
5
  *
6
  * Examples and documentation at: http://malsup.com/jquery/form/
7
+ * Project repository: https://github.com/malsup/form
8
  * Dual licensed under the MIT and GPL licenses:
9
+ * http://malsup.github.com/mit-license.txt
10
+ * http://malsup.github.com/gpl-license-v2.txt
11
  */
12
+ /*global ActiveXObject alert */
13
  ;(function($) {
14
+ "use strict";
15
 
16
  /*
17
+ Usage Note:
18
+ -----------
19
+ Do not use both ajaxSubmit and ajaxForm on the same form. These
20
+ functions are mutually exclusive. Use ajaxSubmit if you want
21
+ to bind your own submit handler to the form. For example,
22
+
23
+ $(document).ready(function() {
24
+ $('#myForm').on('submit', function(e) {
25
+ e.preventDefault(); // <-- important
26
+ $(this).ajaxSubmit({
27
+ target: '#output'
28
+ });
29
+ });
30
+ });
31
+
32
+ Use ajaxForm when you want the plugin to manage all the event binding
33
+ for you. For example,
34
+
35
+ $(document).ready(function() {
36
+ $('#myForm').ajaxForm({
37
+ target: '#output'
38
+ });
39
+ });
40
+
41
+ You can also use ajaxForm with delegation (requires jQuery v1.7+), so the
42
+ form does not have to exist when you invoke ajaxForm:
43
+
44
+ $('#myForm').ajaxForm({
45
+ delegation: true,
46
+ target: '#output'
47
+ });
48
+
49
+ When using ajaxForm, the ajaxSubmit function will be invoked for you
50
+ at the appropriate time.
51
  */
52
 
53
+ /**
54
+ * Feature detection
55
+ */
56
+ var feature = {};
57
+ feature.fileapi = $("<input type='file'/>").get(0).files !== undefined;
58
+ feature.formdata = window.FormData !== undefined;
59
+
60
  /**
61
  * ajaxSubmit() provides a mechanism for immediately submitting
62
  * an HTML form using AJAX.
63
  */
64
  $.fn.ajaxSubmit = function(options) {
65
+ /*jshint scripturl:true */
66
+
67
+ // fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
68
+ if (!this.length) {
69
+ log('ajaxSubmit: skipping submit process - no element selected');
70
+ return this;
71
+ }
72
+
73
+ var method, action, url, $form = this;
74
+
75
+ if (typeof options == 'function') {
76
+ options = { success: options };
77
+ }
78
+
79
+ method = this.attr('method');
80
+ action = this.attr('action');
81
+ url = (typeof action === 'string') ? $.trim(action) : '';
82
+ url = url || window.location.href || '';
83
+ if (url) {
84
+ // clean url (don't include hash vaue)
85
+ url = (url.match(/^([^#]+)/)||[])[1];
86
+ }
87
+
88
+ options = $.extend(true, {
89
+ url: url,
90
+ success: $.ajaxSettings.success,
91
+ type: method || 'GET',
92
+ iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank'
93
+ }, options);
94
+
95
+ // hook for manipulating the form data before it is extracted;
96
+ // convenient for use with rich editors like tinyMCE or FCKEditor
97
+ var veto = {};
98
+ this.trigger('form-pre-serialize', [this, options, veto]);
99
+ if (veto.veto) {
100
+ log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
101
+ return this;
102
+ }
103
+
104
+ // provide opportunity to alter form data before it is serialized
105
+ if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
106
+ log('ajaxSubmit: submit aborted via beforeSerialize callback');
107
+ return this;
108
+ }
109
+
110
+ var traditional = options.traditional;
111
+ if ( traditional === undefined ) {
112
+ traditional = $.ajaxSettings.traditional;
113
+ }
114
+
115
+ var elements = [];
116
+ var qx, a = this.formToArray(options.semantic, elements);
117
+ if (options.data) {
118
+ options.extraData = options.data;
119
+ qx = $.param(options.data, traditional);
120
+ }
121
+
122
+ // give pre-submit callback an opportunity to abort the submit
123
+ if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
124
+ log('ajaxSubmit: submit aborted via beforeSubmit callback');
125
+ return this;
126
+ }
127
+
128
+ // fire vetoable 'validate' event
129
+ this.trigger('form-submit-validate', [a, this, options, veto]);
130
+ if (veto.veto) {
131
+ log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
132
+ return this;
133
+ }
134
+
135
+ var q = $.param(a, traditional);
136
+ if (qx) {
137
+ q = ( q ? (q + '&' + qx) : qx );
138
+ }
139
+ if (options.type.toUpperCase() == 'GET') {
140
+ options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
141
+ options.data = null; // data is null for 'get'
142
+ }
143
+ else {
144
+ options.data = q; // data is the query string for 'post'
145
+ }
146
+
147
+ var callbacks = [];
148
+ if (options.resetForm) {
149
+ callbacks.push(function() { $form.resetForm(); });
150
+ }
151
+ if (options.clearForm) {
152
+ callbacks.push(function() { $form.clearForm(options.includeHidden); });
153
+ }
154
+
155
+ // perform a load on the target only if dataType is not provided
156
+ if (!options.dataType && options.target) {
157
+ var oldSuccess = options.success || function(){};
158
+ callbacks.push(function(data) {
159
+ var fn = options.replaceTarget ? 'replaceWith' : 'html';
160
+ $(options.target)[fn](data).each(oldSuccess, arguments);
161
+ });
162
+ }
163
+ else if (options.success) {
164
+ callbacks.push(options.success);
165
+ }
166
+
167
+ options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg
168
+ var context = options.context || this ; // jQuery 1.4+ supports scope context
169
+ for (var i=0, max=callbacks.length; i < max; i++) {
170
+ callbacks[i].apply(context, [data, status, xhr || $form, $form]);
171
+ }
172
+ };
173
+
174
+ // are there files to upload?
175
+
176
+ // [value] (issue #113), also see comment:
177
+ // https://github.com/malsup/form/commit/588306aedba1de01388032d5f42a60159eea9228#commitcomment-2180219
178
+ var fileInputs = $('input[type=file]:enabled[value!=""]', this);
179
+
180
+ var hasFileInputs = fileInputs.length > 0;
181
+ var mp = 'multipart/form-data';
182
+ var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);
183
+
184
+ var fileAPI = feature.fileapi && feature.formdata;
185
+ log("fileAPI :" + fileAPI);
186
+ var shouldUseFrame = (hasFileInputs || multipart) && !fileAPI;
187
+
188
+ var jqxhr;
189
+
190
+ // options.iframe allows user to force iframe mode
191
+ // 06-NOV-09: now defaulting to iframe mode if file input is detected
192
+ if (options.iframe !== false && (options.iframe || shouldUseFrame)) {
193
+ // hack to fix Safari hang (thanks to Tim Molendijk for this)
194
+ // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
195
+ if (options.closeKeepAlive) {
196
+ $.get(options.closeKeepAlive, function() {
197
+ jqxhr = fileUploadIframe(a);
198
+ });
199
+ }
200
+ else {
201
+ jqxhr = fileUploadIframe(a);
202
+ }
203
+ }
204
+ else if ((hasFileInputs || multipart) && fileAPI) {
205
+ jqxhr = fileUploadXhr(a);
206
+ }
207
+ else {
208
+ jqxhr = $.ajax(options);
209
+ }
210
+
211
+ $form.removeData('jqxhr').data('jqxhr', jqxhr);
212
+
213
+ // clear element array
214
+ for (var k=0; k < elements.length; k++)
215
+ elements[k] = null;
216
+
217
+ // fire 'notify' event
218
+ this.trigger('form-submit-notify', [this, options]);
219
+ return this;
220
+
221
+ // utility fn for deep serialization
222
+ function deepSerialize(extraData){
223
+ var serialized = $.param(extraData).split('&');
224
+ var len = serialized.length;
225
+ var result = {};
226
+ var i, part;
227
+ for (i=0; i < len; i++) {
228
+ // #252; undo param space replacement
229
+ serialized[i] = serialized[i].replace(/\+/g,' ');
230
+ part = serialized[i].split('=');
231
+ result[decodeURIComponent(part[0])] = decodeURIComponent(part[1]);
232
+ }
233
+ return result;
234
+ }
235
+
236
+ // XMLHttpRequest Level 2 file uploads (big hat tip to francois2metz)
237
+ function fileUploadXhr(a) {
238
+ var formdata = new FormData();
239
+
240
+ for (var i=0; i < a.length; i++) {
241
+ formdata.append(a[i].name, a[i].value);
242
+ }
243
+
244
+ if (options.extraData) {
245
+ var serializedData = deepSerialize(options.extraData);
246
+ for (var p in serializedData)
247
+ if (serializedData.hasOwnProperty(p))
248
+ formdata.append(p, serializedData[p]);
249
+ }
250
+
251
+ options.data = null;
252
+
253
+ var s = $.extend(true, {}, $.ajaxSettings, options, {
254
+ contentType: false,
255
+ processData: false,
256
+ cache: false,
257
+ type: method || 'POST'
258
+ });
259
+
260
+ if (options.uploadProgress) {
261
+ // workaround because jqXHR does not expose upload property
262
+ s.xhr = function() {
263
+ var xhr = jQuery.ajaxSettings.xhr();
264
+ if (xhr.upload) {
265
+ xhr.upload.onprogress = function(event) {
266
+ var percent = 0;
267
+ var position = event.loaded || event.position; /*event.position is deprecated*/
268
+ var total = event.total;
269
+ if (event.lengthComputable) {
270
+ percent = Math.ceil(position / total * 100);
271
+ }
272
+ options.uploadProgress(event, position, total, percent);
273
+ };
274
+ }
275
+ return xhr;
276
+ };
277
+ }
278
+
279
+ s.data = null;
280
+ var beforeSend = s.beforeSend;
281
+ s.beforeSend = function(xhr, o) {
282
+ o.data = formdata;
283
+ if(beforeSend)
284
+ beforeSend.call(this, xhr, o);
285
+ };
286
+ return $.ajax(s);
287
+ }
288
+
289
+ // private function for handling file uploads (hat tip to YAHOO!)
290
+ function fileUploadIframe(a) {
291
+ var form = $form[0], el, i, s, g, id, $io, io, xhr, sub, n, timedOut, timeoutHandle;
292
+ var useProp = !!$.fn.prop;
293
+ var deferred = $.Deferred();
294
+
295
+ if ($('[name=submit],[id=submit]', form).length) {
296
+ // if there is an input with a name or id of 'submit' then we won't be
297
+ // able to invoke the submit fn on the form (at least not x-browser)
298
+ alert('Error: Form elements must not have name or id of "submit".');
299
+ deferred.reject();
300
+ return deferred;
301
+ }
302
+
303
+ if (a) {
304
+ // ensure that every serialized input is still enabled
305
+ for (i=0; i < elements.length; i++) {
306
+ el = $(elements[i]);
307
+ if ( useProp )
308
+ el.prop('disabled', false);
309
+ else
310
+ el.removeAttr('disabled');
311
+ }
312
+ }
313
+
314
+ s = $.extend(true, {}, $.ajaxSettings, options);
315
+ s.context = s.context || s;
316
+ id = 'jqFormIO' + (new Date().getTime());
317
+ if (s.iframeTarget) {
318
+ $io = $(s.iframeTarget);
319
+ n = $io.attr('name');
320
+ if (!n)
321
+ $io.attr('name', id);
322
+ else
323
+ id = n;
324
+ }
325
+ else {
326
+ $io = $('<iframe name="' + id + '" src="'+ s.iframeSrc +'" />');
327
+ $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
328
+ }
329
+ io = $io[0];
330
+
331
+
332
+ xhr = { // mock object
333
+ aborted: 0,
334
+ responseText: null,
335
+ responseXML: null,
336
+ status: 0,
337
+ statusText: 'n/a',
338
+ getAllResponseHeaders: function() {},
339
+ getResponseHeader: function() {},
340
+ setRequestHeader: function() {},
341
+ abort: function(status) {
342
+ var e = (status === 'timeout' ? 'timeout' : 'aborted');
343
+ log('aborting upload... ' + e);
344
+ this.aborted = 1;
345
+
346
+ try { // #214, #257
347
+ if (io.contentWindow.document.execCommand) {
348
+ io.contentWindow.document.execCommand('Stop');
349
+ }
350
+ }
351
+ catch(ignore) {}
352
+
353
+ $io.attr('src', s.iframeSrc); // abort op in progress
354
+ xhr.error = e;
355
+ if (s.error)
356
+ s.error.call(s.context, xhr, e, status);
357
+ if (g)
358
+ $.event.trigger("ajaxError", [xhr, s, e]);
359
+ if (s.complete)
360
+ s.complete.call(s.context, xhr, e);
361
+ }
362
+ };
363
+
364
+ g = s.global;
365
+ // trigger ajax global events so that activity/block indicators work like normal
366
+ if (g && 0 === $.active++) {
367
+ $.event.trigger("ajaxStart");
368
+ }
369
+ if (g) {
370
+ $.event.trigger("ajaxSend", [xhr, s]);
371
+ }
372
+
373
+ if (s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false) {
374
+ if (s.global) {
375
+ $.active--;
376
+ }
377
+ deferred.reject();
378
+ return deferred;
379
+ }
380
+ if (xhr.aborted) {
381
+ deferred.reject();
382
+ return deferred;
383
+ }
384
+
385
+ // add submitting element to data if we know it
386
+ sub = form.clk;
387
+ if (sub) {
388
+ n = sub.name;
389
+ if (n && !sub.disabled) {
390
+ s.extraData = s.extraData || {};
391
+ s.extraData[n] = sub.value;
392
+ if (sub.type == "image") {
393
+ s.extraData[n+'.x'] = form.clk_x;
394
+ s.extraData[n+'.y'] = form.clk_y;
395
+ }
396
+ }
397
+ }
398
+
399
+ var CLIENT_TIMEOUT_ABORT = 1;
400
+ var SERVER_ABORT = 2;
401
+
402
+ function getDoc(frame) {
403
+ var doc = frame.contentWindow ? frame.contentWindow.document : frame.contentDocument ? frame.contentDocument : frame.document;
404
+ return doc;
405
+ }
406
+
407
+ // Rails CSRF hack (thanks to Yvan Barthelemy)
408
+ var csrf_token = $('meta[name=csrf-token]').attr('content');
409
+ var csrf_param = $('meta[name=csrf-param]').attr('content');
410
+ if (csrf_param && csrf_token) {
411
+ s.extraData = s.extraData || {};
412
+ s.extraData[csrf_param] = csrf_token;
413
+ }
414
+
415
+ // take a breath so that pending repaints get some cpu time before the upload starts
416
+ function doSubmit() {
417
+ // make sure form attrs are set
418
+ var t = $form.attr('target'), a = $form.attr('action');
419
+
420
+ // update form attrs in IE friendly way
421
+ form.setAttribute('target',id);
422
+ if (!method) {
423
+ form.setAttribute('method', 'POST');
424
+ }
425
+ if (a != s.url) {
426
+ form.setAttribute('action', s.url);
427
+ }
428
+
429
+ // ie borks in some cases when setting encoding
430
+ if (! s.skipEncodingOverride && (!method || /post/i.test(method))) {
431
+ $form.attr({
432
+ encoding: 'multipart/form-data',
433
+ enctype: 'multipart/form-data'
434
+ });
435
+ }
436
+
437
+ // support timout
438
+ if (s.timeout) {
439
+ timeoutHandle = setTimeout(function() { timedOut = true; cb(CLIENT_TIMEOUT_ABORT); }, s.timeout);
440
+ }
441
+
442
+ // look for server aborts
443
+ function checkState() {
444
+ try {
445
+ var state = getDoc(io).readyState;
446
+ log('state = ' + state);
447
+ if (state && state.toLowerCase() == 'uninitialized')
448
+ setTimeout(checkState,50);
449
+ }
450
+ catch(e) {
451
+ log('Server abort: ' , e, ' (', e.name, ')');
452
+ cb(SERVER_ABORT);
453
+ if (timeoutHandle)
454
+ clearTimeout(timeoutHandle);
455
+ timeoutHandle = undefined;
456
+ }
457
+ }
458
+
459
+ // add "extra" data to form if provided in options
460
+ var extraInputs = [];
461
+ try {
462
+ if (s.extraData) {
463
+ for (var n in s.extraData) {
464
+ if (s.extraData.hasOwnProperty(n)) {
465
+ // if using the $.param format that allows for multiple values with the same name
466
+ if($.isPlainObject(s.extraData[n]) && s.extraData[n].hasOwnProperty('name') && s.extraData[n].hasOwnProperty('value')) {
467
+ extraInputs.push(
468
+ $('<input type="hidden" name="'+s.extraData[n].name+'">').val(s.extraData[n].value)
469
+ .appendTo(form)[0]);
470
+ } else {
471
+ extraInputs.push(
472
+ $('<input type="hidden" name="'+n+'">').val(s.extraData[n])
473
+ .appendTo(form)[0]);
474
+ }
475
+ }
476
+ }
477
+ }
478
+
479
+ if (!s.iframeTarget) {
480
+ // add iframe to doc and submit the form
481
+ $io.appendTo('body');
482
+ if (io.attachEvent)
483
+ io.attachEvent('onload', cb);
484
+ else
485
+ io.addEventListener('load', cb, false);
486
+ }
487
+ setTimeout(checkState,15);
488
+ form.submit();
489
+ }
490
+ finally {
491
+ // reset attrs and remove "extra" input elements
492
+ form.setAttribute('action',a);
493
+ if(t) {
494
+ form.setAttribute('target', t);
495
+ } else {
496
+ $form.removeAttr('target');
497
+ }
498
+ $(extraInputs).remove();
499
+ }
500
+ }
501
+
502
+ if (s.forceSync) {
503
+ doSubmit();
504
+ }
505
+ else {
506
+ setTimeout(doSubmit, 10); // this lets dom updates render
507
+ }
508
+
509
+ var data, doc, domCheckCount = 50, callbackProcessed;
510
+
511
+ function cb(e) {
512
+ if (xhr.aborted || callbackProcessed) {
513
+ return;
514
+ }
515
+ try {
516
+ doc = getDoc(io);
517
+ }
518
+ catch(ex) {
519
+ log('cannot access response document: ', ex);
520
+ e = SERVER_ABORT;
521
+ }
522
+ if (e === CLIENT_TIMEOUT_ABORT && xhr) {
523
+ xhr.abort('timeout');
524
+ deferred.reject(xhr, 'timeout');
525
+ return;
526
+ }
527
+ else if (e == SERVER_ABORT && xhr) {
528
+ xhr.abort('server abort');
529
+ deferred.reject(xhr, 'error', 'server abort');
530
+ return;
531
+ }
532
+
533
+ if (!doc || doc.location.href == s.iframeSrc) {
534
+ // response not received yet
535
+ if (!timedOut)
536
+ return;
537
+ }
538
+ if (io.detachEvent)
539
+ io.detachEvent('onload', cb);
540
+ else
541
+ io.removeEventListener('load', cb, false);
542
+
543
+ var status = 'success', errMsg;
544
+ try {
545
+ if (timedOut) {
546
+ throw 'timeout';
547
+ }
548
+
549
+ var isXml = s.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
550
+ log('isXml='+isXml);
551
+ if (!isXml && window.opera && (doc.body === null || !doc.body.innerHTML)) {
552
+ if (--domCheckCount) {
553
+ // in some browsers (Opera) the iframe DOM is not always traversable when
554
+ // the onload callback fires, so we loop a bit to accommodate
555
+ log('requeing onLoad callback, DOM not available');
556
+ setTimeout(cb, 250);
557
+ return;
558
+ }
559
+ // let this fall through because server response could be an empty document
560
+ //log('Could not access iframe DOM after mutiple tries.');
561
+ //throw 'DOMException: not available';
562
+ }
563
+
564
+ //log('response detected');
565
+ var docRoot = doc.body ? doc.body : doc.documentElement;
566
+ xhr.responseText = docRoot ? docRoot.innerHTML : null;
567
+ xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
568
+ if (isXml)
569
+ s.dataType = 'xml';
570
+ xhr.getResponseHeader = function(header){
571
+ var headers = {'content-type': s.dataType};
572
+ return headers[header];
573
+ };
574
+ // support for XHR 'status' & 'statusText' emulation :
575
+ if (docRoot) {
576
+ xhr.status = Number( docRoot.getAttribute('status') ) || xhr.status;
577
+ xhr.statusText = docRoot.getAttribute('statusText') || xhr.statusText;
578
+ }
579
+
580
+ var dt = (s.dataType || '').toLowerCase();
581
+ var scr = /(json|script|text)/.test(dt);
582
+ if (scr || s.textarea) {
583
+ // see if user embedded response in textarea
584
+ var ta = doc.getElementsByTagName('textarea')[0];
585
+ if (ta) {
586
+ xhr.responseText = ta.value;
587
+ // support for XHR 'status' & 'statusText' emulation :
588
+ xhr.status = Number( ta.getAttribute('status') ) || xhr.status;
589
+ xhr.statusText = ta.getAttribute('statusText') || xhr.statusText;
590
+ }
591
+ else if (scr) {
592
+ // account for browsers injecting pre around json response
593
+ var pre = doc.getElementsByTagName('pre')[0];
594
+ var b = doc.getElementsByTagName('body')[0];
595
+ if (pre) {
596
+ xhr.responseText = pre.textContent ? pre.textContent : pre.innerText;
597
+ }
598
+ else if (b) {
599
+ xhr.responseText = b.textContent ? b.textContent : b.innerText;
600
+ }
601
+ }
602
+ }
603
+ else if (dt == 'xml' && !xhr.responseXML && xhr.responseText) {
604
+ xhr.responseXML = toXml(xhr.responseText);
605
+ }
606
+
607
+ try {
608
+ data = httpData(xhr, dt, s);
609
+ }
610
+ catch (e) {
611
+ status = 'parsererror';
612
+ xhr.error = errMsg = (e || status);
613
+ }
614
+ }
615
+ catch (e) {
616
+ log('error caught: ',e);
617
+ status = 'error';
618
+ xhr.error = errMsg = (e || status);
619
+ }
620
+
621
+ if (xhr.aborted) {
622
+ log('upload aborted');
623
+ status = null;
624
+ }
625
+
626
+ if (xhr.status) { // we've set xhr.status
627
+ status = (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) ? 'success' : 'error';
628
+ }
629
+
630
+ // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
631
+ if (status === 'success') {
632
+ if (s.success)
633
+ s.success.call(s.context, data, 'success', xhr);
634
+ deferred.resolve(xhr.responseText, 'success', xhr);
635
+ if (g)
636
+ $.event.trigger("ajaxSuccess", [xhr, s]);
637
+ }
638
+ else if (status) {
639
+ if (errMsg === undefined)
640
+ errMsg = xhr.statusText;
641
+ if (s.error)
642
+ s.error.call(s.context, xhr, status, errMsg);
643
+ deferred.reject(xhr, 'error', errMsg);
644
+ if (g)
645
+ $.event.trigger("ajaxError", [xhr, s, errMsg]);
646
+ }
647
+
648
+ if (g)
649
+ $.event.trigger("ajaxComplete", [xhr, s]);
650
+
651
+ if (g && ! --$.active) {
652
+ $.event.trigger("ajaxStop");
653
+ }
654
+
655
+ if (s.complete)
656
+ s.complete.call(s.context, xhr, status);
657
+
658
+ callbackProcessed = true;
659
+ if (s.timeout)
660
+ clearTimeout(timeoutHandle);
661
+
662
+ // clean up
663
+ setTimeout(function() {
664
+ if (!s.iframeTarget)
665
+ $io.remove();
666
+ xhr.responseXML = null;
667
+ }, 100);
668
+ }
669
+
670
+ var toXml = $.parseXML || function(s, doc) { // use parseXML if available (jQuery 1.5+)
671
+ if (window.ActiveXObject) {
672
+ doc = new ActiveXObject('Microsoft.XMLDOM');
673
+ doc.async = 'false';
674
+ doc.loadXML(s);
675
+ }
676
+ else {
677
+ doc = (new DOMParser()).parseFromString(s, 'text/xml');
678
+ }
679
+ return (doc && doc.documentElement && doc.documentElement.nodeName != 'parsererror') ? doc : null;
680
+ };
681
+ var parseJSON = $.parseJSON || function(s) {
682
+ /*jslint evil:true */
683
+ return window['eval']('(' + s + ')');
684
+ };
685
+
686
+ var httpData = function( xhr, type, s ) { // mostly lifted from jq1.4.4
687
+
688
+ var ct = xhr.getResponseHeader('content-type') || '',
689
+ xml = type === 'xml' || !type && ct.indexOf('xml') >= 0,
690
+ data = xml ? xhr.responseXML : xhr.responseText;
691
+
692
+ if (xml && data.documentElement.nodeName === 'parsererror') {
693
+ if ($.error)
694
+ $.error('parsererror');
695
+ }
696
+ if (s && s.dataFilter) {
697
+ data = s.dataFilter(data, type);
698
+ }
699
+ if (typeof data === 'string') {
700
+ if (type === 'json' || !type && ct.indexOf('json') >= 0) {
701
+ data = parseJSON(data);
702
+ } else if (type === "script" || !type && ct.indexOf("javascript") >= 0) {
703
+ $.globalEval(data);
704
+ }
705
+ }
706
+ return data;
707
+ };
708
+
709
+ return deferred;
710
+ }
711
  };
712
 
713
  /**
716
  * The advantages of using this method instead of ajaxSubmit() are:
717
  *
718
  * 1: This method will include coordinates for <input type="image" /> elements (if the element
719
+ * is used to submit the form).
720
  * 2. This method will include the submit element's name/value data (for the element that was
721
+ * used to submit the form).
722
  * 3. This method binds the submit() method to the form for you.
723
  *
724
  * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely
726
  * the form itself.
727
  */
728
  $.fn.ajaxForm = function(options) {
729
+ options = options || {};
730
+ options.delegation = options.delegation && $.isFunction($.fn.on);
731
+
732
+ // in jQuery 1.3+ we can fix mistakes with the ready state
733
+ if (!options.delegation && this.length === 0) {
734
+ var o = { s: this.selector, c: this.context };
735
+ if (!$.isReady && o.s) {
736
+ log('DOM not ready, queuing ajaxForm');
737
+ $(function() {
738
+ $(o.s,o.c).ajaxForm(options);
739
+ });
740
+ return this;
741
+ }
742
+ // is your DOM ready? http://docs.jquery.com/Tutorials:Introducing_$(document).ready()
743
+ log('terminating; zero elements found by selector' + ($.isReady ? '' : ' (DOM not ready)'));
744
+ return this;
745
+ }
746
+
747
+ if ( options.delegation ) {
748
+ $(document)
749
+ .off('submit.form-plugin', this.selector, doAjaxSubmit)
750
+ .off('click.form-plugin', this.selector, captureSubmittingElement)
751
+ .on('submit.form-plugin', this.selector, options, doAjaxSubmit)
752
+ .on('click.form-plugin', this.selector, options, captureSubmittingElement);
753
+ return this;
754
+ }
755
+
756
+ return this.ajaxFormUnbind()
757
+ .bind('submit.form-plugin', options, doAjaxSubmit)
758
+ .bind('click.form-plugin', options, captureSubmittingElement);
 
759
  };
760
 
761
+ // private event handlers
762
+ function doAjaxSubmit(e) {
763
+ /*jshint validthis:true */
764
+ var options = e.data;
765
+ if (!e.isDefaultPrevented()) { // if event has been canceled, don't proceed
766
+ e.preventDefault();
767
+ $(this).ajaxSubmit(options);
768
+ }
769
+ }
770
+
771
+ function captureSubmittingElement(e) {
772
+ /*jshint validthis:true */
773
+ var target = e.target;
774
+ var $el = $(target);
775
+ if (!($el.is("[type=submit],[type=image]"))) {
776
+ // is this a child element of the submit el? (ex: a span within a button)
777
+ var t = $el.closest('[type=submit]');
778
+ if (t.length === 0) {
779
+ return;
780
+ }
781
+ target = t[0];
782
+ }
783
+ var form = this;
784
+ form.clk = target;
785
+ if (target.type == 'image') {
786
+ if (e.offsetX !== undefined) {
787
+ form.clk_x = e.offsetX;
788
+ form.clk_y = e.offsetY;
789
+ } else if (typeof $.fn.offset == 'function') {
790
+ var offset = $el.offset();
791
+ form.clk_x = e.pageX - offset.left;
792
+ form.clk_y = e.pageY - offset.top;
793
+ } else {
794
+ form.clk_x = e.pageX - target.offsetLeft;
795
+ form.clk_y = e.pageY - target.offsetTop;
796
+ }
797
+ }
798
+ // clear form vars
799
+ setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100);
800
+ }
801
+
802
+
803
  // ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
804
  $.fn.ajaxFormUnbind = function() {
805
+ return this.unbind('submit.form-plugin click.form-plugin');
806
  };
807
 
808
  /**
816
  * It is this array that is passed to pre-submit callback functions provided to the
817
  * ajaxSubmit() and ajaxForm() methods.
818
  */
819
+ $.fn.formToArray = function(semantic, elements) {
820
+ var a = [];
821
+ if (this.length === 0) {
822
+ return a;
823
+ }
824
+
825
+ var form = this[0];
826
+ var els = semantic ? form.getElementsByTagName('*') : form.elements;
827
+ if (!els) {
828
+ return a;
829
+ }
830
+
831
+ var i,j,n,v,el,max,jmax;
832
+ for(i=0, max=els.length; i < max; i++) {
833
+ el = els[i];
834
+ n = el.name;
835
+ if (!n) {
836
+ continue;
837
+ }
838
+
839
+ if (semantic && form.clk && el.type == "image") {
840
+ // handle image inputs on the fly when semantic == true
841
+ if(!el.disabled && form.clk == el) {
842
+ a.push({name: n, value: $(el).val(), type: el.type });
843
+ a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
844
+ }
845
+ continue;
846
+ }
847
+
848
+ v = $.fieldValue(el, true);
849
+ if (v && v.constructor == Array) {
850
+ if (elements)
851
+ elements.push(el);
852
+ for(j=0, jmax=v.length; j < jmax; j++) {
853
+ a.push({name: n, value: v[j]});
854
+ }
855
+ }
856
+ else if (feature.fileapi && el.type == 'file' && !el.disabled) {
857
+ if (elements)
858
+ elements.push(el);
859
+ var files = el.files;
860
+ if (files.length) {
861
+ for (j=0; j < files.length; j++) {
862
+ a.push({name: n, value: files[j], type: el.type});
863
+ }
864
+ }
865
+ else {
866
+ // #180
867
+ a.push({ name: n, value: '', type: el.type });
868
+ }
869
+ }
870
+ else if (v !== null && typeof v != 'undefined') {
871
+ if (elements)
872
+ elements.push(el);
873
+ a.push({name: n, value: v, type: el.type, required: el.required});
874
+ }
875
+ }
876
+
877
+ if (!semantic && form.clk) {
878
+ // input type=='image' are not found in elements array! handle it here
879
+ var $input = $(form.clk), input = $input[0];
880
+ n = input.name;
881
+ if (n && !input.disabled && input.type == 'image') {
882
+ a.push({name: n, value: $input.val()});
883
+ a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
884
+ }
885
+ }
886
+ return a;
887
  };
888
 
889
  /**
891
  * in the format: name1=value1&amp;name2=value2
892
  */
893
  $.fn.formSerialize = function(semantic) {
894
+ //hand off to jQuery.param for proper encoding
895
+ return $.param(this.formToArray(semantic));
896
  };
897
 
898
  /**
900
  * This method will return a string in the format: name1=value1&amp;name2=value2
901
  */
902
  $.fn.fieldSerialize = function(successful) {
903
+ var a = [];
904
+ this.each(function() {
905
+ var n = this.name;
906
+ if (!n) {
907
+ return;
908
+ }
909
+ var v = $.fieldValue(this, successful);
910
+ if (v && v.constructor == Array) {
911
+ for (var i=0,max=v.length; i < max; i++) {
912
+ a.push({name: n, value: v[i]});
913
+ }
914
+ }
915
+ else if (v !== null && typeof v != 'undefined') {
916
+ a.push({name: this.name, value: v});
917
+ }
918
+ });
919
+ //hand off to jQuery.param for proper encoding
920
+ return $.param(a);
921
  };
922
 
923
  /**
924
  * Returns the value(s) of the element in the matched set. For example, consider the following form:
925
  *
926
  * <form><fieldset>
927
+ * <input name="A" type="text" />
928
+ * <input name="A" type="text" />
929
+ * <input name="B" type="checkbox" value="B1" />
930
+ * <input name="B" type="checkbox" value="B2"/>
931
+ * <input name="C" type="radio" value="C1" />
932
+ * <input name="C" type="radio" value="C2" />
933
  * </fieldset></form>
934
  *
935
+ * var v = $('input[type=text]').fieldValue();
936
  * // if no values are entered into the text inputs
937
  * v == ['','']
938
  * // if values entered into the text inputs are 'foo' and 'bar'
939
  * v == ['foo','bar']
940
  *
941
+ * var v = $('input[type=checkbox]').fieldValue();
942
  * // if neither checkbox is checked
943
  * v === undefined
944
  * // if both checkboxes are checked
945
  * v == ['B1', 'B2']
946
  *
947
+ * var v = $('input[type=radio]').fieldValue();
948
  * // if neither radio is checked
949
  * v === undefined
950
  * // if first radio is checked
956
  * for each element is returned.
957
  *
958
  * Note: This method *always* returns an array. If no valid value can be determined the
959
+ * array will be empty, otherwise it will contain one or more values.
960
  */
961
  $.fn.fieldValue = function(successful) {
962
+ for (var val=[], i=0, max=this.length; i < max; i++) {
963
+ var el = this[i];
964
+ var v = $.fieldValue(el, successful);
965
+ if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length)) {
966
+ continue;
967
+ }
968
+ if (v.constructor == Array)
969
+ $.merge(val, v);
970
+ else
971
+ val.push(v);
972
+ }
973
+ return val;
974
  };
975
 
976
  /**
977
  * Returns the value of the field element.
978
  */
979
  $.fieldValue = function(el, successful) {
980
+ var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
981
+ if (successful === undefined) {
982
+ successful = true;
983
+ }
984
+
985
+ if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
986
+ (t == 'checkbox' || t == 'radio') && !el.checked ||
987
+ (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
988
+ tag == 'select' && el.selectedIndex == -1)) {
989
+ return null;
990
+ }
991
+
992
+ if (tag == 'select') {
993
+ var index = el.selectedIndex;
994
+ if (index < 0) {
995
+ return null;
996
+ }
997
+ var a = [], ops = el.options;
998
+ var one = (t == 'select-one');
999
+ var max = (one ? index+1 : ops.length);
1000
+ for(var i=(one ? index : 0); i < max; i++) {
1001
+ var op = ops[i];
1002
+ if (op.selected) {
1003
+ var v = op.value;
1004
+ if (!v) { // extra pain for IE...
1005
+ v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
1006
+ }
1007
+ if (one) {
1008
+ return v;
1009
+ }
1010
+ a.push(v);
1011
+ }
1012
+ }
1013
+ return a;
1014
+ }
1015
+ return $(el).val();
1016
  };
1017
 
1018
  /**
1023
  * - inputs of type submit, button, reset, and hidden will *not* be effected
1024
  * - button elements will *not* be effected
1025
  */
1026
+ $.fn.clearForm = function(includeHidden) {
1027
+ return this.each(function() {
1028
+ $('input,select,textarea', this).clearFields(includeHidden);
1029
+ });
1030
  };
1031
 
1032
  /**
1033
  * Clears the selected form elements.
1034
  */
1035
+ $.fn.clearFields = $.fn.clearInputs = function(includeHidden) {
1036
+ var re = /^(?:color|date|datetime|email|month|number|password|range|search|tel|text|time|url|week)$/i; // 'hidden' is not in this list
1037
+ return this.each(function() {
1038
+ var t = this.type, tag = this.tagName.toLowerCase();
1039
+ if (re.test(t) || tag == 'textarea') {
1040
+ this.value = '';
1041
+ }
1042
+ else if (t == 'checkbox' || t == 'radio') {
1043
+ this.checked = false;
1044
+ }
1045
+ else if (tag == 'select') {
1046
+ this.selectedIndex = -1;
1047
+ }
1048
+ else if (t == "file") {
1049
+ if ($.browser.msie) {
1050
+ $(this).replaceWith($(this).clone());
1051
+ } else {
1052
+ $(this).val('');
1053
+ }
1054
+ }
1055
+ else if (includeHidden) {
1056
+ // includeHidden can be the value true, or it can be a selector string
1057
+ // indicating a special test; for example:
1058
+ // $('#myForm').clearForm('.special:hidden')
1059
+ // the above would clean hidden inputs that have the class of 'special'
1060
+ if ( (includeHidden === true && /hidden/.test(t)) ||
1061
+ (typeof includeHidden == 'string' && $(this).is(includeHidden)) )
1062
+ this.value = '';
1063
+ }
1064
+ });
1065
  };
1066
 
1067
  /**
1068
  * Resets the form data. Causes all form elements to be reset to their original value.
1069
  */
1070
  $.fn.resetForm = function() {
1071
+ return this.each(function() {
1072
+ // guard against an input with the name of 'reset'
1073
+ // note that IE reports the reset function as an 'object'
1074
+ if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType)) {
1075
+ this.reset();
1076
+ }
1077
+ });
1078
  };
1079
 
1080
  /**
1081
  * Enables or disables any matching elements.
1082
  */
1083
  $.fn.enable = function(b) {
1084
+ if (b === undefined) {
1085
+ b = true;
1086
+ }
1087
+ return this.each(function() {
1088
+ this.disabled = !b;
1089
+ });
1090
  };
1091
 
1092
  /**
1094
  * selects/deselects and matching option elements.
1095
  */
1096
  $.fn.selected = function(select) {
1097
+ if (select === undefined) {
1098
+ select = true;
1099
+ }
1100
+ return this.each(function() {
1101
+ var t = this.type;
1102
+ if (t == 'checkbox' || t == 'radio') {
1103
+ this.checked = select;
1104
+ }
1105
+ else if (this.tagName.toLowerCase() == 'option') {
1106
+ var $sel = $(this).parent('select');
1107
+ if (select && $sel[0] && $sel[0].type == 'select-one') {
1108
+ // deselect all other options
1109
+ $sel.find('option').selected(false);
1110
+ }
1111
+ this.selected = select;
1112
+ }
1113
+ });
1114
  };
1115
 
1116
+ // expose debug var
1117
+ $.fn.ajaxSubmit.debug = false;
1118
+
1119
  // helper fn for console logging
 
1120
  function log() {
1121
+ if (!$.fn.ajaxSubmit.debug)
1122
+ return;
1123
+ var msg = '[jquery.form] ' + Array.prototype.join.call(arguments,'');
1124
+ if (window.console && window.console.log) {
1125
+ window.console.log(msg);
1126
+ }
1127
+ else if (window.opera && window.opera.postError) {
1128
+ window.opera.postError(msg);
1129
+ }
1130
+ }
1131
 
1132
  })(jQuery);
js/{jquery.json-1.3.js → jquery.json.js} RENAMED
File without changes
js/jquery.qtip.js ADDED
@@ -0,0 +1,3365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*! qTip2 - Pretty powerful tooltips - v2.0.0 - 2012-09-10
2
+ * http://craigsworks.com/projects/qtip2/
3
+ * Copyright (c) 2012 Craig Michael Thompson; Licensed MIT, GPL */
4
+
5
+ /*jslint browser: true, onevar: true, undef: true, nomen: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: true */
6
+ /*global window: false, jQuery: false, console: false, define: false */
7
+
8
+ /* Cache window, document, undefined */
9
+ (function( window, document, undefined ) {
10
+
11
+ // Uses AMD or browser globals to create a jQuery plugin.
12
+ (function( factory ) {
13
+ "use strict";
14
+ if(typeof define === 'function' && define.amd) {
15
+ define(['jquery'], factory);
16
+ }
17
+ else if(jQuery && !jQuery.fn.qtip) {
18
+ factory(jQuery);
19
+ }
20
+ }
21
+ (function($) {
22
+ /* This currently causes issues with Safari 6, so for it's disabled */
23
+ //"use strict"; // (Dis)able ECMAScript "strict" operation for this function. See more: http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/
24
+
25
+ // Munge the primitives - Paul Irish tip
26
+ var TRUE = true,
27
+ FALSE = false,
28
+ NULL = null,
29
+
30
+ // Side names and other stuff
31
+ X = 'x', Y = 'y',
32
+ WIDTH = 'width',
33
+ HEIGHT = 'height',
34
+ TOP = 'top',
35
+ LEFT = 'left',
36
+ BOTTOM = 'bottom',
37
+ RIGHT = 'right',
38
+ CENTER = 'center',
39
+ FLIP = 'flip',
40
+ FLIPINVERT = 'flipinvert',
41
+ SHIFT = 'shift',
42
+
43
+ // Shortcut vars
44
+ QTIP, PLUGINS, MOUSE,
45
+ usedIDs = {},
46
+ uitooltip = 'ui-tooltip',
47
+ widget = 'ui-widget',
48
+ disabled = 'ui-state-disabled',
49
+ selector = 'div.qtip.'+uitooltip,
50
+ defaultClass = uitooltip + '-default',
51
+ focusClass = uitooltip + '-focus',
52
+ hoverClass = uitooltip + '-hover',
53
+ replaceSuffix = '_replacedByqTip',
54
+ oldtitle = 'oldtitle',
55
+ trackingBound,
56
+ redrawContainer;
57
+
58
+ /*
59
+ * redraw() container for width/height calculations
60
+ */
61
+ redrawContainer = $('<div/>', { id: 'qtip-rcontainer' });
62
+ $(function() { redrawContainer.appendTo(document.body); });
63
+
64
+
65
+
66
+ // Option object sanitizer
67
+ function sanitizeOptions(opts)
68
+ {
69
+ var invalid = function(a) { return a === NULL || 'object' !== typeof a; },
70
+ invalidContent = function(c) { return !$.isFunction(c) && ((!c && !c.attr) || c.length < 1 || ('object' === typeof c && !c.jquery)); };
71
+
72
+ if(!opts || 'object' !== typeof opts) { return FALSE; }
73
+
74
+ if(invalid(opts.metadata)) {
75
+ opts.metadata = { type: opts.metadata };
76
+ }
77
+
78
+ if('content' in opts) {
79
+ if(invalid(opts.content) || opts.content.jquery) {
80
+ opts.content = { text: opts.content };
81
+ }
82
+
83
+ if(invalidContent(opts.content.text || FALSE)) {
84
+ opts.content.text = FALSE;
85
+ }
86
+
87
+ if('title' in opts.content) {
88
+ if(invalid(opts.content.title)) {
89
+ opts.content.title = { text: opts.content.title };
90
+ }
91
+
92
+ if(invalidContent(opts.content.title.text || FALSE)) {
93
+ opts.content.title.text = FALSE;
94
+ }
95
+ }
96
+ }
97
+
98
+ if('position' in opts && invalid(opts.position)) {
99
+ opts.position = { my: opts.position, at: opts.position };
100
+ }
101
+
102
+ if('show' in opts && invalid(opts.show)) {
103
+ opts.show = opts.show.jquery ? { target: opts.show } : { event: opts.show };
104
+ }
105
+
106
+ if('hide' in opts && invalid(opts.hide)) {
107
+ opts.hide = opts.hide.jquery ? { target: opts.hide } : { event: opts.hide };
108
+ }
109
+
110
+ if('style' in opts && invalid(opts.style)) {
111
+ opts.style = { classes: opts.style };
112
+ }
113
+
114
+ // Sanitize plugin options
115
+ $.each(PLUGINS, function() {
116
+ if(this.sanitize) { this.sanitize(opts); }
117
+ });
118
+
119
+ return opts;
120
+ }
121
+
122
+ /*
123
+ * Core plugin implementation
124
+ */
125
+ function QTip(target, options, id, attr)
126
+ {
127
+ // Declare this reference
128
+ var self = this,
129
+ docBody = document.body,
130
+ tooltipID = uitooltip + '-' + id,
131
+ isPositioning = 0,
132
+ isDrawing = 0,
133
+ tooltip = $(),
134
+ namespace = '.qtip-' + id,
135
+ elements, cache;
136
+
137
+ // Setup class attributes
138
+ self.id = id;
139
+ self.rendered = FALSE;
140
+ self.destroyed = FALSE;
141
+ self.elements = elements = { target: target };
142
+ self.timers = { img: {} };
143
+ self.options = options;
144
+ self.checks = {};
145
+ self.plugins = {};
146
+ self.cache = cache = {
147
+ event: {},
148
+ target: $(),
149
+ disabled: FALSE,
150
+ attr: attr,
151
+ onTarget: FALSE,
152
+ lastClass: ''
153
+ };
154
+
155
+ /*
156
+ * Private core functions
157
+ */
158
+ function convertNotation(notation)
159
+ {
160
+ var i = 0, obj, option = options,
161
+
162
+ // Split notation into array
163
+ levels = notation.split('.');
164
+
165
+ // Loop through
166
+ while( option = option[ levels[i++] ] ) {
167
+ if(i < levels.length) { obj = option; }
168
+ }
169
+
170
+ return [obj || options, levels.pop()];
171
+ }
172
+
173
+ function triggerEvent(type, args, event) {
174
+ var callback = $.Event('tooltip'+type);
175
+ callback.originalEvent = (event ? $.extend({}, event) : NULL) || cache.event || NULL;
176
+ tooltip.trigger(callback, [self].concat(args || []));
177
+
178
+ return !callback.isDefaultPrevented();
179
+ }
180
+
181
+ function setWidget()
182
+ {
183
+ var on = options.style.widget;
184
+
185
+ tooltip.toggleClass('ui-helper-reset '+widget, on).toggleClass(defaultClass, options.style.def && !on);
186
+
187
+ if(elements.content) {
188
+ elements.content.toggleClass(widget+'-content', on);
189
+ }
190
+
191
+ if(elements.titlebar) {
192
+ elements.titlebar.toggleClass(widget+'-header', on);
193
+ }
194
+ if(elements.button) {
195
+ elements.button.toggleClass(uitooltip+'-icon', !on);
196
+ }
197
+ }
198
+
199
+ function removeTitle(reposition)
200
+ {
201
+ if(elements.title) {
202
+ elements.titlebar.remove();
203
+ elements.titlebar = elements.title = elements.button = NULL;
204
+
205
+ // Reposition if enabled
206
+ if(reposition !== FALSE) { self.reposition(); }
207
+ }
208
+ }
209
+
210
+ function createButton()
211
+ {
212
+ var button = options.content.title.button,
213
+ isString = typeof button === 'string',
214
+ close = isString ? button : 'Close tooltip';
215
+
216
+ if(elements.button) { elements.button.remove(); }
217
+
218
+ // Use custom button if one was supplied by user, else use default
219
+ if(button.jquery) {
220
+ elements.button = button;
221
+ }
222
+ else {
223
+ elements.button = $('<a />', {
224
+ 'class': 'ui-state-default ui-tooltip-close ' + (options.style.widget ? '' : uitooltip+'-icon'),
225
+ 'title': close,
226
+ 'aria-label': close
227
+ })
228
+ .prepend(
229
+ $('<span />', {
230
+ 'class': 'ui-icon ui-icon-close',
231
+ 'html': '&times;'
232
+ })
233
+ );
234
+ }
235
+
236
+ // Create button and setup attributes
237
+ elements.button.appendTo(elements.titlebar)
238
+ .attr('role', 'button')
239
+ .click(function(event) {
240
+ if(!tooltip.hasClass(disabled)) { self.hide(event); }
241
+ return FALSE;
242
+ });
243
+
244
+ // Redraw the tooltip when we're done
245
+ self.redraw();
246
+ }
247
+
248
+ function createTitle()
249
+ {
250
+ var id = tooltipID+'-title';
251
+
252
+ // Destroy previous title element, if present
253
+ if(elements.titlebar) { removeTitle(); }
254
+
255
+ // Create title bar and title elements
256
+ elements.titlebar = $('<div />', {
257
+ 'class': uitooltip + '-titlebar ' + (options.style.widget ? 'ui-widget-header' : '')
258
+ })
259
+ .append(
260
+ elements.title = $('<div />', {
261
+ 'id': id,
262
+ 'class': uitooltip + '-title',
263
+ 'aria-atomic': TRUE
264
+ })
265
+ )
266
+ .insertBefore(elements.content)
267
+
268
+ // Button-specific events
269
+ .delegate('.ui-tooltip-close', 'mousedown keydown mouseup keyup mouseout', function(event) {
270
+ $(this).toggleClass('ui-state-active ui-state-focus', event.type.substr(-4) === 'down');
271
+ })
272
+ .delegate('.ui-tooltip-close', 'mouseover mouseout', function(event){
273
+ $(this).toggleClass('ui-state-hover', event.type === 'mouseover');
274
+ });
275
+
276
+ // Create button if enabled
277
+ if(options.content.title.button) { createButton(); }
278
+
279
+ // Redraw the tooltip dimensions if it's rendered
280
+ else if(self.rendered){ self.redraw(); }
281
+ }
282
+
283
+ function updateButton(button)
284
+ {
285
+ var elem = elements.button,
286
+ title = elements.title;
287
+
288
+ // Make sure tooltip is rendered and if not, return
289
+ if(!self.rendered) { return FALSE; }
290
+
291
+ if(!button) {
292
+ elem.remove();
293
+ }
294
+ else {
295
+ if(!title) {
296
+ createTitle();
297
+ }
298
+ createButton();
299
+ }
300
+ }
301
+
302
+ function updateTitle(content, reposition)
303
+ {
304
+ var elem = elements.title;
305
+
306
+ // Make sure tooltip is rendered and if not, return
307
+ if(!self.rendered || !content) { return FALSE; }
308
+
309
+ // Use function to parse content
310
+ if($.isFunction(content)) {
311
+ content = content.call(target, cache.event, self);
312
+ }
313
+
314
+ // Remove title if callback returns false or null/undefined (but not '')
315
+ if(content === FALSE || (!content && content !== '')) { return removeTitle(FALSE); }
316
+
317
+ // Append new content if its a DOM array and show it if hidden
318
+ else if(content.jquery && content.length > 0) {
319
+ elem.empty().append(content.css({ display: 'block' }));
320
+ }
321
+
322
+ // Content is a regular string, insert the new content
323
+ else { elem.html(content); }
324
+
325
+ // Redraw and reposition
326
+ self.redraw();
327
+ if(reposition !== FALSE && self.rendered && tooltip[0].offsetWidth > 0) {
328
+ self.reposition(cache.event);
329
+ }
330
+ }
331
+
332
+ function updateContent(content, reposition)
333
+ {
334
+ var elem = elements.content;
335
+
336
+ // Make sure tooltip is rendered and content is defined. If not return
337
+ if(!self.rendered || !content) { return FALSE; }
338
+
339
+ // Use function to parse content
340
+ if($.isFunction(content)) {
341
+ content = content.call(target, cache.event, self) || '';
342
+ }
343
+
344
+ // Append new content if its a DOM array and show it if hidden
345
+ if(content.jquery && content.length > 0) {
346
+ elem.empty().append(content.css({ display: 'block' }));
347
+ }
348
+
349
+ // Content is a regular string, insert the new content
350
+ else { elem.html(content); }
351
+
352
+ // Image detection
353
+ function detectImages(next) {
354
+ var images, srcs = {};
355
+
356
+ function imageLoad(image) {
357
+ // Clear src from object and any timers and events associated with the image
358
+ if(image) {
359
+ delete srcs[image.src];
360
+ clearTimeout(self.timers.img[image.src]);
361
+ $(image).unbind(namespace);
362
+ }
363
+
364
+ // If queue is empty after image removal, update tooltip and continue the queue
365
+ if($.isEmptyObject(srcs)) {
366
+ self.redraw();
367
+ if(reposition !== FALSE) {
368
+ self.reposition(cache.event);
369
+ }
370
+
371
+ next();
372
+ }
373
+ }
374
+
375
+ // Find all content images without dimensions, and if no images were found, continue
376
+ if((images = elem.find('img[src]:not([height]):not([width])')).length === 0) { return imageLoad(); }
377
+
378
+ // Apply timer to each image to poll for dimensions
379
+ images.each(function(i, elem) {
380
+ // Skip if the src is already present
381
+ if(srcs[elem.src] !== undefined) { return; }
382
+
383
+ // Keep track of how many times we poll for image dimensions.
384
+ // If it doesn't return in a reasonable amount of time, it's better
385
+ // to display the tooltip, rather than hold up the queue.
386
+ var iterations = 0, maxIterations = 3;
387
+
388
+ (function timer(){
389
+ // When the dimensions are found, remove the image from the queue
390
+ if(elem.height || elem.width || (iterations > maxIterations)) { return imageLoad(elem); }
391
+
392
+ // Increase iterations and restart timer
393
+ iterations += 1;
394
+ self.timers.img[elem.src] = setTimeout(timer, 700);
395
+ }());
396
+
397
+ // Also apply regular load/error event handlers
398
+ $(elem).bind('error'+namespace+' load'+namespace, function(){ imageLoad(this); });
399
+
400
+ // Store the src and element in our object
401
+ srcs[elem.src] = elem;
402
+ });
403
+ }
404
+
405
+ /*
406
+ * If we're still rendering... insert into 'fx' queue our image dimension
407
+ * checker which will halt the showing of the tooltip until image dimensions
408
+ * can be detected properly.
409
+ */
410
+ if(self.rendered < 0) { tooltip.queue('fx', detectImages); }
411
+
412
+ // We're fully rendered, so reset isDrawing flag and proceed without queue delay
413
+ else { isDrawing = 0; detectImages($.noop); }
414
+
415
+ return self;
416
+ }
417
+
418
+ function assignEvents()
419
+ {
420
+ var posOptions = options.position,
421
+ targets = {
422
+ show: options.show.target,
423
+ hide: options.hide.target,
424
+ viewport: $(posOptions.viewport),
425
+ document: $(document),
426
+ body: $(document.body),
427
+ window: $(window)
428
+ },
429
+ events = {
430
+ show: $.trim('' + options.show.event).split(' '),
431
+ hide: $.trim('' + options.hide.event).split(' ')
432
+ },
433
+ IE6 = $.browser.msie && parseInt($.browser.version, 10) === 6;
434
+
435
+ // Define show event method
436
+ function showMethod(event)
437
+ {
438
+ if(tooltip.hasClass(disabled)) { return FALSE; }
439
+
440
+ // Clear hide timers
441
+ clearTimeout(self.timers.show);
442
+ clearTimeout(self.timers.hide);
443
+
444
+ // Start show timer
445
+ var callback = function(){ self.toggle(TRUE, event); };
446
+ if(options.show.delay > 0) {
447
+ self.timers.show = setTimeout(callback, options.show.delay);
448
+ }
449
+ else{ callback(); }
450
+ }
451
+
452
+ // Define hide method
453
+ function hideMethod(event)
454
+ {
455
+ if(tooltip.hasClass(disabled) || isPositioning || isDrawing) { return FALSE; }
456
+
457
+ // Check if new target was actually the tooltip element
458
+ var relatedTarget = $(event.relatedTarget || event.target),
459
+ ontoTooltip = relatedTarget.closest(selector)[0] === tooltip[0],
460
+ ontoTarget = relatedTarget[0] === targets.show[0];
461
+
462
+ // Clear timers and stop animation queue
463
+ clearTimeout(self.timers.show);
464
+ clearTimeout(self.timers.hide);
465
+
466
+ // Prevent hiding if tooltip is fixed and event target is the tooltip. Or if mouse positioning is enabled and cursor momentarily overlaps
467
+ if((posOptions.target === 'mouse' && ontoTooltip) || (options.hide.fixed && ((/mouse(out|leave|move)/).test(event.type) && (ontoTooltip || ontoTarget)))) {
468
+ try { event.preventDefault(); event.stopImmediatePropagation(); } catch(e) {} return;
469
+ }
470
+
471
+ // If tooltip has displayed, start hide timer
472
+ if(options.hide.delay > 0) {
473
+ self.timers.hide = setTimeout(function(){ self.hide(event); }, options.hide.delay);
474
+ }
475
+ else{ self.hide(event); }
476
+ }
477
+
478
+ // Define inactive method
479
+ function inactiveMethod(event)
480
+ {
481
+ if(tooltip.hasClass(disabled)) { return FALSE; }
482
+
483
+ // Clear timer
484
+ clearTimeout(self.timers.inactive);
485
+ self.timers.inactive = setTimeout(function(){ self.hide(event); }, options.hide.inactive);
486
+ }
487
+
488
+ function repositionMethod(event) {
489
+ if(self.rendered && tooltip[0].offsetWidth > 0) { self.reposition(event); }
490
+ }
491
+
492
+ // On mouseenter/mouseleave...
493
+ tooltip.bind('mouseenter'+namespace+' mouseleave'+namespace, function(event) {
494
+ var state = event.type === 'mouseenter';
495
+
496
+ // Focus the tooltip on mouseenter (z-index stacking)
497
+ if(state) { self.focus(event); }
498
+
499
+ // Add hover class
500
+ tooltip.toggleClass(hoverClass, state);
501
+ });
502
+
503
+ // If using mouseout/mouseleave as a hide event...
504
+ if(/mouse(out|leave)/i.test(options.hide.event)) {
505
+ // Hide tooltips when leaving current window/frame (but not select/option elements)
506
+ if(options.hide.leave === 'window') {
507
+ targets.window.bind('mouseout'+namespace+' blur'+namespace, function(event) {
508
+ if(!/select|option/.test(event.target.nodeName) && !event.relatedTarget) { self.hide(event); }
509
+ });
510
+ }
511
+ }
512
+
513
+ // Enable hide.fixed
514
+ if(options.hide.fixed) {
515
+ // Add tooltip as a hide target
516
+ targets.hide = targets.hide.add(tooltip);
517
+
518
+ // Clear hide timer on tooltip hover to prevent it from closing
519
+ tooltip.bind('mouseover'+namespace, function() {
520
+ if(!tooltip.hasClass(disabled)) { clearTimeout(self.timers.hide); }
521
+ });
522
+ }
523
+
524
+ /*
525
+ * Make sure hoverIntent functions properly by using mouseleave to clear show timer if
526
+ * mouseenter/mouseout is used for show.event, even if it isn't in the users options.
527
+ */
528
+ else if(/mouse(over|enter)/i.test(options.show.event)) {
529
+ targets.hide.bind('mouseleave'+namespace, function(event) {
530
+ clearTimeout(self.timers.show);
531
+ });
532
+ }
533
+
534
+ // Hide tooltip on document mousedown if unfocus events are enabled
535
+ if(('' + options.hide.event).indexOf('unfocus') > -1) {
536
+ posOptions.container.closest('html').bind('mousedown'+namespace, function(event) {
537
+ var elem = $(event.target),
538
+ enabled = self.rendered && !tooltip.hasClass(disabled) && tooltip[0].offsetWidth > 0,
539
+ isAncestor = elem.parents(selector).filter(tooltip[0]).length > 0;
540
+
541
+ if(elem[0] !== target[0] && elem[0] !== tooltip[0] && !isAncestor &&
542
+ !target.has(elem[0]).length && !elem.attr('disabled')
543
+ ) {
544
+ self.hide(event);
545
+ }
546
+ });
547
+ }
548
+
549
+ // Check if the tooltip hides when inactive
550
+ if('number' === typeof options.hide.inactive) {
551
+ // Bind inactive method to target as a custom event
552
+ targets.show.bind('qtip-'+id+'-inactive', inactiveMethod);
553
+
554
+ // Define events which reset the 'inactive' event handler
555
+ $.each(QTIP.inactiveEvents, function(index, type){
556
+ targets.hide.add(elements.tooltip).bind(type+namespace+'-inactive', inactiveMethod);
557
+ });
558
+ }
559
+
560
+ // Apply hide events
561
+ $.each(events.hide, function(index, type) {
562
+ var showIndex = $.inArray(type, events.show),
563
+ targetHide = $(targets.hide);
564
+
565
+ // Both events and targets are identical, apply events using a toggle
566
+ if((showIndex > -1 && targetHide.add(targets.show).length === targetHide.length) || type === 'unfocus')
567
+ {
568
+ targets.show.bind(type+namespace, function(event) {
569
+ if(tooltip[0].offsetWidth > 0) { hideMethod(event); }
570
+ else { showMethod(event); }
571
+ });
572
+
573
+ // Don't bind the event again
574
+ delete events.show[ showIndex ];
575
+ }
576
+
577
+ // Events are not identical, bind normally
578
+ else { targets.hide.bind(type+namespace, hideMethod); }
579
+ });
580
+
581
+ // Apply show events
582
+ $.each(events.show, function(index, type) {
583
+ targets.show.bind(type+namespace, showMethod);
584
+ });
585
+
586
+ // Check if the tooltip hides when mouse is moved a certain distance
587
+ if('number' === typeof options.hide.distance) {
588
+ // Bind mousemove to target to detect distance difference
589
+ targets.show.add(tooltip).bind('mousemove'+namespace, function(event) {
590
+ var origin = cache.origin || {},
591
+ limit = options.hide.distance,
592
+ abs = Math.abs;
593
+
594
+ // Check if the movement has gone beyond the limit, and hide it if so
595
+ if(abs(event.pageX - origin.pageX) >= limit || abs(event.pageY - origin.pageY) >= limit) {
596
+ self.hide(event);
597
+ }
598
+ });
599
+ }
600
+
601
+ // Mouse positioning events
602
+ if(posOptions.target === 'mouse') {
603
+ // Cache mousemove coords on show targets
604
+ targets.show.bind('mousemove'+namespace, function(event) {
605
+ MOUSE = { pageX: event.pageX, pageY: event.pageY, type: 'mousemove' };
606
+ });
607
+
608
+ // If mouse adjustment is on...
609
+ if(posOptions.adjust.mouse) {
610
+ // Apply a mouseleave event so we don't get problems with overlapping
611
+ if(options.hide.event) {
612
+ // Hide when we leave the tooltip and not onto the show target
613
+ tooltip.bind('mouseleave'+namespace, function(event) {
614
+ if((event.relatedTarget || event.target) !== targets.show[0]) { self.hide(event); }
615
+ });
616
+
617
+ // Track if we're on the target or not
618
+ elements.target.bind('mouseenter'+namespace+' mouseleave'+namespace, function(event) {
619
+ cache.onTarget = event.type === 'mouseenter';
620
+ });
621
+ }
622
+
623
+ // Update tooltip position on mousemove
624
+ targets.document.bind('mousemove'+namespace, function(event) {
625
+ // Update the tooltip position only if the tooltip is visible and adjustment is enabled
626
+ if(self.rendered && cache.onTarget && !tooltip.hasClass(disabled) && tooltip[0].offsetWidth > 0) {
627
+ self.reposition(event || MOUSE);
628
+ }
629
+ });
630
+ }
631
+ }
632
+
633
+ // Adjust positions of the tooltip on window resize if enabled
634
+ if(posOptions.adjust.resize || targets.viewport.length) {
635
+ ($.event.special.resize ? targets.viewport : targets.window).bind('resize'+namespace, repositionMethod);
636
+ }
637
+
638
+ // Adjust tooltip position on scroll if screen adjustment is enabled
639
+ if(targets.viewport.length || (IE6 && tooltip.css('position') === 'fixed')) {
640
+ targets.viewport.bind('scroll'+namespace, repositionMethod);
641
+ }
642
+ }
643
+
644
+ function unassignEvents()
645
+ {
646
+ var targets = [
647
+ options.show.target[0],
648
+ options.hide.target[0],
649
+ self.rendered && elements.tooltip[0],
650
+ options.position.container[0],
651
+ options.position.viewport[0],
652
+ options.position.container.closest('html')[0], // unfocus
653
+ window,
654
+ document
655
+ ];
656
+
657
+ // Check if tooltip is rendered
658
+ if(self.rendered) {
659
+ $([]).pushStack( $.grep(targets, function(i){ return typeof i === 'object'; }) ).unbind(namespace);
660
+ }
661
+
662
+ // Tooltip isn't yet rendered, remove render event
663
+ else { options.show.target.unbind(namespace+'-create'); }
664
+ }
665
+
666
+ // Setup builtin .set() option checks
667
+ self.checks.builtin = {
668
+ // Core checks
669
+ '^id$': function(obj, o, v) {
670
+ var id = v === TRUE ? QTIP.nextid : v,
671
+ tooltipID = uitooltip + '-' + id;
672
+
673
+ if(id !== FALSE && id.length > 0 && !$('#'+tooltipID).length) {
674
+ tooltip[0].id = tooltipID;
675
+ elements.content[0].id = tooltipID + '-content';
676
+ elements.title[0].id = tooltipID + '-title';
677
+ }
678
+ },
679
+
680
+ // Content checks
681
+ '^content.text$': function(obj, o, v){ updateContent(v); },
682
+ '^content.title.text$': function(obj, o, v) {
683
+ // Remove title if content is null
684
+ if(!v) { return removeTitle(); }
685
+
686
+ // If title isn't already created, create it now and update
687
+ if(!elements.title && v) { createTitle(); }
688
+ updateTitle(v);
689
+ },
690
+ '^content.title.button$': function(obj, o, v){ updateButton(v); },
691
+
692
+ // Position checks
693
+ '^position.(my|at)$': function(obj, o, v){
694
+ // Parse new corner value into Corner objecct
695
+ if('string' === typeof v) {
696
+ obj[o] = new PLUGINS.Corner(v);
697
+ }
698
+ },
699
+ '^position.container$': function(obj, o, v){
700
+ if(self.rendered) { tooltip.appendTo(v); }
701
+ },
702
+
703
+ // Show checks
704
+ '^show.ready$': function() {
705
+ if(!self.rendered) { self.render(1); }
706
+ else { self.toggle(TRUE); }
707
+ },
708
+
709
+ // Style checks
710
+ '^style.classes$': function(obj, o, v) {
711
+ tooltip.attr('class', uitooltip + ' qtip ' + v);
712
+ },
713
+ '^style.widget|content.title': setWidget,
714
+
715
+ // Events check
716
+ '^events.(render|show|move|hide|focus|blur)$': function(obj, o, v) {
717
+ tooltip[($.isFunction(v) ? '' : 'un') + 'bind']('tooltip'+o, v);
718
+ },
719
+
720
+ // Properties which require event reassignment
721
+ '^(show|hide|position).(event|target|fixed|inactive|leave|distance|viewport|adjust)': function() {
722
+ var posOptions = options.position;
723
+
724
+ // Set tracking flag
725
+ tooltip.attr('tracking', posOptions.target === 'mouse' && posOptions.adjust.mouse);
726
+
727
+ // Reassign events
728
+ unassignEvents(); assignEvents();
729
+ }
730
+ };
731
+
732
+ /*
733
+ * Public API methods
734
+ */
735
+ $.extend(self, {
736
+ render: function(show)
737
+ {
738
+ if(self.rendered) { return self; } // If tooltip has already been rendered, exit
739
+
740
+ var text = options.content.text,
741
+ title = options.content.title.text,
742
+ posOptions = options.position;
743
+
744
+ // Add ARIA attributes to target
745
+ $.attr(target[0], 'aria-describedby', tooltipID);
746
+
747
+ // Create tooltip element
748
+ tooltip = elements.tooltip = $('<div/>', {
749
+ 'id': tooltipID,
750
+ 'class': uitooltip + ' qtip ' + defaultClass + ' ' + options.style.classes + ' '+ uitooltip + '-pos-' + options.position.my.abbrev(),
751
+ 'width': options.style.width || '',
752
+ 'height': options.style.height || '',
753
+ 'tracking': posOptions.target === 'mouse' && posOptions.adjust.mouse,
754
+
755
+ /* ARIA specific attributes */
756
+ 'role': 'alert',
757
+ 'aria-live': 'polite',
758
+ 'aria-atomic': FALSE,
759
+ 'aria-describedby': tooltipID + '-content',
760
+ 'aria-hidden': TRUE
761
+ })
762
+ .toggleClass(disabled, cache.disabled)
763
+ .data('qtip', self)
764
+ .appendTo(options.position.container)
765
+ .append(
766
+ // Create content element
767
+ elements.content = $('<div />', {
768
+ 'class': uitooltip + '-content',
769
+ 'id': tooltipID + '-content',
770
+ 'aria-atomic': TRUE
771
+ })
772
+ );
773
+
774
+ // Set rendered flag and prevent redundant redraw/reposition calls for now
775
+ self.rendered = -1;
776
+ isDrawing = 1; isPositioning = 1;
777
+
778
+ // Create title...
779
+ if(title) {
780
+ createTitle();
781
+
782
+ // Update title only if its not a callback (called in toggle if so)
783
+ if(!$.isFunction(title)) { updateTitle(title, FALSE); }
784
+ }
785
+
786
+ // Set proper rendered flag and update content if not a callback function (called in toggle)
787
+ if(!$.isFunction(text)) { updateContent(text, FALSE); }
788
+ self.rendered = TRUE;
789
+
790
+ // Setup widget classes
791
+ setWidget();
792
+
793
+ // Assign passed event callbacks (before plugins!)
794
+ $.each(options.events, function(name, callback) {
795
+ if($.isFunction(callback)) {
796
+ tooltip.bind(name === 'toggle' ? 'tooltipshow tooltiphide' : 'tooltip'+name, callback);
797
+ }
798
+ });
799
+
800
+ // Initialize 'render' plugins
801
+ $.each(PLUGINS, function() {
802
+ if(this.initialize === 'render') { this(self); }
803
+ });
804
+
805
+ // Assign events
806
+ assignEvents();
807
+
808
+ /* Queue this part of the render process in our fx queue so we can
809
+ * load images before the tooltip renders fully.
810
+ *
811
+ * See: updateContent method
812
+ */
813
+ tooltip.queue('fx', function(next) {
814
+ // tooltiprender event
815
+ triggerEvent('render');
816
+
817
+ // Reset flags
818
+ isDrawing = 0; isPositioning = 0;
819
+
820
+ // Redraw the tooltip manually now we're fully rendered
821
+ self.redraw();
822
+
823
+ // Show tooltip if needed
824
+ if(options.show.ready || show) {
825
+ self.toggle(TRUE, cache.event, FALSE);
826
+ }
827
+
828
+ next(); // Move on to next method in queue
829
+ });
830
+
831
+ return self;
832
+ },
833
+
834
+ get: function(notation)
835
+ {
836
+ var result, o;
837
+
838
+ switch(notation.toLowerCase())
839
+ {
840
+ case 'dimensions':
841
+ result = {
842
+ height: tooltip.outerHeight(), width: tooltip.outerWidth()
843
+ };
844
+ break;
845
+
846
+ case 'offset':
847
+ result = PLUGINS.offset(tooltip, options.position.container);
848
+ break;
849
+
850
+ default:
851
+ o = convertNotation(notation.toLowerCase());
852
+ result = o[0][ o[1] ];
853
+ result = result.precedance ? result.string() : result;
854
+ break;
855
+ }
856
+
857
+ return result;
858
+ },
859
+
860
+ set: function(option, value)
861
+ {
862
+ var rmove = /^position\.(my|at|adjust|target|container)|style|content|show\.ready/i,
863
+ rdraw = /^content\.(title|attr)|style/i,
864
+ reposition = FALSE,
865
+ redraw = FALSE,
866
+ checks = self.checks,
867
+ name;
868
+
869
+ function callback(notation, args) {
870
+ var category, rule, match;
871
+
872
+ for(category in checks) {
873
+ for(rule in checks[category]) {
874
+ if(match = (new RegExp(rule, 'i')).exec(notation)) {
875
+ args.push(match);
876
+ checks[category][rule].apply(self, args);
877
+ }
878
+ }
879
+ }
880
+ }
881
+
882
+ // Convert singular option/value pair into object form
883
+ if('string' === typeof option) {
884
+ name = option; option = {}; option[name] = value;
885
+ }
886
+ else { option = $.extend(TRUE, {}, option); }
887
+
888
+ // Set all of the defined options to their new values
889
+ $.each(option, function(notation, value) {
890
+ var obj = convertNotation( notation.toLowerCase() ), previous;
891
+
892
+ // Set new obj value
893
+ previous = obj[0][ obj[1] ];
894
+ obj[0][ obj[1] ] = 'object' === typeof value && value.nodeType ? $(value) : value;
895
+
896
+ // Set the new params for the callback
897
+ option[notation] = [obj[0], obj[1], value, previous];
898
+
899
+ // Also check if we need to reposition / redraw
900
+ reposition = rmove.test(notation) || reposition;
901
+ redraw = rdraw.test(notation) || redraw;
902
+ });
903
+
904
+ // Re-sanitize options
905
+ sanitizeOptions(options);
906
+
907
+ /*
908
+ * Execute any valid callbacks for the set options
909
+ * Also set isPositioning/isDrawing so we don't get loads of redundant repositioning
910
+ * and redraw calls.
911
+ */
912
+ isPositioning = isDrawing = 1; $.each(option, callback); isPositioning = isDrawing = 0;
913
+
914
+ // Update position / redraw if needed
915
+ if(self.rendered && tooltip[0].offsetWidth > 0) {
916
+ if(reposition) {
917
+ self.reposition( options.position.target === 'mouse' ? NULL : cache.event );
918
+ }
919
+ if(redraw) { self.redraw(); }
920
+ }
921
+
922
+ return self;
923
+ },
924
+
925
+ toggle: function(state, event)
926
+ {
927
+ // Render the tooltip if showing and it isn't already
928
+ if(!self.rendered) { return state ? self.render(1) : self; }
929
+
930
+ var type = state ? 'show' : 'hide',
931
+ opts = options[type],
932
+ otherOpts = options[ !state ? 'show' : 'hide' ],
933
+ posOptions = options.position,
934
+ contentOptions = options.content,
935
+ visible = tooltip[0].offsetWidth > 0,
936
+ animate = state || opts.target.length === 1,
937
+ sameTarget = !event || opts.target.length < 2 || cache.target[0] === event.target,
938
+ showEvent, delay;
939
+
940
+ // Detect state if valid one isn't provided
941
+ if((typeof state).search('boolean|number')) { state = !visible; }
942
+
943
+ // Return if element is already in correct state
944
+ if(!tooltip.is(':animated') && visible === state && sameTarget) { return self; }
945
+
946
+ // Try to prevent flickering when tooltip overlaps show element
947
+ if(event) {
948
+ if((/over|enter/).test(event.type) && (/out|leave/).test(cache.event.type) &&
949
+ options.show.target.add(event.target).length === options.show.target.length &&
950
+ tooltip.has(event.relatedTarget).length) {
951
+ return self;
952
+ }
953
+
954
+ // Cache event
955
+ cache.event = $.extend({}, event);
956
+ }
957
+
958
+ // tooltipshow/tooltiphide events
959
+ if(!triggerEvent(type, [90])) { return self; }
960
+
961
+ // Set ARIA hidden status attribute
962
+ $.attr(tooltip[0], 'aria-hidden', !!!state);
963
+
964
+ // Execute state specific properties
965
+ if(state) {
966
+ // Store show origin coordinates
967
+ cache.origin = $.extend({}, MOUSE);
968
+
969
+ // Focus the tooltip
970
+ self.focus(event);
971
+
972
+ // Update tooltip content & title if it's a dynamic function
973
+ if($.isFunction(contentOptions.text)) { updateContent(contentOptions.text, FALSE); }
974
+ if($.isFunction(contentOptions.title.text)) { updateTitle(contentOptions.title.text, FALSE); }
975
+
976
+ // Cache mousemove events for positioning purposes (if not already tracking)
977
+ if(!trackingBound && posOptions.target === 'mouse' && posOptions.adjust.mouse) {
978
+ $(document).bind('mousemove.qtip', function(event) {
979
+ MOUSE = { pageX: event.pageX, pageY: event.pageY, type: 'mousemove' };
980
+ });
981
+ trackingBound = TRUE;
982
+ }
983
+
984
+ // Update the tooltip position
985
+ self.reposition(event, arguments[2]);
986
+
987
+ // Hide other tooltips if tooltip is solo
988
+ if(!!opts.solo) {
989
+ $(selector, opts.solo).not(tooltip).qtip('hide', $.Event('tooltipsolo'));
990
+ }
991
+ }
992
+ else {
993
+ // Clear show timer if we're hiding
994
+ clearTimeout(self.timers.show);
995
+
996
+ // Remove cached origin on hide
997
+ delete cache.origin;
998
+
999
+ // Remove mouse tracking event if not needed (all tracking qTips are hidden)
1000
+ if(trackingBound && !$(selector+'[tracking="true"]:visible', opts.solo).not(tooltip).length) {
1001
+ $(document).unbind('mousemove.qtip');
1002
+ trackingBound = FALSE;
1003
+ }
1004
+
1005
+ // Blur the tooltip
1006
+ self.blur(event);
1007
+ }
1008
+
1009
+ // Define post-animation, state specific properties
1010
+ function after() {
1011
+ if(state) {
1012
+ // Prevent antialias from disappearing in IE by removing filter
1013
+ if($.browser.msie) { tooltip[0].style.removeAttribute('filter'); }
1014
+
1015
+ // Remove overflow setting to prevent tip bugs
1016
+ tooltip.css('overflow', '');
1017
+
1018
+ // Autofocus elements if enabled
1019
+ if('string' === typeof opts.autofocus) {
1020
+ $(opts.autofocus, tooltip).focus();
1021
+ }
1022
+
1023
+ // If set, hide tooltip when inactive for delay period
1024
+ opts.target.trigger('qtip-'+id+'-inactive');
1025
+ }
1026
+ else {
1027
+ // Reset CSS states
1028
+ tooltip.css({
1029
+ display: '',
1030
+ visibility: '',
1031
+ opacity: '',
1032
+ left: '',
1033
+ top: ''
1034
+ });
1035
+ }
1036
+
1037
+ // tooltipvisible/tooltiphidden events
1038
+ triggerEvent(state ? 'visible' : 'hidden');
1039
+ }
1040
+
1041
+ // If no effect type is supplied, use a simple toggle
1042
+ if(opts.effect === FALSE || animate === FALSE) {
1043
+ tooltip[ type ]();
1044
+ after.call(tooltip);
1045
+ }
1046
+
1047
+ // Use custom function if provided
1048
+ else if($.isFunction(opts.effect)) {
1049
+ tooltip.stop(1, 1);
1050
+ opts.effect.call(tooltip, self);
1051
+ tooltip.queue('fx', function(n){ after(); n(); });
1052
+ }
1053
+
1054
+ // Use basic fade function by default
1055
+ else { tooltip.fadeTo(90, state ? 1 : 0, after); }
1056
+
1057
+ // If inactive hide method is set, active it
1058
+ if(state) { opts.target.trigger('qtip-'+id+'-inactive'); }
1059
+
1060
+ return self;
1061
+ },
1062
+
1063
+ show: function(event){ return self.toggle(TRUE, event); },
1064
+
1065
+ hide: function(event){ return self.toggle(FALSE, event); },
1066
+
1067
+ focus: function(event)
1068
+ {
1069
+ if(!self.rendered) { return self; }
1070
+
1071
+ var qtips = $(selector),
1072
+ curIndex = parseInt(tooltip[0].style.zIndex, 10),
1073
+ newIndex = QTIP.zindex + qtips.length,
1074
+ cachedEvent = $.extend({}, event),
1075
+ focusedElem;
1076
+
1077
+ // Only update the z-index if it has changed and tooltip is not already focused
1078
+ if(!tooltip.hasClass(focusClass))
1079
+ {
1080
+ // tooltipfocus event
1081
+ if(triggerEvent('focus', [newIndex], cachedEvent)) {
1082
+ // Only update z-index's if they've changed
1083
+ if(curIndex !== newIndex) {
1084
+ // Reduce our z-index's and keep them properly ordered
1085
+ qtips.each(function() {
1086
+ if(this.style.zIndex > curIndex) {
1087
+ this.style.zIndex = this.style.zIndex - 1;
1088
+ }
1089
+ });
1090
+
1091
+ // Fire blur event for focused tooltip
1092
+ qtips.filter('.' + focusClass).qtip('blur', cachedEvent);
1093
+ }
1094
+
1095
+ // Set the new z-index
1096
+ tooltip.addClass(focusClass)[0].style.zIndex = newIndex;
1097
+ }
1098
+ }
1099
+
1100
+ return self;
1101
+ },
1102
+
1103
+ blur: function(event) {
1104
+ // Set focused status to FALSE
1105
+ tooltip.removeClass(focusClass);
1106
+
1107
+ // tooltipblur event
1108
+ triggerEvent('blur', [tooltip.css('zIndex')], event);
1109
+
1110
+ return self;
1111
+ },
1112
+
1113
+ reposition: function(event, effect)
1114
+ {
1115
+ if(!self.rendered || isPositioning) { return self; }
1116
+
1117
+ // Set positioning flag
1118
+ isPositioning = 1;
1119
+
1120
+ var target = options.position.target,
1121
+ posOptions = options.position,
1122
+ my = posOptions.my,
1123
+ at = posOptions.at,
1124
+ adjust = posOptions.adjust,
1125
+ method = adjust.method.split(' '),
1126
+ elemWidth = tooltip.outerWidth(),
1127
+ elemHeight = tooltip.outerHeight(),
1128
+ targetWidth = 0,
1129
+ targetHeight = 0,
1130
+ fixed = tooltip.css('position') === 'fixed',
1131
+ viewport = posOptions.viewport,
1132
+ position = { left: 0, top: 0 },
1133
+ container = posOptions.container,
1134
+ visible = tooltip[0].offsetWidth > 0,
1135
+ adjusted, offset, win;
1136
+
1137
+ // Check if absolute position was passed
1138
+ if($.isArray(target) && target.length === 2) {
1139
+ // Force left top and set position
1140
+ at = { x: LEFT, y: TOP };
1141
+ position = { left: target[0], top: target[1] };
1142
+ }
1143
+
1144
+ // Check if mouse was the target
1145
+ else if(target === 'mouse' && ((event && event.pageX) || cache.event.pageX)) {
1146
+ // Force left top to allow flipping
1147
+ at = { x: LEFT, y: TOP };
1148
+
1149
+ // Use cached event if one isn't available for positioning
1150
+ event = (event && (event.type === 'resize' || event.type === 'scroll') ? cache.event :
1151
+ event && event.pageX && event.type === 'mousemove' ? event :
1152
+ MOUSE && MOUSE.pageX && (adjust.mouse || !event || !event.pageX) ? { pageX: MOUSE.pageX, pageY: MOUSE.pageY } :
1153
+ !adjust.mouse && cache.origin && cache.origin.pageX && options.show.distance ? cache.origin :
1154
+ event) || event || cache.event || MOUSE || {};
1155
+
1156
+ // Use event coordinates for position
1157
+ position = { top: event.pageY, left: event.pageX };
1158
+ }
1159
+
1160
+ // Target wasn't mouse or absolute...
1161
+ else {
1162
+ // Check if event targetting is being used
1163
+ if(target === 'event' && event && event.target && event.type !== 'scroll' && event.type !== 'resize') {
1164
+ cache.target = $(event.target);
1165
+ }
1166
+ else if(target !== 'event'){
1167
+ cache.target = $(target.jquery ? target : elements.target);
1168
+ }
1169
+ target = cache.target;
1170
+
1171
+ // Parse the target into a jQuery object and make sure there's an element present
1172
+ target = $(target).eq(0);
1173
+ if(target.length === 0) { return self; }
1174
+
1175
+ // Check if window or document is the target
1176
+ else if(target[0] === document || target[0] === window) {
1177
+ targetWidth = PLUGINS.iOS ? window.innerWidth : target.width();
1178
+ targetHeight = PLUGINS.iOS ? window.innerHeight : target.height();
1179
+
1180
+ if(target[0] === window) {
1181
+ position = {
1182
+ top: (viewport || target).scrollTop(),
1183
+ left: (viewport || target).scrollLeft()
1184
+ };
1185
+ }
1186
+ }
1187
+
1188
+ // Use Imagemap/SVG plugins if needed
1189
+ else if(PLUGINS.imagemap && target.is('area')) {
1190
+ adjusted = PLUGINS.imagemap(self, target, at, PLUGINS.viewport ? method : FALSE);
1191
+ }
1192
+ else if(PLUGINS.svg && typeof target[0].xmlbase === 'string') {
1193
+ adjusted = PLUGINS.svg(self, target, at, PLUGINS.viewport ? method : FALSE);
1194
+ }
1195
+
1196
+ else {
1197
+ targetWidth = target.outerWidth();
1198
+ targetHeight = target.outerHeight();
1199
+
1200
+ position = PLUGINS.offset(target, container);
1201
+ }
1202
+
1203
+ // Parse returned plugin values into proper variables
1204
+ if(adjusted) {
1205
+ targetWidth = adjusted.width;
1206
+ targetHeight = adjusted.height;
1207
+ offset = adjusted.offset;
1208
+ position = adjusted.position;
1209
+ }
1210
+
1211
+ // Adjust for position.fixed tooltips (and also iOS scroll bug in v3.2-4.0 & v4.3-4.3.2)
1212
+ if((PLUGINS.iOS > 3.1 && PLUGINS.iOS < 4.1) ||
1213
+ (PLUGINS.iOS >= 4.3 && PLUGINS.iOS < 4.33) ||
1214
+ (!PLUGINS.iOS && fixed)
1215
+ ){
1216
+ win = $(window);
1217
+ position.left -= win.scrollLeft();
1218
+ position.top -= win.scrollTop();
1219
+ }
1220
+
1221
+ // Adjust position relative to target
1222
+ position.left += at.x === RIGHT ? targetWidth : at.x === CENTER ? targetWidth / 2 : 0;
1223
+ position.top += at.y === BOTTOM ? targetHeight : at.y === CENTER ? targetHeight / 2 : 0;
1224
+ }
1225
+
1226
+ // Adjust position relative to tooltip
1227
+ position.left += adjust.x + (my.x === RIGHT ? -elemWidth : my.x === CENTER ? -elemWidth / 2 : 0);
1228
+ position.top += adjust.y + (my.y === BOTTOM ? -elemHeight : my.y === CENTER ? -elemHeight / 2 : 0);
1229
+
1230
+ // Use viewport adjustment plugin if enabled
1231
+ if(PLUGINS.viewport) {
1232
+ position.adjusted = PLUGINS.viewport(
1233
+ self, position, posOptions, targetWidth, targetHeight, elemWidth, elemHeight
1234
+ );
1235
+
1236
+ // Apply offsets supplied by positioning plugin (if used)
1237
+ if(offset && position.adjusted.left) { position.left += offset.left; }
1238
+ if(offset && position.adjusted.top) { position.top += offset.top; }
1239
+ }
1240
+
1241
+ // Viewport adjustment is disabled, set values to zero
1242
+ else { position.adjusted = { left: 0, top: 0 }; }
1243
+
1244
+ // tooltipmove event
1245
+ if(!triggerEvent('move', [position, viewport.elem || viewport], event)) { return self; }
1246
+ delete position.adjusted;
1247
+
1248
+ // If effect is disabled, target it mouse, no animation is defined or positioning gives NaN out, set CSS directly
1249
+ if(effect === FALSE || !visible || isNaN(position.left) || isNaN(position.top) || target === 'mouse' || !$.isFunction(posOptions.effect)) {
1250
+ tooltip.css(position);
1251
+ }
1252
+
1253
+ // Use custom function if provided
1254
+ else if($.isFunction(posOptions.effect)) {
1255
+ posOptions.effect.call(tooltip, self, $.extend({}, position));
1256
+ tooltip.queue(function(next) {
1257
+ // Reset attributes to avoid cross-browser rendering bugs
1258
+ $(this).css({ opacity: '', height: '' });
1259
+ if($.browser.msie) { this.style.removeAttribute('filter'); }
1260
+
1261
+ next();
1262
+ });
1263
+ }
1264
+
1265
+ // Set positioning flag
1266
+ isPositioning = 0;
1267
+
1268
+ return self;
1269
+ },
1270
+
1271
+ // Max/min width simulator function for all browsers.. yeaaah!
1272
+ redraw: function()
1273
+ {
1274
+ if(self.rendered < 1 || isDrawing) { return self; }
1275
+
1276
+ var style = options.style,
1277
+ container = options.position.container,
1278
+ perc, width, max, min;
1279
+
1280
+ // Set drawing flag
1281
+ isDrawing = 1;
1282
+
1283
+ // tooltipredraw event
1284
+ triggerEvent('redraw');
1285
+
1286
+ // If tooltip has a set height/width, just set it... like a boss!
1287
+ if(style.height) { tooltip.css(HEIGHT, style.height); }
1288
+ if(style.width) { tooltip.css(WIDTH, style.width); }
1289
+
1290
+ // Simulate max/min width if not set width present...
1291
+ else {
1292
+ // Reset width and add fluid class
1293
+ tooltip.css(WIDTH, '').appendTo(redrawContainer);
1294
+
1295
+ // Grab our tooltip width (add 1 if odd so we don't get wrapping problems.. huzzah!)
1296
+ width = tooltip.width();
1297
+ if(width % 2 < 1) { width += 1; }
1298
+
1299
+ // Grab our max/min properties
1300
+ max = tooltip.css('max-width') || '';
1301
+ min = tooltip.css('min-width') || '';
1302
+
1303
+ // Parse into proper pixel values
1304
+ perc = (max + min).indexOf('%') > -1 ? container.width() / 100 : 0;
1305
+ max = ((max.indexOf('%') > -1 ? perc : 1) * parseInt(max, 10)) || width;
1306
+ min = ((min.indexOf('%') > -1 ? perc : 1) * parseInt(min, 10)) || 0;
1307
+
1308
+ // Determine new dimension size based on max/min/current values
1309
+ width = max + min ? Math.min(Math.max(width, min), max) : width;
1310
+
1311
+ // Set the newly calculated width and remvoe fluid class
1312
+ tooltip.css(WIDTH, Math.round(width)).appendTo(container);
1313
+ }
1314
+
1315
+ // tooltipredrawn event
1316
+ triggerEvent('redrawn');
1317
+
1318
+ // Set drawing flag
1319
+ isDrawing = 0;
1320
+
1321
+ return self;
1322
+ },
1323
+
1324
+ disable: function(state)
1325
+ {
1326
+ if('boolean' !== typeof state) {
1327
+ state = !(tooltip.hasClass(disabled) || cache.disabled);
1328
+ }
1329
+
1330
+ if(self.rendered) {
1331
+ tooltip.toggleClass(disabled, state);
1332
+ $.attr(tooltip[0], 'aria-disabled', state);
1333
+ }
1334
+ else {
1335
+ cache.disabled = !!state;
1336
+ }
1337
+
1338
+ return self;
1339
+ },
1340
+
1341
+ enable: function() { return self.disable(FALSE); },
1342
+
1343
+ destroy: function()
1344
+ {
1345
+ var t = target[0],
1346
+ title = $.attr(t, oldtitle),
1347
+ elemAPI = target.data('qtip');
1348
+
1349
+ // Set flag the signify destroy is taking place to plugins
1350
+ self.destroyed = TRUE;
1351
+
1352
+ // Destroy tooltip and any associated plugins if rendered
1353
+ if(self.rendered) {
1354
+ tooltip.stop(1,0).remove();
1355
+
1356
+ $.each(self.plugins, function() {
1357
+ if(this.destroy) { this.destroy(); }
1358
+ });
1359
+ }
1360
+
1361
+ // Clear timers and remove bound events
1362
+ clearTimeout(self.timers.show);
1363
+ clearTimeout(self.timers.hide);
1364
+ unassignEvents();
1365
+
1366
+ // If the API if actually this qTip API...
1367
+ if(!elemAPI || self === elemAPI) {
1368
+ // Remove api object
1369
+ $.removeData(t, 'qtip');
1370
+
1371
+ // Reset old title attribute if removed
1372
+ if(options.suppress && title) {
1373
+ $.attr(t, 'title', title);
1374
+ target.removeAttr(oldtitle);
1375
+ }
1376
+
1377
+ // Remove ARIA attributes
1378
+ target.removeAttr('aria-describedby');
1379
+ }
1380
+
1381
+ // Remove qTip events associated with this API
1382
+ target.unbind('.qtip-'+id);
1383
+
1384
+ // Remove ID from sued id object
1385
+ delete usedIDs[self.id];
1386
+
1387
+ return target;
1388
+ }
1389
+ });
1390
+ }
1391
+
1392
+ // Initialization method
1393
+ function init(id, opts)
1394
+ {
1395
+ var obj, posOptions, attr, config, title,
1396
+
1397
+ // Setup element references
1398
+ elem = $(this),
1399
+ docBody = $(document.body),
1400
+
1401
+ // Use document body instead of document element if needed
1402
+ newTarget = this === document ? docBody : elem,
1403
+
1404
+ // Grab metadata from element if plugin is present
1405
+ metadata = (elem.metadata) ? elem.metadata(opts.metadata) : NULL,
1406
+
1407
+ // If metadata type if HTML5, grab 'name' from the object instead, or use the regular data object otherwise
1408
+ metadata5 = opts.metadata.type === 'html5' && metadata ? metadata[opts.metadata.name] : NULL,
1409
+
1410
+ // Grab data from metadata.name (or data-qtipopts as fallback) using .data() method,
1411
+ html5 = elem.data(opts.metadata.name || 'qtipopts');
1412
+
1413
+ // If we don't get an object returned attempt to parse it manualyl without parseJSON
1414
+ try { html5 = typeof html5 === 'string' ? $.parseJSON(html5) : html5; } catch(e) {}
1415
+
1416
+ // Merge in and sanitize metadata
1417
+ config = $.extend(TRUE, {}, QTIP.defaults, opts,
1418
+ typeof html5 === 'object' ? sanitizeOptions(html5) : NULL,
1419
+ sanitizeOptions(metadata5 || metadata));
1420
+
1421
+ // Re-grab our positioning options now we've merged our metadata and set id to passed value
1422
+ posOptions = config.position;
1423
+ config.id = id;
1424
+
1425
+ // Setup missing content if none is detected
1426
+ if('boolean' === typeof config.content.text) {
1427
+ attr = elem.attr(config.content.attr);
1428
+
1429
+ // Grab from supplied attribute if available
1430
+ if(config.content.attr !== FALSE && attr) { config.content.text = attr; }
1431
+
1432
+ // No valid content was found, abort render
1433
+ else { return FALSE; }
1434
+ }
1435
+
1436
+ // Setup target options
1437
+ if(!posOptions.container.length) { posOptions.container = docBody; }
1438
+ if(posOptions.target === FALSE) { posOptions.target = newTarget; }
1439
+ if(config.show.target === FALSE) { config.show.target = newTarget; }
1440
+ if(config.show.solo === TRUE) { config.show.solo = posOptions.container.closest('body'); }
1441
+ if(config.hide.target === FALSE) { config.hide.target = newTarget; }
1442
+ if(config.position.viewport === TRUE) { config.position.viewport = posOptions.container; }
1443
+
1444
+ // Ensure we only use a single container
1445
+ posOptions.container = posOptions.container.eq(0);
1446
+
1447
+ // Convert position corner values into x and y strings
1448
+ posOptions.at = new PLUGINS.Corner(posOptions.at);
1449
+ posOptions.my = new PLUGINS.Corner(posOptions.my);
1450
+
1451
+ // Destroy previous tooltip if overwrite is enabled, or skip element if not
1452
+ if($.data(this, 'qtip')) {
1453
+ if(config.overwrite) {
1454
+ elem.qtip('destroy');
1455
+ }
1456
+ else if(config.overwrite === FALSE) {
1457
+ return FALSE;
1458
+ }
1459
+ }
1460
+
1461
+ // Remove title attribute and store it if present
1462
+ if(config.suppress && (title = $.attr(this, 'title'))) {
1463
+ // Final attr call fixes event delegatiom and IE default tooltip showing problem
1464
+ $(this).removeAttr('title').attr(oldtitle, title).attr('title', '');
1465
+ }
1466
+
1467
+ // Initialize the tooltip and add API reference
1468
+ obj = new QTip(elem, config, id, !!attr);
1469
+ $.data(this, 'qtip', obj);
1470
+
1471
+ // Catch remove/removeqtip events on target element to destroy redundant tooltip
1472
+ elem.bind('remove.qtip-'+id+' removeqtip.qtip-'+id, function(){ obj.destroy(); });
1473
+
1474
+ return obj;
1475
+ }
1476
+
1477
+ // jQuery $.fn extension method
1478
+ QTIP = $.fn.qtip = function(options, notation, newValue)
1479
+ {
1480
+ var command = ('' + options).toLowerCase(), // Parse command
1481
+ returned = NULL,
1482
+ args = $.makeArray(arguments).slice(1),
1483
+ event = args[args.length - 1],
1484
+ opts = this[0] ? $.data(this[0], 'qtip') : NULL;
1485
+
1486
+ // Check for API request
1487
+ if((!arguments.length && opts) || command === 'api') {
1488
+ return opts;
1489
+ }
1490
+
1491
+ // Execute API command if present
1492
+ else if('string' === typeof options)
1493
+ {
1494
+ this.each(function()
1495
+ {
1496
+ var api = $.data(this, 'qtip');
1497
+ if(!api) { return TRUE; }
1498
+
1499
+ // Cache the event if possible
1500
+ if(event && event.timeStamp) { api.cache.event = event; }
1501
+
1502
+ // Check for specific API commands
1503
+ if((command === 'option' || command === 'options') && notation) {
1504
+ if($.isPlainObject(notation) || newValue !== undefined) {
1505
+ api.set(notation, newValue);
1506
+ }
1507
+ else {
1508
+ returned = api.get(notation);
1509
+ return FALSE;
1510
+ }
1511
+ }
1512
+
1513
+ // Execute API command
1514
+ else if(api[command]) {
1515
+ api[command].apply(api[command], args);
1516
+ }
1517
+ });
1518
+
1519
+ return returned !== NULL ? returned : this;
1520
+ }
1521
+
1522
+ // No API commands. validate provided options and setup qTips
1523
+ else if('object' === typeof options || !arguments.length)
1524
+ {
1525
+ opts = sanitizeOptions($.extend(TRUE, {}, options));
1526
+
1527
+ // Bind the qTips
1528
+ return QTIP.bind.call(this, opts, event);
1529
+ }
1530
+ };
1531
+
1532
+ // $.fn.qtip Bind method
1533
+ QTIP.bind = function(opts, event)
1534
+ {
1535
+ return this.each(function(i) {
1536
+ var options, targets, events, namespace, api, id;
1537
+
1538
+ // Find next available ID, or use custom ID if provided
1539
+ id = $.isArray(opts.id) ? opts.id[i] : opts.id;
1540
+ id = !id || id === FALSE || id.length < 1 || usedIDs[id] ? QTIP.nextid++ : (usedIDs[id] = id);
1541
+
1542
+ // Setup events namespace
1543
+ namespace = '.qtip-'+id+'-create';
1544
+
1545
+ // Initialize the qTip and re-grab newly sanitized options
1546
+ api = init.call(this, id, opts);
1547
+ if(api === FALSE) { return TRUE; }
1548
+ options = api.options;
1549
+
1550
+ // Initialize plugins
1551
+ $.each(PLUGINS, function() {
1552
+ if(this.initialize === 'initialize') { this(api); }
1553
+ });
1554
+
1555
+ // Determine hide and show targets
1556
+ targets = { show: options.show.target, hide: options.hide.target };
1557
+ events = {
1558
+ show: $.trim('' + options.show.event).replace(/ /g, namespace+' ') + namespace,
1559
+ hide: $.trim('' + options.hide.event).replace(/ /g, namespace+' ') + namespace
1560
+ };
1561
+
1562
+ /*
1563
+ * Make sure hoverIntent functions properly by using mouseleave as a hide event if
1564
+ * mouseenter/mouseout is used for show.event, even if it isn't in the users options.
1565
+ */
1566
+ if(/mouse(over|enter)/i.test(events.show) && !/mouse(out|leave)/i.test(events.hide)) {
1567
+ events.hide += ' mouseleave' + namespace;
1568
+ }
1569
+
1570
+ /*
1571
+ * Also make sure initial mouse targetting works correctly by caching mousemove coords
1572
+ * on show targets before the tooltip has rendered.
1573
+ *
1574
+ * Also set onTarget when triggered to keep mouse tracking working
1575
+ */
1576
+ targets.show.bind('mousemove'+namespace, function(event) {
1577
+ MOUSE = { pageX: event.pageX, pageY: event.pageY, type: 'mousemove' };
1578
+ api.cache.onTarget = TRUE;
1579
+ });
1580
+
1581
+ // Define hoverIntent function
1582
+ function hoverIntent(event) {
1583
+ function render() {
1584
+ // Cache mouse coords,render and render the tooltip
1585
+ api.render(typeof event === 'object' || options.show.ready);
1586
+
1587
+ // Unbind show and hide events
1588
+ targets.show.add(targets.hide).unbind(namespace);
1589
+ }
1590
+
1591
+ // Only continue if tooltip isn't disabled
1592
+ if(api.cache.disabled) { return FALSE; }
1593
+
1594
+ // Cache the event data
1595
+ api.cache.event = $.extend({}, event);
1596
+ api.cache.target = event ? $(event.target) : [undefined];
1597
+
1598
+ // Start the event sequence
1599
+ if(options.show.delay > 0) {
1600
+ clearTimeout(api.timers.show);
1601
+ api.timers.show = setTimeout(render, options.show.delay);
1602
+ if(events.show !== events.hide) {
1603
+ targets.hide.bind(events.hide, function() { clearTimeout(api.timers.show); });
1604
+ }
1605
+ }
1606
+ else { render(); }
1607
+ }
1608
+
1609
+ // Bind show events to target
1610
+ targets.show.bind(events.show, hoverIntent);
1611
+
1612
+ // Prerendering is enabled, create tooltip now
1613
+ if(options.show.ready || options.prerender) { hoverIntent(event); }
1614
+ });
1615
+ };
1616
+
1617
+ // Setup base plugins
1618
+ PLUGINS = QTIP.plugins = {
1619
+ // Corner object parser
1620
+ Corner: function(corner) {
1621
+ corner = ('' + corner).replace(/([A-Z])/, ' $1').replace(/middle/gi, CENTER).toLowerCase();
1622
+ this.x = (corner.match(/left|right/i) || corner.match(/center/) || ['inherit'])[0].toLowerCase();
1623
+ this.y = (corner.match(/top|bottom|center/i) || ['inherit'])[0].toLowerCase();
1624
+
1625
+ var f = corner.charAt(0); this.precedance = (f === 't' || f === 'b' ? Y : X);
1626
+
1627
+ this.string = function() { return this.precedance === Y ? this.y+this.x : this.x+this.y; };
1628
+ this.abbrev = function() {
1629
+ var x = this.x.substr(0,1), y = this.y.substr(0,1);
1630
+ return x === y ? x : this.precedance === Y ? y + x : x + y;
1631
+ };
1632
+
1633
+ this.invertx = function(center) { this.x = this.x === LEFT ? RIGHT : this.x === RIGHT ? LEFT : center || this.x; };
1634
+ this.inverty = function(center) { this.y = this.y === TOP ? BOTTOM : this.y === BOTTOM ? TOP : center || this.y; };
1635
+
1636
+ this.clone = function() {
1637
+ return {
1638
+ x: this.x, y: this.y, precedance: this.precedance,
1639
+ string: this.string, abbrev: this.abbrev, clone: this.clone,
1640
+ invertx: this.invertx, inverty: this.inverty
1641
+ };
1642
+ };
1643
+ },
1644
+
1645
+ // Custom (more correct for qTip!) offset calculator
1646
+ offset: function(elem, container) {
1647
+ var pos = elem.offset(),
1648
+ docBody = elem.closest('body')[0],
1649
+ parent = container, scrolled,
1650
+ coffset, overflow;
1651
+
1652
+ function scroll(e, i) {
1653
+ pos.left += i * e.scrollLeft();
1654
+ pos.top += i * e.scrollTop();
1655
+ }
1656
+
1657
+ if(parent) {
1658
+ // Compensate for non-static containers offset
1659
+ do {
1660
+ if(parent.css('position') !== 'static') {
1661
+ coffset = parent.position();
1662
+
1663
+ // Account for element positioning, borders and margins
1664
+ pos.left -= coffset.left + (parseInt(parent.css('borderLeftWidth'), 10) || 0) + (parseInt(parent.css('marginLeft'), 10) || 0);
1665
+ pos.top -= coffset.top + (parseInt(parent.css('borderTopWidth'), 10) || 0) + (parseInt(parent.css('marginTop'), 10) || 0);
1666
+
1667
+ // If this is the first parent element with an overflow of "scroll" or "auto", store it
1668
+ if(!scrolled && (overflow = parent.css('overflow')) !== 'hidden' && overflow !== 'visible') { scrolled = parent; }
1669
+ }
1670
+ }
1671
+ while((parent = $(parent[0].offsetParent)).length);
1672
+
1673
+ // Compensate for containers scroll if it also has an offsetParent
1674
+ if(scrolled && scrolled[0] !== docBody) { scroll( scrolled, 1 ); }
1675
+ }
1676
+
1677
+ return pos;
1678
+ },
1679
+
1680
+ /*
1681
+ * iOS version detection
1682
+ */
1683
+ iOS: parseFloat(
1684
+ ('' + (/CPU.*OS ([0-9_]{1,5})|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent) || [0,''])[1])
1685
+ .replace('undefined', '3_2').replace('_', '.').replace('_', '')
1686
+ ) || FALSE,
1687
+
1688
+ /*
1689
+ * jQuery-specific $.fn overrides
1690
+ */
1691
+ fn: {
1692
+ /* Allow other plugins to successfully retrieve the title of an element with a qTip applied */
1693
+ attr: function(attr, val) {
1694
+ if(this.length) {
1695
+ var self = this[0],
1696
+ title = 'title',
1697
+ api = $.data(self, 'qtip');
1698
+
1699
+ if(attr === title && api && 'object' === typeof api && api.options.suppress) {
1700
+ if(arguments.length < 2) {
1701
+ return $.attr(self, oldtitle);
1702
+ }
1703
+
1704
+ // If qTip is rendered and title was originally used as content, update it
1705
+ if(api && api.options.content.attr === title && api.cache.attr) {
1706
+ api.set('content.text', val);
1707
+ }
1708
+
1709
+ // Use the regular attr method to set, then cache the result
1710
+ return this.attr(oldtitle, val);
1711
+ }
1712
+ }
1713
+
1714
+ return $.fn['attr'+replaceSuffix].apply(this, arguments);
1715
+ },
1716
+
1717
+ /* Allow clone to correctly retrieve cached title attributes */
1718
+ clone: function(keepData) {
1719
+ var titles = $([]), title = 'title',
1720
+
1721
+ // Clone our element using the real clone method
1722
+ elems = $.fn['clone'+replaceSuffix].apply(this, arguments);
1723
+
1724
+ // Grab all elements with an oldtitle set, and change it to regular title attribute, if keepData is false
1725
+ if(!keepData) {
1726
+ elems.filter('['+oldtitle+']').attr('title', function() {
1727
+ return $.attr(this, oldtitle);
1728
+ })
1729
+ .removeAttr(oldtitle);
1730
+ }
1731
+
1732
+ return elems;
1733
+ }
1734
+ }
1735
+ };
1736
+
1737
+ // Apply the fn overrides above
1738
+ $.each(PLUGINS.fn, function(name, func) {
1739
+ if(!func || $.fn[name+replaceSuffix]) { return TRUE; }
1740
+
1741
+ var old = $.fn[name+replaceSuffix] = $.fn[name];
1742
+ $.fn[name] = function() {
1743
+ return func.apply(this, arguments) || old.apply(this, arguments);
1744
+ };
1745
+ });
1746
+
1747
+ /* Fire off 'removeqtip' handler in $.cleanData if jQuery UI not present (it already does similar).
1748
+ * This snippet is taken directly from jQuery UI source code found here:
1749
+ * http://code.jquery.com/ui/jquery-ui-git.js
1750
+ */
1751
+ if(!$.ui) {
1752
+ $['cleanData'+replaceSuffix] = $.cleanData;
1753
+ $.cleanData = function( elems ) {
1754
+ for(var i = 0, elem; (elem = elems[i]) !== undefined; i++) {
1755
+ try { $( elem ).triggerHandler('removeqtip'); }
1756
+ catch( e ) {}
1757
+ }
1758
+ $['cleanData'+replaceSuffix]( elems );
1759
+ };
1760
+ }
1761
+
1762
+ // Set global qTip properties
1763
+ QTIP.version = '@VERSION';
1764
+ QTIP.nextid = 0;
1765
+ QTIP.inactiveEvents = 'click dblclick mousedown mouseup mousemove mouseleave mouseenter'.split(' ');
1766
+ QTIP.zindex = 15000;
1767
+
1768
+ // Define configuration defaults
1769
+ QTIP.defaults = {
1770
+ prerender: FALSE,
1771
+ id: FALSE,
1772
+ overwrite: TRUE,
1773
+ suppress: TRUE,
1774
+ content: {
1775
+ text: TRUE,
1776
+ attr: 'title',
1777
+ title: {
1778
+ text: FALSE,
1779
+ button: FALSE
1780
+ }
1781
+ },
1782
+ position: {
1783
+ my: 'top left',
1784
+ at: 'bottom right',
1785
+ target: FALSE,
1786
+ container: FALSE,
1787
+ viewport: FALSE,
1788
+ adjust: {
1789
+ x: 0, y: 0,
1790
+ mouse: TRUE,
1791
+ resize: TRUE,
1792
+ method: 'flip flip'
1793
+ },
1794
+ effect: function(api, pos, viewport) {
1795
+ $(this).animate(pos, {
1796
+ duration: 200,
1797
+ queue: FALSE
1798
+ });
1799
+ }
1800
+ },
1801
+ show: {
1802
+ target: FALSE,
1803
+ event: 'mouseenter',
1804
+ effect: TRUE,
1805
+ delay: 90,
1806
+ solo: FALSE,
1807
+ ready: FALSE,
1808
+ autofocus: FALSE
1809
+ },
1810
+ hide: {
1811
+ target: FALSE,
1812
+ event: 'mouseleave',
1813
+ effect: TRUE,
1814
+ delay: 0,
1815
+ fixed: FALSE,
1816
+ inactive: FALSE,
1817
+ leave: 'window',
1818
+ distance: FALSE
1819
+ },
1820
+ style: {
1821
+ classes: '',
1822
+ widget: FALSE,
1823
+ width: FALSE,
1824
+ height: FALSE,
1825
+ def: TRUE
1826
+ },
1827
+ events: {
1828
+ render: NULL,
1829
+ move: NULL,
1830
+ show: NULL,
1831
+ hide: NULL,
1832
+ toggle: NULL,
1833
+ visible: NULL,
1834
+ hidden: NULL,
1835
+ focus: NULL,
1836
+ blur: NULL
1837
+ }
1838
+ };
1839
+
1840
+
1841
+ PLUGINS.svg = function(api, svg, corner, adjustMethod)
1842
+ {
1843
+ var doc = $(document),
1844
+ elem = svg[0],
1845
+ result = {
1846
+ width: 0, height: 0,
1847
+ position: { top: 1e10, left: 1e10 }
1848
+ },
1849
+ box, mtx, root, point, tPoint;
1850
+
1851
+ // Ascend the parentNode chain until we find an element with getBBox()
1852
+ while(!elem.getBBox) { elem = elem.parentNode; }
1853
+
1854
+ // Check for a valid bounding box method
1855
+ if (elem.getBBox && elem.parentNode) {
1856
+ box = elem.getBBox();
1857
+ mtx = elem.getScreenCTM();
1858
+ root = elem.farthestViewportElement || elem;
1859
+
1860
+ // Return if no method is found
1861
+ if(!root.createSVGPoint) { return result; }
1862
+
1863
+ // Create our point var
1864
+ point = root.createSVGPoint();
1865
+
1866
+ // Adjust top and left
1867
+ point.x = box.x;
1868
+ point.y = box.y;
1869
+ tPoint = point.matrixTransform(mtx);
1870
+ result.position.left = tPoint.x;
1871
+ result.position.top = tPoint.y;
1872
+
1873
+ // Adjust width and height
1874
+ point.x += box.width;
1875
+ point.y += box.height;
1876
+ tPoint = point.matrixTransform(mtx);
1877
+ result.width = tPoint.x - result.position.left;
1878
+ result.height = tPoint.y - result.position.top;
1879
+
1880
+ // Adjust by scroll offset
1881
+ result.position.left += doc.scrollLeft();
1882
+ result.position.top += doc.scrollTop();
1883
+ }
1884
+
1885
+ return result;
1886
+ };
1887
+
1888
+
1889
+ function Ajax(api)
1890
+ {
1891
+ var self = this,
1892
+ tooltip = api.elements.tooltip,
1893
+ opts = api.options.content.ajax,
1894
+ defaults = QTIP.defaults.content.ajax,
1895
+ namespace = '.qtip-ajax',
1896
+ rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
1897
+ first = TRUE,
1898
+ stop = FALSE,
1899
+ xhr;
1900
+
1901
+ api.checks.ajax = {
1902
+ '^content.ajax': function(obj, name, v) {
1903
+ // If content.ajax object was reset, set our local var
1904
+ if(name === 'ajax') { opts = v; }
1905
+
1906
+ if(name === 'once') {
1907
+ self.init();
1908
+ }
1909
+ else if(opts && opts.url) {
1910
+ self.load();
1911
+ }
1912
+ else {
1913
+ tooltip.unbind(namespace);
1914
+ }
1915
+ }
1916
+ };
1917
+
1918
+ $.extend(self, {
1919
+ init: function() {
1920
+ // Make sure ajax options are enabled and bind event
1921
+ if(opts && opts.url) {
1922
+ tooltip.unbind(namespace)[ opts.once ? 'one' : 'bind' ]('tooltipshow'+namespace, self.load);
1923
+ }
1924
+
1925
+ return self;
1926
+ },
1927
+
1928
+ load: function(event) {
1929
+ if(stop) {stop = FALSE; return; }
1930
+
1931
+ var hasSelector = opts.url.lastIndexOf(' '),
1932
+ url = opts.url,
1933
+ selector,
1934
+ hideFirst = !opts.loading && first;
1935
+
1936
+ // If loading option is disabled, prevent the tooltip showing until we've completed the request
1937
+ if(hideFirst) { try{ event.preventDefault(); } catch(e) {} }
1938
+
1939
+ // Make sure default event hasn't been prevented
1940
+ else if(event && event.isDefaultPrevented()) { return self; }
1941
+
1942
+ // Cancel old request
1943
+ if(xhr && xhr.abort) { xhr.abort(); }
1944
+
1945
+ // Check if user delcared a content selector like in .load()
1946
+ if(hasSelector > -1) {
1947
+ selector = url.substr(hasSelector);
1948
+ url = url.substr(0, hasSelector);
1949
+ }
1950
+
1951
+ // Define common after callback for both success/error handlers
1952
+ function after() {
1953
+ var complete;
1954
+
1955
+ // Don't proceed if tooltip is destroyed
1956
+ if(api.destroyed) { return; }
1957
+
1958
+ // Set first flag to false
1959
+ first = FALSE;
1960
+
1961
+ // Re-display tip if loading and first time, and reset first flag
1962
+ if(hideFirst) { stop = TRUE; api.show(event.originalEvent); }
1963
+
1964
+ // Call users complete method if it was defined
1965
+ if((complete = defaults.complete || opts.complete) && $.isFunction(complete)) {
1966
+ complete.apply(opts.context || api, arguments);
1967
+ }
1968
+ }
1969
+
1970
+ // Define success handler
1971
+ function successHandler(content, status, jqXHR) {
1972
+ var success;
1973
+
1974
+ // Don't proceed if tooltip is destroyed
1975
+ if(api.destroyed) { return; }
1976
+
1977
+ // If URL contains a selector
1978
+ if(selector && 'string' === typeof content) {
1979
+ // Create a dummy div to hold the results and grab the selector element
1980
+ content = $('<div/>')
1981
+ // inject the contents of the document in, removing the scripts
1982
+ // to avoid any 'Permission Denied' errors in IE
1983
+ .append(content.replace(rscript, ""))
1984
+
1985
+ // Locate the specified elements
1986
+ .find(selector);
1987
+ }
1988
+
1989
+ // Call the success function if one is defined
1990
+ if((success = defaults.success || opts.success) && $.isFunction(success)) {
1991
+ success.call(opts.context || api, content, status, jqXHR);
1992
+ }
1993
+
1994
+ // Otherwise set the content
1995
+ else { api.set('content.text', content); }
1996
+ }
1997
+
1998
+ // Error handler
1999
+ function errorHandler(xhr, status, error) {
2000
+ if(api.destroyed || xhr.status === 0) { return; }
2001
+ api.set('content.text', status + ': ' + error);
2002
+ }
2003
+
2004
+ // Setup $.ajax option object and process the request
2005
+ xhr = $.ajax(
2006
+ $.extend({
2007
+ error: defaults.error || errorHandler,
2008
+ context: api
2009
+ },
2010
+ opts, { url: url, success: successHandler, complete: after })
2011
+ );
2012
+ },
2013
+
2014
+ destroy: function() {
2015
+ // Cancel ajax request if possible
2016
+ if(xhr && xhr.abort) { xhr.abort(); }
2017
+
2018
+ // Set api.destroyed flag
2019
+ api.destroyed = TRUE;
2020
+ }
2021
+ });
2022
+
2023
+ self.init();
2024
+ }
2025
+
2026
+
2027
+ PLUGINS.ajax = function(api)
2028
+ {
2029
+ var self = api.plugins.ajax;
2030
+
2031
+ return 'object' === typeof self ? self : (api.plugins.ajax = new Ajax(api));
2032
+ };
2033
+
2034
+ PLUGINS.ajax.initialize = 'render';
2035
+
2036
+ // Setup plugin sanitization
2037
+ PLUGINS.ajax.sanitize = function(options)
2038
+ {
2039
+ var content = options.content, opts;
2040
+ if(content && 'ajax' in content) {
2041
+ opts = content.ajax;
2042
+ if(typeof opts !== 'object') { opts = options.content.ajax = { url: opts }; }
2043
+ if('boolean' !== typeof opts.once && opts.once) { opts.once = !!opts.once; }
2044
+ }
2045
+ };
2046
+
2047
+ // Extend original api defaults
2048
+ $.extend(TRUE, QTIP.defaults, {
2049
+ content: {
2050
+ ajax: {
2051
+ loading: TRUE,
2052
+ once: TRUE
2053
+ }
2054
+ }
2055
+ });
2056
+
2057
+
2058
+ // Tip coordinates calculator
2059
+ function calculateTip(corner, width, height)
2060
+ {
2061
+ var width2 = Math.ceil(width / 2), height2 = Math.ceil(height / 2),
2062
+
2063
+ // Define tip coordinates in terms of height and width values
2064
+ tips = {
2065
+ bottomright: [[0,0], [width,height], [width,0]],
2066
+ bottomleft: [[0,0], [width,0], [0,height]],
2067
+ topright: [[0,height], [width,0], [width,height]],
2068
+ topleft: [[0,0], [0,height], [width,height]],
2069
+ topcenter: [[0,height], [width2,0], [width,height]],
2070
+ bottomcenter: [[0,0], [width,0], [width2,height]],
2071
+ rightcenter: [[0,0], [width,height2], [0,height]],
2072
+ leftcenter: [[width,0], [width,height], [0,height2]]
2073
+ };
2074
+
2075
+ // Set common side shapes
2076
+ tips.lefttop = tips.bottomright; tips.righttop = tips.bottomleft;
2077
+ tips.leftbottom = tips.topright; tips.rightbottom = tips.topleft;
2078
+
2079
+ return tips[ corner.string() ];
2080
+ }
2081
+
2082
+
2083
+ function Tip(qTip, command)
2084
+ {
2085
+ var self = this,
2086
+ opts = qTip.options.style.tip,
2087
+ elems = qTip.elements,
2088
+ tooltip = elems.tooltip,
2089
+ cache = { top: 0, left: 0 },
2090
+ size = {
2091
+ width: opts.width,
2092
+ height: opts.height
2093
+ },
2094
+ color = { },
2095
+ border = opts.border || 0,
2096
+ namespace = '.qtip-tip',
2097
+ hasCanvas = !!($('<canvas />')[0] || {}).getContext,
2098
+ tiphtml;
2099
+
2100
+ self.corner = NULL;
2101
+ self.mimic = NULL;
2102
+ self.border = border;
2103
+ self.offset = opts.offset;
2104
+ self.size = size;
2105
+
2106
+ // Add new option checks for the plugin
2107
+ qTip.checks.tip = {
2108
+ '^position.my|style.tip.(corner|mimic|border)$': function() {
2109
+ // Make sure a tip can be drawn
2110
+ if(!self.init()) {
2111
+ self.destroy();
2112
+ }
2113
+
2114
+ // Reposition the tooltip
2115
+ qTip.reposition();
2116
+ },
2117
+ '^style.tip.(height|width)$': function() {
2118
+ // Re-set dimensions and redraw the tip
2119
+ size = {
2120
+ width: opts.width,
2121
+ height: opts.height
2122
+ };
2123
+ self.create();
2124
+ self.update();
2125
+
2126
+ // Reposition the tooltip
2127
+ qTip.reposition();
2128
+ },
2129
+ '^content.title.text|style.(classes|widget)$': function() {
2130
+ if(elems.tip && elems.tip.length) {
2131
+ self.update();
2132
+ }
2133
+ }
2134
+ };
2135
+
2136
+ function whileVisible(callback) {
2137
+ var visible = tooltip.is(':visible');
2138
+ tooltip.show(); callback(); tooltip.toggle(visible);
2139
+ }
2140
+
2141
+ function swapDimensions() {
2142
+ size.width = opts.height;
2143
+ size.height = opts.width;
2144
+ }
2145
+
2146
+ function resetDimensions() {
2147
+ size.width = opts.width;
2148
+ size.height = opts.height;
2149
+ }
2150
+
2151
+ function reposition(event, api, pos, viewport) {
2152
+ if(!elems.tip) { return; }
2153
+
2154
+ var newCorner = self.corner.clone(),
2155
+ adjust = pos.adjusted,
2156
+ method = qTip.options.position.adjust.method.split(' '),
2157
+ horizontal = method[0],
2158
+ vertical = method[1] || method[0],
2159
+ shift = { left: FALSE, top: FALSE, x: 0, y: 0 },
2160
+ offset, css = {}, props;
2161
+
2162
+ // If our tip position isn't fixed e.g. doesn't adjust with viewport...
2163
+ if(self.corner.fixed !== TRUE) {
2164
+ // Horizontal - Shift or flip method
2165
+ if(horizontal === SHIFT && newCorner.precedance === X && adjust.left && newCorner.y !== CENTER) {
2166
+ newCorner.precedance = newCorner.precedance === X ? Y : X;
2167
+ }
2168
+ else if(horizontal !== SHIFT && adjust.left){
2169
+ newCorner.x = newCorner.x === CENTER ? (adjust.left > 0 ? LEFT : RIGHT) : (newCorner.x === LEFT ? RIGHT : LEFT);
2170
+ }
2171
+
2172
+ // Vertical - Shift or flip method
2173
+ if(vertical === SHIFT && newCorner.precedance === Y && adjust.top && newCorner.x !== CENTER) {
2174
+ newCorner.precedance = newCorner.precedance === Y ? X : Y;
2175
+ }
2176
+ else if(vertical !== SHIFT && adjust.top) {
2177
+ newCorner.y = newCorner.y === CENTER ? (adjust.top > 0 ? TOP : BOTTOM) : (newCorner.y === TOP ? BOTTOM : TOP);
2178
+ }
2179
+
2180
+ // Update and redraw the tip if needed (check cached details of last drawn tip)
2181
+ if(newCorner.string() !== cache.corner.string() && (cache.top !== adjust.top || cache.left !== adjust.left)) {
2182
+ self.update(newCorner, FALSE);
2183
+ }
2184
+ }
2185
+
2186
+ // Setup tip offset properties
2187
+ offset = self.position(newCorner, adjust);
2188
+ offset[ newCorner.x ] += parseWidth(newCorner, newCorner.x);
2189
+ offset[ newCorner.y ] += parseWidth(newCorner, newCorner.y);
2190
+
2191
+ // Readjust offset object to make it left/top
2192
+ if(offset.right !== undefined) { offset.left = -offset.right; }
2193
+ if(offset.bottom !== undefined) { offset.top = -offset.bottom; }
2194
+ offset.user = Math.max(0, opts.offset);
2195
+
2196
+ // Viewport "shift" specific adjustments
2197
+ if(shift.left = (horizontal === SHIFT && !!adjust.left)) {
2198
+ if(newCorner.x === CENTER) {
2199
+ css['margin-left'] = shift.x = offset['margin-left'] - adjust.left;
2200
+ }
2201
+ else {
2202
+ props = offset.right !== undefined ?
2203
+ [ adjust.left, -offset.left ] : [ -adjust.left, offset.left ];
2204
+
2205
+ if( (shift.x = Math.max(props[0], props[1])) > props[0] ) {
2206
+ pos.left -= adjust.left;
2207
+ shift.left = FALSE;
2208
+ }
2209
+
2210
+ css[ offset.right !== undefined ? RIGHT : LEFT ] = shift.x;
2211
+ }
2212
+ }
2213
+ if(shift.top = (vertical === SHIFT && !!adjust.top)) {
2214
+ if(newCorner.y === CENTER) {
2215
+ css['margin-top'] = shift.y = offset['margin-top'] - adjust.top;
2216
+ }
2217
+ else {
2218
+ props = offset.bottom !== undefined ?
2219
+ [ adjust.top, -offset.top ] : [ -adjust.top, offset.top ];
2220
+
2221
+ if( (shift.y = Math.max(props[0], props[1])) > props[0] ) {
2222
+ pos.top -= adjust.top;
2223
+ shift.top = FALSE;
2224
+ }
2225
+
2226
+ css[ offset.bottom !== undefined ? BOTTOM : TOP ] = shift.y;
2227
+ }
2228
+ }
2229
+
2230
+ /*
2231
+ * If the tip is adjusted in both dimensions, or in a
2232
+ * direction that would cause it to be anywhere but the
2233
+ * outer border, hide it!
2234
+ */
2235
+ elems.tip.css(css).toggle(
2236
+ !((shift.x && shift.y) || (newCorner.x === CENTER && shift.y) || (newCorner.y === CENTER && shift.x))
2237
+ );
2238
+
2239
+ // Adjust position to accomodate tip dimensions
2240
+ pos.left -= offset.left.charAt ? offset.user : horizontal !== SHIFT || shift.top || !shift.left && !shift.top ? offset.left : 0;
2241
+ pos.top -= offset.top.charAt ? offset.user : vertical !== SHIFT || shift.left || !shift.left && !shift.top ? offset.top : 0;
2242
+
2243
+ // Cache details
2244
+ cache.left = adjust.left; cache.top = adjust.top;
2245
+ cache.corner = newCorner.clone();
2246
+ }
2247
+
2248
+ function parseCorner() {
2249
+ var corner = opts.corner,
2250
+ posOptions = qTip.options.position,
2251
+ at = posOptions.at,
2252
+ my = posOptions.my.string ? posOptions.my.string() : posOptions.my;
2253
+
2254
+ // Detect corner and mimic properties
2255
+ if(corner === FALSE || (my === FALSE && at === FALSE)) {
2256
+ return FALSE;
2257
+ }
2258
+ else {
2259
+ if(corner === TRUE) {
2260
+ self.corner = new PLUGINS.Corner(my);
2261
+ }
2262
+ else if(!corner.string) {
2263
+ self.corner = new PLUGINS.Corner(corner);
2264
+ self.corner.fixed = TRUE;
2265
+ }
2266
+ }
2267
+
2268
+ // Cache it
2269
+ cache.corner = new PLUGINS.Corner( self.corner.string() );
2270
+
2271
+ return self.corner.string() !== 'centercenter';
2272
+ }
2273
+
2274
+ /* border width calculator */
2275
+ function parseWidth(corner, side, use) {
2276
+ side = !side ? corner[corner.precedance] : side;
2277
+
2278
+ var isTitleTop = elems.titlebar && corner.y === TOP,
2279
+ elem = isTitleTop ? elems.titlebar : tooltip,
2280
+ borderSide = 'border-' + side + '-width',
2281
+ css = function(elem) { return parseInt(elem.css(borderSide), 10); },
2282
+ val;
2283
+
2284
+ // Grab the border-width value (make tooltip visible first)
2285
+ whileVisible(function() {
2286
+ val = (use ? css(use) : (css(elems.content) || css(elem) || css(tooltip))) || 0;
2287
+ });
2288
+ return val;
2289
+ }
2290
+
2291
+ function parseRadius(corner) {
2292
+ var isTitleTop = elems.titlebar && corner.y === TOP,
2293
+ elem = isTitleTop ? elems.titlebar : elems.content,
2294
+ moz = $.browser.mozilla,
2295
+ prefix = moz ? '-moz-' : $.browser.webkit ? '-webkit-' : '',
2296
+ nonStandard = 'border-radius-' + corner.y + corner.x,
2297
+ standard = 'border-' + corner.y + '-' + corner.x + '-radius',
2298
+ css = function(c) { return parseInt(elem.css(c), 10) || parseInt(tooltip.css(c), 10); },
2299
+ val;
2300
+
2301
+ whileVisible(function() {
2302
+ val = css(standard) || css(prefix + standard) || css(prefix + nonStandard) || css(nonStandard) || 0;
2303
+ });
2304
+ return val;
2305
+ }
2306
+
2307
+ function parseColours(actual) {
2308
+ var i, fill, border,
2309
+ tip = elems.tip.css('cssText', ''),
2310
+ corner = actual || self.corner,
2311
+ invalid = /rgba?\(0, 0, 0(, 0)?\)|transparent|#123456/i,
2312
+ borderSide = 'border-' + corner[ corner.precedance ] + '-color',
2313
+ bgColor = 'background-color',
2314
+ transparent = 'transparent',
2315
+ important = ' !important',
2316
+
2317
+ titlebar = elems.titlebar,
2318
+ useTitle = titlebar && (corner.y === TOP || (corner.y === CENTER && tip.position().top + (size.height / 2) + opts.offset < titlebar.outerHeight(TRUE))),
2319
+ colorElem = useTitle ? titlebar : elems.content;
2320
+
2321
+ function css(elem, prop, compare) {
2322
+ var val = elem.css(prop) || transparent;
2323
+ if(compare && val === elem.css(compare)) { return FALSE; }
2324
+ else { return invalid.test(val) ? FALSE : val; }
2325
+ }
2326
+
2327
+ // Ensure tooltip is visible then...
2328
+ whileVisible(function() {
2329
+ // Attempt to detect the background colour from various elements, left-to-right precedance
2330
+ color.fill = css(tip, bgColor) || css(colorElem, bgColor) || css(elems.content, bgColor) ||
2331
+ css(tooltip, bgColor) || tip.css(bgColor);
2332
+
2333
+ // Attempt to detect the correct border side colour from various elements, left-to-right precedance
2334
+ color.border = css(tip, borderSide, 'color') || css(colorElem, borderSide, 'color') ||
2335
+ css(elems.content, borderSide, 'color') || css(tooltip, borderSide, 'color') || tooltip.css(borderSide);
2336
+
2337
+ // Reset background and border colours
2338
+ $('*', tip).add(tip).css('cssText', bgColor+':'+transparent+important+';border:0'+important+';');
2339
+ });
2340
+ }
2341
+
2342
+ function calculateSize(corner) {
2343
+ var y = corner.precedance === Y,
2344
+ width = size [ y ? WIDTH : HEIGHT ],
2345
+ height = size [ y ? HEIGHT : WIDTH ],
2346
+ isCenter = corner.string().indexOf(CENTER) > -1,
2347
+ base = width * (isCenter ? 0.5 : 1),
2348
+ pow = Math.pow,
2349
+ round = Math.round,
2350
+ bigHyp, ratio, result,
2351
+
2352
+ smallHyp = Math.sqrt( pow(base, 2) + pow(height, 2) ),
2353
+
2354
+ hyp = [
2355
+ (border / base) * smallHyp, (border / height) * smallHyp
2356
+ ];
2357
+ hyp[2] = Math.sqrt( pow(hyp[0], 2) - pow(border, 2) );
2358
+ hyp[3] = Math.sqrt( pow(hyp[1], 2) - pow(border, 2) );
2359
+
2360
+ bigHyp = smallHyp + hyp[2] + hyp[3] + (isCenter ? 0 : hyp[0]);
2361
+ ratio = bigHyp / smallHyp;
2362
+
2363
+ result = [ round(ratio * height), round(ratio * width) ];
2364
+ return { height: result[ y ? 0 : 1 ], width: result[ y ? 1 : 0 ] };
2365
+ }
2366
+
2367
+ function createVML(tag, props, style) {
2368
+ return '<qvml:'+tag+' xmlns="urn:schemas-microsoft.com:vml" class="qtip-vml" '+(props||'')+
2369
+ ' style="behavior: url(#default#VML); '+(style||'')+ '" />';
2370
+ }
2371
+
2372
+ $.extend(self, {
2373
+ init: function()
2374
+ {
2375
+ var enabled = parseCorner() && (hasCanvas || $.browser.msie);
2376
+
2377
+ // Determine tip corner and type
2378
+ if(enabled) {
2379
+ // Create a new tip and draw it
2380
+ self.create();
2381
+ self.update();
2382
+
2383
+ // Bind update events
2384
+ tooltip.unbind(namespace).bind('tooltipmove'+namespace, reposition);
2385
+
2386
+ // Fix for issue of tips not showing after redraw in IE (VML...)
2387
+ if(!hasCanvas) {
2388
+ tooltip.bind('tooltipredraw tooltipredrawn', function(event) {
2389
+ if(event.type === 'tooltipredraw') {
2390
+ tiphtml = elems.tip.html();
2391
+ elems.tip.html('');
2392
+ }
2393
+ else { elems.tip.html(tiphtml); }
2394
+ });
2395
+ }
2396
+ }
2397
+
2398
+ return enabled;
2399
+ },
2400
+
2401
+ create: function()
2402
+ {
2403
+ var width = size.width,
2404
+ height = size.height,
2405
+ vml;
2406
+
2407
+ // Remove previous tip element if present
2408
+ if(elems.tip) { elems.tip.remove(); }
2409
+
2410
+ // Create tip element and prepend to the tooltip
2411
+ elems.tip = $('<div />', { 'class': 'ui-tooltip-tip' }).css({ width: width, height: height }).prependTo(tooltip);
2412
+
2413
+ // Create tip drawing element(s)
2414
+ if(hasCanvas) {
2415
+ // save() as soon as we create the canvas element so FF2 doesn't bork on our first restore()!
2416
+ $('<canvas />').appendTo(elems.tip)[0].getContext('2d').save();
2417
+ }
2418
+ else {
2419
+ vml = createVML('shape', 'coordorigin="0,0"', 'position:absolute;');
2420
+ elems.tip.html(vml + vml);
2421
+
2422
+ // Prevent mousing down on the tip since it causes problems with .live() handling in IE due to VML
2423
+ $('*', elems.tip).bind('click mousedown', function(event) { event.stopPropagation(); });
2424
+ }
2425
+ },
2426
+
2427
+ update: function(corner, position)
2428
+ {
2429
+ var tip = elems.tip,
2430
+ inner = tip.children(),
2431
+ width = size.width,
2432
+ height = size.height,
2433
+ mimic = opts.mimic,
2434
+ round = Math.round,
2435
+ precedance, context, coords, translate, newSize;
2436
+
2437
+ // Re-determine tip if not already set
2438
+ if(!corner) { corner = cache.corner || self.corner; }
2439
+
2440
+ // Use corner property if we detect an invalid mimic value
2441
+ if(mimic === FALSE) { mimic = corner; }
2442
+
2443
+ // Otherwise inherit mimic properties from the corner object as necessary
2444
+ else {
2445
+ mimic = new PLUGINS.Corner(mimic);
2446
+ mimic.precedance = corner.precedance;
2447
+
2448
+ if(mimic.x === 'inherit') { mimic.x = corner.x; }
2449
+ else if(mimic.y === 'inherit') { mimic.y = corner.y; }
2450
+ else if(mimic.x === mimic.y) {
2451
+ mimic[ corner.precedance ] = corner[ corner.precedance ];
2452
+ }
2453
+ }
2454
+ precedance = mimic.precedance;
2455
+
2456
+ // Ensure the tip width.height are relative to the tip position
2457
+ if(corner.precedance === X) { swapDimensions(); }
2458
+ else { resetDimensions(); }
2459
+
2460
+ // Set the tip dimensions
2461
+ elems.tip.css({
2462
+ width: (width = size.width),
2463
+ height: (height = size.height)
2464
+ });
2465
+
2466
+ // Update our colours
2467
+ parseColours(corner);
2468
+
2469
+ // Detect border width, taking into account colours
2470
+ if(color.border !== 'transparent') {
2471
+ // Grab border width
2472
+ border = parseWidth(corner, NULL);
2473
+
2474
+ // If border width isn't zero, use border color as fill (1.0 style tips)
2475
+ if(opts.border === 0 && border > 0) { color.fill = color.border; }
2476
+
2477
+ // Set border width (use detected border width if opts.border is true)
2478
+ self.border = border = opts.border !== TRUE ? opts.border : border;
2479
+ }
2480
+
2481
+ // Border colour was invalid, set border to zero
2482
+ else { self.border = border = 0; }
2483
+
2484
+ // Calculate coordinates
2485
+ coords = calculateTip(mimic, width , height);
2486
+
2487
+ // Determine tip size
2488
+ self.size = newSize = calculateSize(corner);
2489
+ tip.css(newSize);
2490
+
2491
+ // Calculate tip translation
2492
+ if(corner.precedance === Y) {
2493
+ translate = [
2494
+ round(mimic.x === LEFT ? border : mimic.x === RIGHT ? newSize.width - width - border : (newSize.width - width) / 2),
2495
+ round(mimic.y === TOP ? newSize.height - height : 0)
2496
+ ];
2497
+ }
2498
+ else {
2499
+ translate = [
2500
+ round(mimic.x === LEFT ? newSize.width - width : 0),
2501
+ round(mimic.y === TOP ? border : mimic.y === BOTTOM ? newSize.height - height - border : (newSize.height - height) / 2)
2502
+ ];
2503
+ }
2504
+
2505
+ // Canvas drawing implementation
2506
+ if(hasCanvas) {
2507
+ // Set the canvas size using calculated size
2508
+ inner.attr(newSize);
2509
+
2510
+ // Grab canvas context and clear/save it
2511
+ context = inner[0].getContext('2d');
2512
+ context.restore(); context.save();
2513
+ context.clearRect(0,0,3000,3000);
2514
+
2515
+ // Set properties
2516
+ context.fillStyle = color.fill;
2517
+ context.strokeStyle = color.border;
2518
+ context.lineWidth = border * 2;
2519
+ context.lineJoin = 'miter';
2520
+ context.miterLimit = 100;
2521
+
2522
+ // Translate origin
2523
+ context.translate(translate[0], translate[1]);
2524
+
2525
+ // Draw the tip
2526
+ context.beginPath();
2527
+ context.moveTo(coords[0][0], coords[0][1]);
2528
+ context.lineTo(coords[1][0], coords[1][1]);
2529
+ context.lineTo(coords[2][0], coords[2][1]);
2530
+ context.closePath();
2531
+
2532
+ // Apply fill and border
2533
+ if(border) {
2534
+ // Make sure transparent borders are supported by doing a stroke
2535
+ // of the background colour before the stroke colour
2536
+ if(tooltip.css('background-clip') === 'border-box') {
2537
+ context.strokeStyle = color.fill;
2538
+ context.stroke();
2539
+ }
2540
+ context.strokeStyle = color.border;
2541
+ context.stroke();
2542
+ }
2543
+ context.fill();
2544
+ }
2545
+
2546
+ // VML (IE Proprietary implementation)
2547
+ else {
2548
+ // Setup coordinates string
2549
+ coords = 'm' + coords[0][0] + ',' + coords[0][1] + ' l' + coords[1][0] +
2550
+ ',' + coords[1][1] + ' ' + coords[2][0] + ',' + coords[2][1] + ' xe';
2551
+
2552
+ // Setup VML-specific offset for pixel-perfection
2553
+ translate[2] = border && /^(r|b)/i.test(corner.string()) ?
2554
+ parseFloat($.browser.version, 10) === 8 ? 2 : 1 : 0;
2555
+
2556
+ // Set initial CSS
2557
+ inner.css({
2558
+ coordsize: (width+border) + ' ' + (height+border),
2559
+ antialias: ''+(mimic.string().indexOf(CENTER) > -1),
2560
+ left: translate[0],
2561
+ top: translate[1],
2562
+ width: width + border,
2563
+ height: height + border
2564
+ })
2565
+ .each(function(i) {
2566
+ var $this = $(this);
2567
+
2568
+ // Set shape specific attributes
2569
+ $this[ $this.prop ? 'prop' : 'attr' ]({
2570
+ coordsize: (width+border) + ' ' + (height+border),
2571
+ path: coords,
2572
+ fillcolor: color.fill,
2573
+ filled: !!i,
2574
+ stroked: !i
2575
+ })
2576
+ .toggle(!!(border || i));
2577
+
2578
+ // Check if border is enabled and add stroke element
2579
+ if(!i && $this.html() === '') {
2580
+ $this.html(
2581
+ createVML('stroke', 'weight="'+(border*2)+'px" color="'+color.border+'" miterlimit="1000" joinstyle="miter"')
2582
+ );
2583
+ }
2584
+ });
2585
+ }
2586
+
2587
+ // Position if needed
2588
+ if(position !== FALSE) { self.position(corner); }
2589
+ },
2590
+
2591
+ // Tip positioning method
2592
+ position: function(corner)
2593
+ {
2594
+ var tip = elems.tip,
2595
+ position = {},
2596
+ userOffset = Math.max(0, opts.offset),
2597
+ precedance, dimensions, corners;
2598
+
2599
+ // Return if tips are disabled or tip is not yet rendered
2600
+ if(opts.corner === FALSE || !tip) { return FALSE; }
2601
+
2602
+ // Inherit corner if not provided
2603
+ corner = corner || self.corner;
2604
+ precedance = corner.precedance;
2605
+
2606
+ // Determine which tip dimension to use for adjustment
2607
+ dimensions = calculateSize(corner);
2608
+
2609
+ // Setup corners and offset array
2610
+ corners = [ corner.x, corner.y ];
2611
+ if(precedance === X) { corners.reverse(); }
2612
+
2613
+ // Calculate tip position
2614
+ $.each(corners, function(i, side) {
2615
+ var b, bc, br;
2616
+
2617
+ if(side === CENTER) {
2618
+ b = precedance === Y ? LEFT : TOP;
2619
+ position[ b ] = '50%';
2620
+ position['margin-' + b] = -Math.round(dimensions[ precedance === Y ? WIDTH : HEIGHT ] / 2) + userOffset;
2621
+ }
2622
+ else {
2623
+ b = parseWidth(corner, side);
2624
+ bc = parseWidth(corner, side, elems.content);
2625
+ br = parseRadius(corner);
2626
+
2627
+ position[ side ] = i ? bc : (userOffset + (br > b ? br : -b));
2628
+ }
2629
+ });
2630
+
2631
+ // Adjust for tip dimensions
2632
+ position[ corner[precedance] ] -= dimensions[ precedance === X ? WIDTH : HEIGHT ];
2633
+
2634
+ // Set and return new position
2635
+ tip.css({ top: '', bottom: '', left: '', right: '', margin: '' }).css(position);
2636
+ return position;
2637
+ },
2638
+
2639
+ destroy: function()
2640
+ {
2641
+ // Remove the tip element
2642
+ if(elems.tip) { elems.tip.remove(); }
2643
+ elems.tip = false;
2644
+
2645
+ // Unbind events
2646
+ tooltip.unbind(namespace);
2647
+ }
2648
+ });
2649
+
2650
+ self.init();
2651
+ }
2652
+
2653
+ PLUGINS.tip = function(api)
2654
+ {
2655
+ var self = api.plugins.tip;
2656
+
2657
+ return 'object' === typeof self ? self : (api.plugins.tip = new Tip(api));
2658
+ };
2659
+
2660
+ // Initialize tip on render
2661
+ PLUGINS.tip.initialize = 'render';
2662
+
2663
+ // Setup plugin sanitization options
2664
+ PLUGINS.tip.sanitize = function(options)
2665
+ {
2666
+ var style = options.style, opts;
2667
+ if(style && 'tip' in style) {
2668
+ opts = options.style.tip;
2669
+ if(typeof opts !== 'object'){ options.style.tip = { corner: opts }; }
2670
+ if(!(/string|boolean/i).test(typeof opts['corner'])) { opts['corner'] = TRUE; }
2671
+ if(typeof opts.width !== 'number'){ delete opts.width; }
2672
+ if(typeof opts.height !== 'number'){ delete opts.height; }
2673
+ if(typeof opts.border !== 'number' && opts.border !== TRUE){ delete opts.border; }
2674
+ if(typeof opts.offset !== 'number'){ delete opts.offset; }
2675
+ }
2676
+ };
2677
+
2678
+ // Extend original qTip defaults
2679
+ $.extend(TRUE, QTIP.defaults, {
2680
+ style: {
2681
+ tip: {
2682
+ corner: TRUE,
2683
+ mimic: FALSE,
2684
+ width: 6,
2685
+ height: 6,
2686
+ border: TRUE,
2687
+ offset: 0
2688
+ }
2689
+ }
2690
+ });
2691
+
2692
+
2693
+ function Modal(api)
2694
+ {
2695
+ var self = this,
2696
+ options = api.options.show.modal,
2697
+ elems = api.elements,
2698
+ tooltip = elems.tooltip,
2699
+ overlaySelector = '#qtip-overlay',
2700
+ globalNamespace = '.qtipmodal',
2701
+ namespace = globalNamespace + api.id,
2702
+ attr = 'is-modal-qtip',
2703
+ docBody = $(document.body),
2704
+ focusableSelector = PLUGINS.modal.focusable.join(','),
2705
+ focusableElems = {}, overlay;
2706
+
2707
+ // Setup option set checks
2708
+ api.checks.modal = {
2709
+ '^show.modal.(on|blur)$': function() {
2710
+ // Initialise
2711
+ self.init();
2712
+
2713
+ // Show the modal if not visible already and tooltip is visible
2714
+ elems.overlay.toggle( tooltip.is(':visible') );
2715
+ },
2716
+ '^content.text$': function() {
2717
+ updateFocusable();
2718
+ }
2719
+ };
2720
+
2721
+ function updateFocusable() {
2722
+ focusableElems = $(focusableSelector, tooltip).not('[disabled]').map(function() {
2723
+ return typeof this.focus === 'function' ? this : null;
2724
+ });
2725
+ }
2726
+
2727
+ function focusInputs(blurElems) {
2728
+ // Blurring body element in IE causes window.open windows to unfocus!
2729
+ if(focusableElems.length < 1 && blurElems.length) { blurElems.not('body').blur(); }
2730
+
2731
+ // Focus the inputs
2732
+ else { focusableElems.first().focus(); }
2733
+ }
2734
+
2735
+ function stealFocus(event) {
2736
+ var target = $(event.target),
2737
+ container = target.closest('.qtip'),
2738
+ targetOnTop;
2739
+
2740
+ // Determine if input container target is above this
2741
+ targetOnTop = container.length < 1 ? FALSE :
2742
+ (parseInt(container[0].style.zIndex, 10) > parseInt(tooltip[0].style.zIndex, 10));
2743
+
2744
+ // If we're showing a modal, but focus has landed on an input below
2745
+ // this modal, divert focus to the first visible input in this modal
2746
+ // or if we can't find one... the tooltip itself
2747
+ if(!targetOnTop && ($(event.target).closest(selector)[0] !== tooltip[0])) {
2748
+ focusInputs(target);
2749
+ }
2750
+ }
2751
+
2752
+ $.extend(self, {
2753
+ init: function()
2754
+ {
2755
+ // If modal is disabled... return
2756
+ if(!options.on) { return self; }
2757
+
2758
+ // Create the overlay if needed
2759
+ overlay = self.create();
2760
+
2761
+ // Add unique attribute so we can grab modal tooltips easily via a selector
2762
+ tooltip.attr(attr, TRUE)
2763
+
2764
+ // Set z-index
2765
+ .css('z-index', PLUGINS.modal.zindex + $(selector+'['+attr+']').length)
2766
+
2767
+ // Remove previous bound events in globalNamespace
2768
+ .unbind(globalNamespace).unbind(namespace)
2769
+
2770
+ // Apply our show/hide/focus modal events
2771
+ .bind('tooltipshow'+globalNamespace+' tooltiphide'+globalNamespace, function(event, api, duration) {
2772
+ var oEvent = event.originalEvent;
2773
+
2774
+ // Make sure mouseout doesn't trigger a hide when showing the modal and mousing onto backdrop
2775
+ if(event.target === tooltip[0]) {
2776
+ if(oEvent && event.type === 'tooltiphide' && /mouse(leave|enter)/.test(oEvent.type) && $(oEvent.relatedTarget).closest(overlay[0]).length) {
2777
+ try { event.preventDefault(); } catch(e) {}
2778
+ }
2779
+ else if(!oEvent || (oEvent && !oEvent.solo)) {
2780
+ self[ event.type.replace('tooltip', '') ](event, duration);
2781
+ }
2782
+ }
2783
+ })
2784
+
2785
+ // Adjust modal z-index on tooltip focus
2786
+ .bind('tooltipfocus'+globalNamespace, function(event) {
2787
+ // If focus was cancelled before it reearch us, don't do anything
2788
+ if(event.isDefaultPrevented() || event.target !== tooltip[0]) { return; }
2789
+
2790
+ var qtips = $(selector).filter('['+attr+']'),
2791
+
2792
+ // Keep the modal's lower than other, regular qtips
2793
+ newIndex = PLUGINS.modal.zindex + qtips.length,
2794
+ curIndex = parseInt(tooltip[0].style.zIndex, 10);
2795
+
2796
+ // Set overlay z-index
2797
+ overlay[0].style.zIndex = newIndex - 2;
2798
+
2799
+ // Reduce modal z-index's and keep them properly ordered
2800
+ qtips.each(function() {
2801
+ if(this.style.zIndex > curIndex) {
2802
+ this.style.zIndex -= 1;
2803
+ }
2804
+ });
2805
+
2806
+ // Fire blur event for focused tooltip
2807
+ qtips.end().filter('.' + focusClass).qtip('blur', event.originalEvent);
2808
+
2809
+ // Set the new z-index
2810
+ tooltip.addClass(focusClass)[0].style.zIndex = newIndex;
2811
+
2812
+ // Prevent default handling
2813
+ try { event.preventDefault(); } catch(e) {}
2814
+ })
2815
+
2816
+ // Focus any other visible modals when this one hides
2817
+ .bind('tooltiphide'+globalNamespace, function(event) {
2818
+ if(event.target === tooltip[0]) {
2819
+ $('[' + attr + ']').filter(':visible').not(tooltip).last().qtip('focus', event);
2820
+ }
2821
+ });
2822
+
2823
+ // Apply keyboard "Escape key" close handler
2824
+ if(options.escape) {
2825
+ $(document).unbind(namespace).bind('keydown'+namespace, function(event) {
2826
+ if(event.keyCode === 27 && tooltip.hasClass(focusClass)) {
2827
+ api.hide(event);
2828
+ }
2829
+ });
2830
+ }
2831
+
2832
+ // Apply click handler for blur option
2833
+ if(options.blur) {
2834
+ elems.overlay.unbind(namespace).bind('click'+namespace, function(event) {
2835
+ if(tooltip.hasClass(focusClass)) { api.hide(event); }
2836
+ });
2837
+ }
2838
+
2839
+ // Update focusable elements
2840
+ updateFocusable();
2841
+
2842
+ return self;
2843
+ },
2844
+
2845
+ create: function()
2846
+ {
2847
+ var elem = $(overlaySelector);
2848
+
2849
+ // Return if overlay is already rendered
2850
+ if(elem.length) {
2851
+ // Modal overlay should always be below all tooltips if possible
2852
+ return (elems.overlay = elem.insertAfter( $(selector).last() ));
2853
+ }
2854
+
2855
+ // Create document overlay
2856
+ overlay = elems.overlay = $('<div />', {
2857
+ id: overlaySelector.substr(1),
2858
+ html: '<div></div>',
2859
+ mousedown: function() { return FALSE; }
2860
+ })
2861
+ .hide()
2862
+ .insertAfter( $(selector).last() );
2863
+
2864
+ // Update position on window resize or scroll
2865
+ function resize() {
2866
+ overlay.css({
2867
+ height: $(window).height(),
2868
+ width: $(window).width()
2869
+ });
2870
+ }
2871
+ $(window).unbind(globalNamespace).bind('resize'+globalNamespace, resize);
2872
+ resize(); // Fire it initially too
2873
+
2874
+ return overlay;
2875
+ },
2876
+
2877
+ toggle: function(event, state, duration)
2878
+ {
2879
+ // Make sure default event hasn't been prevented
2880
+ if(event && event.isDefaultPrevented()) { return self; }
2881
+
2882
+ var effect = options.effect,
2883
+ type = state ? 'show': 'hide',
2884
+ visible = overlay.is(':visible'),
2885
+ modals = $('[' + attr + ']').filter(':visible').not(tooltip),
2886
+ zindex;
2887
+
2888
+ // Create our overlay if it isn't present already
2889
+ if(!overlay) { overlay = self.create(); }
2890
+
2891
+ // Prevent modal from conflicting with show.solo, and don't hide backdrop is other modals are visible
2892
+ if((overlay.is(':animated') && visible === state) || (!state && modals.length)) { return self; }
2893
+
2894
+ // State specific...
2895
+ if(state) {
2896
+ // Set position
2897
+ overlay.css({ left: 0, top: 0 });
2898
+
2899
+ // Toggle backdrop cursor style on show
2900
+ overlay.toggleClass('blurs', options.blur);
2901
+
2902
+ // IF the modal can steal the focus
2903
+ if(options.stealfocus !== FALSE) {
2904
+ // Make sure we can't focus anything outside the tooltip
2905
+ docBody.bind('focusin'+namespace, stealFocus);
2906
+
2907
+ // Blur the current item and focus anything in the modal we an
2908
+ focusInputs( $('body :focus') );
2909
+ }
2910
+ }
2911
+ else {
2912
+ // Undelegate focus handler
2913
+ docBody.unbind('focusin'+namespace);
2914
+ }
2915
+
2916
+ // Stop all animations
2917
+ overlay.stop(TRUE, FALSE);
2918
+
2919
+ // Use custom function if provided
2920
+ if($.isFunction(effect)) {
2921
+ effect.call(overlay, state);
2922
+ }
2923
+
2924
+ // If no effect type is supplied, use a simple toggle
2925
+ else if(effect === FALSE) {
2926
+ overlay[ type ]();
2927
+ }
2928
+
2929
+ // Use basic fade function
2930
+ else {
2931
+ overlay.fadeTo( parseInt(duration, 10) || 90, state ? 1 : 0, function() {
2932
+ if(!state) { $(this).hide(); }
2933
+ });
2934
+ }
2935
+
2936
+ // Reset position on hide
2937
+ if(!state) {
2938
+ overlay.queue(function(next) {
2939
+ overlay.css({ left: '', top: '' });
2940
+ next();
2941
+ });
2942
+ }
2943
+
2944
+ return self;
2945
+ },
2946
+
2947
+ show: function(event, duration) { return self.toggle(event, TRUE, duration); },
2948
+ hide: function(event, duration) { return self.toggle(event, FALSE, duration); },
2949
+
2950
+ destroy: function()
2951
+ {
2952
+ var delBlanket = overlay;
2953
+
2954
+ if(delBlanket) {
2955
+ // Check if any other modal tooltips are present
2956
+ delBlanket = $('[' + attr + ']').not(tooltip).length < 1;
2957
+
2958
+ // Remove overlay if needed
2959
+ if(delBlanket) {
2960
+ elems.overlay.remove();
2961
+ $(document).unbind(globalNamespace);
2962
+ }
2963
+ else {
2964
+ elems.overlay.unbind(globalNamespace+api.id);
2965
+ }
2966
+
2967
+ // Undelegate focus handler
2968
+ docBody.undelegate('*', 'focusin'+namespace);
2969
+ }
2970
+
2971
+ // Remove bound events
2972
+ return tooltip.removeAttr(attr).unbind(globalNamespace);
2973
+ }
2974
+ });
2975
+
2976
+ self.init();
2977
+ }
2978
+
2979
+ PLUGINS.modal = function(api) {
2980
+ var self = api.plugins.modal;
2981
+
2982
+ return 'object' === typeof self ? self : (api.plugins.modal = new Modal(api));
2983
+ };
2984
+
2985
+ // Plugin needs to be initialized on render
2986
+ PLUGINS.modal.initialize = 'render';
2987
+
2988
+ // Setup sanitiztion rules
2989
+ PLUGINS.modal.sanitize = function(opts) {
2990
+ if(opts.show) {
2991
+ if(typeof opts.show.modal !== 'object') { opts.show.modal = { on: !!opts.show.modal }; }
2992
+ else if(typeof opts.show.modal.on === 'undefined') { opts.show.modal.on = TRUE; }
2993
+ }
2994
+ };
2995
+
2996
+ // Base z-index for all modal tooltips (use qTip core z-index as a base)
2997
+ PLUGINS.modal.zindex = QTIP.zindex - 200;
2998
+
2999
+ // Defines the selector used to select all 'focusable' elements within the modal when using the show.modal.stealfocus option.
3000
+ // Selectors initially taken from http://stackoverflow.com/questions/7668525/is-there-a-jquery-selector-to-get-all-elements-that-can-get-focus
3001
+ PLUGINS.modal.focusable = ['a[href]', 'area[href]', 'input', 'select', 'textarea', 'button', 'iframe', 'object', 'embed', '[tabindex]', '[contenteditable]'];
3002
+
3003
+ // Extend original api defaults
3004
+ $.extend(TRUE, QTIP.defaults, {
3005
+ show: {
3006
+ modal: {
3007
+ on: FALSE,
3008
+ effect: TRUE,
3009
+ blur: TRUE,
3010
+ stealfocus: TRUE,
3011
+ escape: TRUE
3012
+ }
3013
+ }
3014
+ });
3015
+
3016
+
3017
+ PLUGINS.viewport = function(api, position, posOptions, targetWidth, targetHeight, elemWidth, elemHeight)
3018
+ {
3019
+ var target = posOptions.target,
3020
+ tooltip = api.elements.tooltip,
3021
+ my = posOptions.my,
3022
+ at = posOptions.at,
3023
+ adjust = posOptions.adjust,
3024
+ method = adjust.method.split(' '),
3025
+ methodX = method[0],
3026
+ methodY = method[1] || method[0],
3027
+ viewport = posOptions.viewport,
3028
+ container = posOptions.container,
3029
+ cache = api.cache,
3030
+ tip = api.plugins.tip,
3031
+ adjusted = { left: 0, top: 0 },
3032
+ fixed, newMy, newClass;
3033
+
3034
+ // If viewport is not a jQuery element, or it's the window/document or no adjustment method is used... return
3035
+ if(!viewport.jquery || target[0] === window || target[0] === document.body || adjust.method === 'none') {
3036
+ return adjusted;
3037
+ }
3038
+
3039
+ // Cache our viewport details
3040
+ fixed = tooltip.css('position') === 'fixed';
3041
+ viewport = {
3042
+ elem: viewport,
3043
+ height: viewport[ (viewport[0] === window ? 'h' : 'outerH') + 'eight' ](),
3044
+ width: viewport[ (viewport[0] === window ? 'w' : 'outerW') + 'idth' ](),
3045
+ scrollleft: fixed ? 0 : viewport.scrollLeft(),
3046
+ scrolltop: fixed ? 0 : viewport.scrollTop(),
3047
+ offset: viewport.offset() || { left: 0, top: 0 }
3048
+ };
3049
+ container = {
3050
+ elem: container,
3051
+ scrollLeft: container.scrollLeft(),
3052
+ scrollTop: container.scrollTop(),
3053
+ offset: container.offset() || { left: 0, top: 0 }
3054
+ };
3055
+
3056
+ // Generic calculation method
3057
+ function calculate(side, otherSide, type, adjust, side1, side2, lengthName, targetLength, elemLength) {
3058
+ var initialPos = position[side1],
3059
+ mySide = my[side], atSide = at[side],
3060
+ isShift = type === SHIFT,
3061
+ viewportScroll = -container.offset[side1] + viewport.offset[side1] + viewport['scroll'+side1],
3062
+ myLength = mySide === side1 ? elemLength : mySide === side2 ? -elemLength : -elemLength / 2,
3063
+ atLength = atSide === side1 ? targetLength : atSide === side2 ? -targetLength : -targetLength / 2,
3064
+ tipLength = tip && tip.size ? tip.size[lengthName] || 0 : 0,
3065
+ tipAdjust = tip && tip.corner && tip.corner.precedance === side && !isShift ? tipLength : 0,
3066
+ overflow1 = viewportScroll - initialPos + tipAdjust,
3067
+ overflow2 = initialPos + elemLength - viewport[lengthName] - viewportScroll + tipAdjust,
3068
+ offset = myLength - (my.precedance === side || mySide === my[otherSide] ? atLength : 0) - (atSide === CENTER ? targetLength / 2 : 0);
3069
+
3070
+ // shift
3071
+ if(isShift) {
3072
+ tipAdjust = tip && tip.corner && tip.corner.precedance === otherSide ? tipLength : 0;
3073
+ offset = (mySide === side1 ? 1 : -1) * myLength - tipAdjust;
3074
+
3075
+ // Adjust position but keep it within viewport dimensions
3076
+ position[side1] += overflow1 > 0 ? overflow1 : overflow2 > 0 ? -overflow2 : 0;
3077
+ position[side1] = Math.max(
3078
+ -container.offset[side1] + viewport.offset[side1] + (tipAdjust && tip.corner[side] === CENTER ? tip.offset : 0),
3079
+ initialPos - offset,
3080
+ Math.min(
3081
+ Math.max(-container.offset[side1] + viewport.offset[side1] + viewport[lengthName], initialPos + offset),
3082
+ position[side1]
3083
+ )
3084
+ );
3085
+ }
3086
+
3087
+ // flip/flipinvert
3088
+ else {
3089
+ // Update adjustment amount depending on if using flipinvert or flip
3090
+ adjust *= (type === FLIPINVERT ? 2 : 0);
3091
+
3092
+ // Check for overflow on the left/top
3093
+ if(overflow1 > 0 && (mySide !== side1 || overflow2 > 0)) {
3094
+ position[side1] -= offset + adjust;
3095
+ newMy['invert'+side](side1);
3096
+ }
3097
+
3098
+ // Check for overflow on the bottom/right
3099
+ else if(overflow2 > 0 && (mySide !== side2 || overflow1 > 0) ) {
3100
+ position[side1] -= (mySide === CENTER ? -offset : offset) + adjust;
3101
+ newMy['invert'+side](side2);
3102
+ }
3103
+
3104
+ // Make sure we haven't made things worse with the adjustment and reset if so
3105
+ if(position[side1] < viewportScroll && -position[side1] > overflow2) {
3106
+ position[side1] = initialPos; newMy = my.clone();
3107
+ }
3108
+ }
3109
+
3110
+ return position[side1] - initialPos;
3111
+ }
3112
+
3113
+ // Set newMy if using flip or flipinvert methods
3114
+ if(methodX !== 'shift' || methodY !== 'shift') { newMy = my.clone(); }
3115
+
3116
+ // Adjust position based onviewport and adjustment options
3117
+ adjusted = {
3118
+ left: methodX !== 'none' ? calculate( X, Y, methodX, adjust.x, LEFT, RIGHT, WIDTH, targetWidth, elemWidth ) : 0,
3119
+ top: methodY !== 'none' ? calculate( Y, X, methodY, adjust.y, TOP, BOTTOM, HEIGHT, targetHeight, elemHeight ) : 0
3120
+ };
3121
+
3122
+ // Set tooltip position class if it's changed
3123
+ if(newMy && cache.lastClass !== (newClass = uitooltip + '-pos-' + newMy.abbrev())) {
3124
+ tooltip.removeClass(api.cache.lastClass).addClass( (api.cache.lastClass = newClass) );
3125
+ }
3126
+
3127
+ return adjusted;
3128
+ };
3129
+ PLUGINS.imagemap = function(api, area, corner, adjustMethod)
3130
+ {
3131
+ if(!area.jquery) { area = $(area); }
3132
+
3133
+ var cache = (api.cache.areas = {}),
3134
+ shape = (area[0].shape || area.attr('shape')).toLowerCase(),
3135
+ coordsString = area[0].coords || area.attr('coords'),
3136
+ baseCoords = coordsString.split(','),
3137
+ coords = [],
3138
+ image = $('img[usemap="#'+area.parent('map').attr('name')+'"]'),
3139
+ imageOffset = image.offset(),
3140
+ result = {
3141
+ width: 0, height: 0,
3142
+ position: {
3143
+ top: 1e10, right: 0,
3144
+ bottom: 0, left: 1e10
3145
+ }
3146
+ },
3147
+ i = 0, next = 0, dimensions;
3148
+
3149
+ // POLY area coordinate calculator
3150
+ // Special thanks to Ed Cradock for helping out with this.
3151
+ // Uses a binary search algorithm to find suitable coordinates.
3152
+ function polyCoordinates(result, coords, corner)
3153
+ {
3154
+ var i = 0,
3155
+ compareX = 1, compareY = 1,
3156
+ realX = 0, realY = 0,
3157
+ newWidth = result.width,
3158
+ newHeight = result.height;
3159
+
3160
+ // Use a binary search algorithm to locate most suitable coordinate (hopefully)
3161
+ while(newWidth > 0 && newHeight > 0 && compareX > 0 && compareY > 0)
3162
+ {
3163
+ newWidth = Math.floor(newWidth / 2);
3164
+ newHeight = Math.floor(newHeight / 2);
3165
+
3166
+ if(corner.x === LEFT){ compareX = newWidth; }
3167
+ else if(corner.x === RIGHT){ compareX = result.width - newWidth; }
3168
+ else{ compareX += Math.floor(newWidth / 2); }
3169
+
3170
+ if(corner.y === TOP){ compareY = newHeight; }
3171
+ else if(corner.y === BOTTOM){ compareY = result.height - newHeight; }
3172
+ else{ compareY += Math.floor(newHeight / 2); }
3173
+
3174
+ i = coords.length; while(i--)
3175
+ {
3176
+ if(coords.length < 2){ break; }
3177
+
3178
+ realX = coords[i][0] - result.position.left;
3179
+ realY = coords[i][1] - result.position.top;
3180
+
3181
+ if((corner.x === LEFT && realX >= compareX) ||
3182
+ (corner.x === RIGHT && realX <= compareX) ||
3183
+ (corner.x === CENTER && (realX < compareX || realX > (result.width - compareX))) ||
3184
+ (corner.y === TOP && realY >= compareY) ||
3185
+ (corner.y === BOTTOM && realY <= compareY) ||
3186
+ (corner.y === CENTER && (realY < compareY || realY > (result.height - compareY)))) {
3187
+ coords.splice(i, 1);
3188
+ }
3189
+ }
3190
+ }
3191
+
3192
+ return { left: coords[0][0], top: coords[0][1] };
3193
+ }
3194
+
3195
+ // Make sure we account for padding and borders on the image
3196
+ imageOffset.left += Math.ceil((image.outerWidth() - image.width()) / 2);
3197
+ imageOffset.top += Math.ceil((image.outerHeight() - image.height()) / 2);
3198
+
3199
+ // Parse coordinates into proper array
3200
+ if(shape === 'poly') {
3201
+ i = baseCoords.length; while(i--)
3202
+ {
3203
+ next = [ parseInt(baseCoords[--i], 10), parseInt(baseCoords[i+1], 10) ];
3204
+
3205
+ if(next[0] > result.position.right){ result.position.right = next[0]; }
3206
+ if(next[0] < result.position.left){ result.position.left = next[0]; }
3207
+ if(next[1] > result.position.bottom){ result.position.bottom = next[1]; }
3208
+ if(next[1] < result.position.top){ result.position.top = next[1]; }
3209
+
3210
+ coords.push(next);
3211
+ }
3212
+ }
3213
+ else {
3214
+ i = -1; while(i++ < baseCoords.length) {
3215
+ coords.push( parseInt(baseCoords[i], 10) );
3216
+ }
3217
+ }
3218
+
3219
+ // Calculate details
3220
+ switch(shape)
3221
+ {
3222
+ case 'rect':
3223
+ result = {
3224
+ width: Math.abs(coords[2] - coords[0]),
3225
+ height: Math.abs(coords[3] - coords[1]),
3226
+ position: {
3227
+ left: Math.min(coords[0], coords[2]),
3228
+ top: Math.min(coords[1], coords[3])
3229
+ }
3230
+ };
3231
+ break;
3232
+
3233
+ case 'circle':
3234
+ result = {
3235
+ width: coords[2] + 2,
3236
+ height: coords[2] + 2,
3237
+ position: { left: coords[0], top: coords[1] }
3238
+ };
3239
+ break;
3240
+
3241
+ case 'poly':
3242
+ result.width = Math.abs(result.position.right - result.position.left);
3243
+ result.height = Math.abs(result.position.bottom - result.position.top);
3244
+
3245
+ if(corner.abbrev() === 'c') {
3246
+ result.position = {
3247
+ left: result.position.left + (result.width / 2),
3248
+ top: result.position.top + (result.height / 2)
3249
+ };
3250
+ }
3251
+ else {
3252
+ // Calculate if we can't find a cached value
3253
+ if(!cache[corner+coordsString]) {
3254
+ result.position = polyCoordinates(result, coords.slice(), corner);
3255
+
3256
+ // If flip adjustment is enabled, also calculate the closest opposite point
3257
+ if(adjustMethod && (adjustMethod[0] === 'flip' || adjustMethod[1] === 'flip')) {
3258
+ result.offset = polyCoordinates(result, coords.slice(), {
3259
+ x: corner.x === LEFT ? RIGHT : corner.x === RIGHT ? LEFT : CENTER,
3260
+ y: corner.y === TOP ? BOTTOM : corner.y === BOTTOM ? TOP : CENTER
3261
+ });
3262
+
3263
+ result.offset.left -= result.position.left;
3264
+ result.offset.top -= result.position.top;
3265
+ }
3266
+
3267
+ // Store the result
3268
+ cache[corner+coordsString] = result;
3269
+ }
3270
+
3271
+ // Grab the cached result
3272
+ result = cache[corner+coordsString];
3273
+ }
3274
+
3275
+ result.width = result.height = 0;
3276
+ break;
3277
+ }
3278
+
3279
+ // Add image position to offset coordinates
3280
+ result.position.left += imageOffset.left;
3281
+ result.position.top += imageOffset.top;
3282
+
3283
+ return result;
3284
+ };
3285
+
3286
+
3287
+ /*
3288
+ * BGIFrame adaption (http://plugins.jquery.com/project/bgiframe)
3289
+ * Special thanks to Brandon Aaron
3290
+ */
3291
+ function BGIFrame(api)
3292
+ {
3293
+ var self = this,
3294
+ elems = api.elements,
3295
+ tooltip = elems.tooltip,
3296
+ namespace = '.bgiframe-' + api.id;
3297
+
3298
+ $.extend(self, {
3299
+ init: function()
3300
+ {
3301
+ // Create the BGIFrame element
3302
+ elems.bgiframe = $('<iframe class="ui-tooltip-bgiframe" frameborder="0" tabindex="-1" src="javascript:\'\';" ' +
3303
+ ' style="display:block; position:absolute; z-index:-1; filter:alpha(opacity=0); ' +
3304
+ '-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";"></iframe>');
3305
+
3306
+ // Append the new element to the tooltip
3307
+ elems.bgiframe.appendTo(tooltip);
3308
+
3309
+ // Update BGIFrame on tooltip move
3310
+ tooltip.bind('tooltipmove'+namespace, self.adjust);
3311
+ },
3312
+
3313
+ adjust: function()
3314
+ {
3315
+ var dimensions = api.get('dimensions'), // Determine current tooltip dimensions
3316
+ plugin = api.plugins.tip,
3317
+ tip = elems.tip,
3318
+ tipAdjust, offset;
3319
+
3320
+ // Adjust border offset
3321
+ offset = parseInt(tooltip.css('border-left-width'), 10) || 0;
3322
+ offset = { left: -offset, top: -offset };
3323
+
3324
+ // Adjust for tips plugin
3325
+ if(plugin && tip) {
3326
+ tipAdjust = (plugin.corner.precedance === 'x') ? ['width', 'left'] : ['height', 'top'];
3327
+ offset[ tipAdjust[1] ] -= tip[ tipAdjust[0] ]();
3328
+ }
3329
+
3330
+ // Update bgiframe
3331
+ elems.bgiframe.css(offset).css(dimensions);
3332
+ },
3333
+
3334
+ destroy: function()
3335
+ {
3336
+ // Remove iframe
3337
+ elems.bgiframe.remove();
3338
+
3339
+ // Remove bound events
3340
+ tooltip.unbind(namespace);
3341
+ }
3342
+ });
3343
+
3344
+ self.init();
3345
+ }
3346
+
3347
+ PLUGINS.bgiframe = function(api)
3348
+ {
3349
+ var browser = $.browser,
3350
+ self = api.plugins.bgiframe;
3351
+
3352
+ // Proceed only if the browser is IE6 and offending elements are present
3353
+ if($('select, object').length < 1 || !(browser.msie && (''+browser.version).charAt(0) === '6')) {
3354
+ return FALSE;
3355
+ }
3356
+
3357
+ return 'object' === typeof self ? self : (api.plugins.bgiframe = new BGIFrame(api));
3358
+ };
3359
+
3360
+ // Plugin needs to be initialized on render
3361
+ PLUGINS.bgiframe.initialize = 'render';
3362
+
3363
+
3364
+ }));
3365
+ }( window, document ));
js/jquery.qtip.min.js ADDED
@@ -0,0 +1,2 @@
 
 
1
+ /*! qTip2 v2.0.0 | http://craigsworks.com/projects/qtip2/ | Licensed MIT, GPL */
2
+ (function(a,b,c){(function(a){"use strict",typeof define=="function"&&define.amd?define(["jquery"],a):jQuery&&!jQuery.fn.qtip&&a(jQuery)})(function(d){function I(a){var b=function(a){return a===g||"object"!=typeof a},c=function(a){return!d.isFunction(a)&&(!a&&!a.attr||a.length<1||"object"==typeof a&&!a.jquery)};if(!a||"object"!=typeof a)return f;b(a.metadata)&&(a.metadata={type:a.metadata});if("content"in a){if(b(a.content)||a.content.jquery)a.content={text:a.content};c(a.content.text||f)&&(a.content.text=f),"title"in a.content&&(b(a.content.title)&&(a.content.title={text:a.content.title}),c(a.content.title.text||f)&&(a.content.title.text=f))}return"position"in a&&b(a.position)&&(a.position={my:a.position,at:a.position}),"show"in a&&b(a.show)&&(a.show=a.show.jquery?{target:a.show}:{event:a.show}),"hide"in a&&b(a.hide)&&(a.hide=a.hide.jquery?{target:a.hide}:{event:a.hide}),"style"in a&&b(a.style)&&(a.style={classes:a.style}),d.each(u,function(){this.sanitize&&this.sanitize(a)}),a}function J(h,i,q,r){function Q(a){var b=0,c,d=i,e=a.split(".");while(d=d[e[b++]])b<e.length&&(c=d);return[c||i,e.pop()]}function R(a,b,c){var e=d.Event("tooltip"+a);return e.originalEvent=(c?d.extend({},c):g)||P.event||g,M.trigger(e,[s].concat(b||[])),!e.isDefaultPrevented()}function S(){var a=i.style.widget;M.toggleClass("ui-helper-reset "+y,a).toggleClass(B,i.style.def&&!a),O.content&&O.content.toggleClass(y+"-content",a),O.titlebar&&O.titlebar.toggleClass(y+"-header",a),O.button&&O.button.toggleClass(x+"-icon",!a)}function T(a){O.title&&(O.titlebar.remove(),O.titlebar=O.title=O.button=g,a!==f&&s.reposition())}function U(){var a=i.content.title.button,b=typeof a=="string",c=b?a:"Close tooltip";O.button&&O.button.remove(),a.jquery?O.button=a:O.button=d("<a />",{"class":"ui-state-default ui-tooltip-close "+(i.style.widget?"":x+"-icon"),title:c,"aria-label":c}).prepend(d("<span />",{"class":"ui-icon ui-icon-close",html:"&times;"})),O.button.appendTo(O.titlebar).attr("role","button").click(function(a){return M.hasClass(z)||s.hide(a),f}),s.redraw()}function V(){var a=J+"-title";O.titlebar&&T(),O.titlebar=d("<div />",{"class":x+"-titlebar "+(i.style.widget?"ui-widget-header":"")}).append(O.title=d("<div />",{id:a,"class":x+"-title","aria-atomic":e})).insertBefore(O.content).delegate(".ui-tooltip-close","mousedown keydown mouseup keyup mouseout",function(a){d(this).toggleClass("ui-state-active ui-state-focus",a.type.substr(-4)==="down")}).delegate(".ui-tooltip-close","mouseover mouseout",function(a){d(this).toggleClass("ui-state-hover",a.type==="mouseover")}),i.content.title.button?U():s.rendered&&s.redraw()}function W(a){var b=O.button,c=O.title;if(!s.rendered)return f;a?(c||V(),U()):b.remove()}function X(a,b){var c=O.title;if(!s.rendered||!a)return f;d.isFunction(a)&&(a=a.call(h,P.event,s));if(a===f||!a&&a!=="")return T(f);a.jquery&&a.length>0?c.empty().append(a.css({display:"block"})):c.html(a),s.redraw(),b!==f&&s.rendered&&M[0].offsetWidth>0&&s.reposition(P.event)}function Y(a,b){function g(a){function i(c){c&&(delete h[c.src],clearTimeout(s.timers.img[c.src]),d(c).unbind(N)),d.isEmptyObject(h)&&(s.redraw(),b!==f&&s.reposition(P.event),a())}var g,h={};if((g=e.find("img[src]:not([height]):not([width])")).length===0)return i();g.each(function(a,b){if(h[b.src]!==c)return;var e=0,f=3;(function g(){if(b.height||b.width||e>f)return i(b);e+=1,s.timers.img[b.src]=setTimeout(g,700)})(),d(b).bind("error"+N+" load"+N,function(){i(this)}),h[b.src]=b})}var e=O.content;return!s.rendered||!a?f:(d.isFunction(a)&&(a=a.call(h,P.event,s)||""),a.jquery&&a.length>0?e.empty().append(a.css({display:"block"})):e.html(a),s.rendered<0?M.queue("fx",g):(L=0,g(d.noop)),s)}function Z(){function l(a){if(M.hasClass(z))return f;clearTimeout(s.timers.show),clearTimeout(s.timers.hide);var b=function(){s.toggle(e,a)};i.show.delay>0?s.timers.show=setTimeout(b,i.show.delay):b()}function m(a){if(M.hasClass(z)||K||L)return f;var b=d(a.relatedTarget||a.target),e=b.closest(A)[0]===M[0],h=b[0]===g.show[0];clearTimeout(s.timers.show),clearTimeout(s.timers.hide);if(c.target==="mouse"&&e||i.hide.fixed&&/mouse(out|leave|move)/.test(a.type)&&(e||h)){try{a.preventDefault(),a.stopImmediatePropagation()}catch(j){}return}i.hide.delay>0?s.timers.hide=setTimeout(function(){s.hide(a)},i.hide.delay):s.hide(a)}function n(a){if(M.hasClass(z))return f;clearTimeout(s.timers.inactive),s.timers.inactive=setTimeout(function(){s.hide(a)},i.hide.inactive)}function o(a){s.rendered&&M[0].offsetWidth>0&&s.reposition(a)}var c=i.position,g={show:i.show.target,hide:i.hide.target,viewport:d(c.viewport),document:d(b),body:d(b.body),window:d(a)},j={show:d.trim(""+i.show.event).split(" "),hide:d.trim(""+i.hide.event).split(" ")},k=d.browser.msie&&parseInt(d.browser.version,10)===6;M.bind("mouseenter"+N+" mouseleave"+N,function(a){var b=a.type==="mouseenter";b&&s.focus(a),M.toggleClass(D,b)}),/mouse(out|leave)/i.test(i.hide.event)&&i.hide.leave==="window"&&g.window.bind("mouseout"+N+" blur"+N,function(a){!/select|option/.test(a.target.nodeName)&&!a.relatedTarget&&s.hide(a)}),i.hide.fixed?(g.hide=g.hide.add(M),M.bind("mouseover"+N,function(){M.hasClass(z)||clearTimeout(s.timers.hide)})):/mouse(over|enter)/i.test(i.show.event)&&g.hide.bind("mouseleave"+N,function(a){clearTimeout(s.timers.show)}),(""+i.hide.event).indexOf("unfocus")>-1&&c.container.closest("html").bind("mousedown"+N,function(a){var b=d(a.target),c=s.rendered&&!M.hasClass(z)&&M[0].offsetWidth>0,e=b.parents(A).filter(M[0]).length>0;b[0]!==h[0]&&b[0]!==M[0]&&!e&&!h.has(b[0]).length&&!b.attr("disabled")&&s.hide(a)}),"number"==typeof i.hide.inactive&&(g.show.bind("qtip-"+q+"-inactive",n),d.each(t.inactiveEvents,function(a,b){g.hide.add(O.tooltip).bind(b+N+"-inactive",n)})),d.each(j.hide,function(a,b){var c=d.inArray(b,j.show),e=d(g.hide);c>-1&&e.add(g.show).length===e.length||b==="unfocus"?(g.show.bind(b+N,function(a){M[0].offsetWidth>0?m(a):l(a)}),delete j.show[c]):g.hide.bind(b+N,m)}),d.each(j.show,function(a,b){g.show.bind(b+N,l)}),"number"==typeof i.hide.distance&&g.show.add(M).bind("mousemove"+N,function(a){var b=P.origin||{},c=i.hide.distance,d=Math.abs;(d(a.pageX-b.pageX)>=c||d(a.pageY-b.pageY)>=c)&&s.hide(a)}),c.target==="mouse"&&(g.show.bind("mousemove"+N,function(a){v={pageX:a.pageX,pageY:a.pageY,type:"mousemove"}}),c.adjust.mouse&&(i.hide.event&&(M.bind("mouseleave"+N,function(a){(a.relatedTarget||a.target)!==g.show[0]&&s.hide(a)}),O.target.bind("mouseenter"+N+" mouseleave"+N,function(a){P.onTarget=a.type==="mouseenter"})),g.document.bind("mousemove"+N,function(a){s.rendered&&P.onTarget&&!M.hasClass(z)&&M[0].offsetWidth>0&&s.reposition(a||v)}))),(c.adjust.resize||g.viewport.length)&&(d.event.special.resize?g.viewport:g.window).bind("resize"+N,o),(g.viewport.length||k&&M.css("position")==="fixed")&&g.viewport.bind("scroll"+N,o)}function _(){var c=[i.show.target[0],i.hide.target[0],s.rendered&&O.tooltip[0],i.position.container[0],i.position.viewport[0],i.position.container.closest("html")[0],a,b];s.rendered?d([]).pushStack(d.grep(c,function(a){return typeof a=="object"})).unbind(N):i.show.target.unbind(N+"-create")}var s=this,E=b.body,J=x+"-"+q,K=0,L=0,M=d(),N=".qtip-"+q,O,P;s.id=q,s.rendered=f,s.destroyed=f,s.elements=O={target:h},s.timers={img:{}},s.options=i,s.checks={},s.plugins={},s.cache=P={event:{},target:d(),disabled:f,attr:r,onTarget:f,lastClass:""},s.checks.builtin={"^id$":function(a,b,c){var g=c===e?t.nextid:c,h=x+"-"+g;g!==f&&g.length>0&&!d("#"+h).length&&(M[0].id=h,O.content[0].id=h+"-content",O.title[0].id=h+"-title")},"^content.text$":function(a,b,c){Y(c)},"^content.title.text$":function(a,b,c){if(!c)return T();!O.title&&c&&V(),X(c)},"^content.title.button$":function(a,b,c){W(c)},"^position.(my|at)$":function(a,b,c){"string"==typeof c&&(a[b]=new u.Corner(c))},"^position.container$":function(a,b,c){s.rendered&&M.appendTo(c)},"^show.ready$":function(){s.rendered?s.toggle(e):s.render(1)},"^style.classes$":function(a,b,c){M.attr("class",x+" qtip "+c)},"^style.widget|content.title":S,"^events.(render|show|move|hide|focus|blur)$":function(a,b,c){M[(d.isFunction(c)?"":"un")+"bind"]("tooltip"+b,c)},"^(show|hide|position).(event|target|fixed|inactive|leave|distance|viewport|adjust)":function(){var a=i.position;M.attr("tracking",a.target==="mouse"&&a.adjust.mouse),_(),Z()}},d.extend(s,{render:function(a){if(s.rendered)return s;var b=i.content.text,c=i.content.title.text,g=i.position;return d.attr(h[0],"aria-describedby",J),M=O.tooltip=d("<div/>",{id:J,"class":x+" qtip "+B+" "+i.style.classes+" "+x+"-pos-"+i.position.my.abbrev(),width:i.style.width||"",height:i.style.height||"",tracking:g.target==="mouse"&&g.adjust.mouse,role:"alert","aria-live":"polite","aria-atomic":f,"aria-describedby":J+"-content","aria-hidden":e}).toggleClass(z,P.disabled).data("qtip",s).appendTo(i.position.container).append(O.content=d("<div />",{"class":x+"-content",id:J+"-content","aria-atomic":e})),s.rendered=-1,L=1,K=1,c&&(V(),d.isFunction(c)||X(c,f)),d.isFunction(b)||Y(b,f),s.rendered=e,S(),d.each(i.events,function(a,b){d.isFunction(b)&&M.bind(a==="toggle"?"tooltipshow tooltiphide":"tooltip"+a,b)}),d.each(u,function(){this.initialize==="render"&&this(s)}),Z(),M.queue("fx",function(b){R("render"),L=0,K=0,s.redraw(),(i.show.ready||a)&&s.toggle(e,P.event,f),b()}),s},get:function(a){var b,c;switch(a.toLowerCase()){case"dimensions":b={height:M.outerHeight(),width:M.outerWidth()};break;case"offset":b=u.offset(M,i.position.container);break;default:c=Q(a.toLowerCase()),b=c[0][c[1]],b=b.precedance?b.string():b}return b},set:function(a,b){function n(a,b){var c,d,e;for(c in l)for(d in l[c])if(e=(new RegExp(d,"i")).exec(a))b.push(e),l[c][d].apply(s,b)}var c=/^position\.(my|at|adjust|target|container)|style|content|show\.ready/i,h=/^content\.(title|attr)|style/i,j=f,k=f,l=s.checks,m;return"string"==typeof a?(m=a,a={},a[m]=b):a=d.extend(e,{},a),d.each(a,function(b,e){var f=Q(b.toLowerCase()),g;g=f[0][f[1]],f[0][f[1]]="object"==typeof e&&e.nodeType?d(e):e,a[b]=[f[0],f[1],e,g],j=c.test(b)||j,k=h.test(b)||k}),I(i),K=L=1,d.each(a,n),K=L=0,s.rendered&&M[0].offsetWidth>0&&(j&&s.reposition(i.position.target==="mouse"?g:P.event),k&&s.redraw()),s},toggle:function(a,c){function t(){a?(d.browser.msie&&M[0].style.removeAttribute("filter"),M.css("overflow",""),"string"==typeof h.autofocus&&d(h.autofocus,M).focus(),h.target.trigger("qtip-"+q+"-inactive")):M.css({display:"",visibility:"",opacity:"",left:"",top:""}),R(a?"visible":"hidden")}if(!s.rendered)return a?s.render(1):s;var g=a?"show":"hide",h=i[g],j=i[a?"hide":"show"],k=i.position,l=i.content,m=M[0].offsetWidth>0,n=a||h.target.length===1,o=!c||h.target.length<2||P.target[0]===c.target,p,r;(typeof a).search("boolean|number")&&(a=!m);if(!M.is(":animated")&&m===a&&o)return s;if(c){if(/over|enter/.test(c.type)&&/out|leave/.test(P.event.type)&&i.show.target.add(c.target).length===i.show.target.length&&M.has(c.relatedTarget).length)return s;P.event=d.extend({},c)}return R(g,[90])?(d.attr(M[0],"aria-hidden",!a),a?(P.origin=d.extend({},v),s.focus(c),d.isFunction(l.text)&&Y(l.text,f),d.isFunction(l.title.text)&&X(l.title.text,f),!G&&k.target==="mouse"&&k.adjust.mouse&&(d(b).bind("mousemove.qtip",function(a){v={pageX:a.pageX,pageY:a.pageY,type:"mousemove"}}),G=e),s.reposition(c,arguments[2]),!h.solo||d(A,h.solo).not(M).qtip("hide",d.Event("tooltipsolo"))):(clearTimeout(s.timers.show),delete P.origin,G&&!d(A+'[tracking="true"]:visible',h.solo).not(M).length&&(d(b).unbind("mousemove.qtip"),G=f),s.blur(c)),h.effect===f||n===f?(M[g](),t.call(M)):d.isFunction(h.effect)?(M.stop(1,1),h.effect.call(M,s),M.queue("fx",function(a){t(),a()})):M.fadeTo(90,a?1:0,t),a&&h.target.trigger("qtip-"+q+"-inactive"),s):s},show:function(a){return s.toggle(e,a)},hide:function(a){return s.toggle(f,a)},focus:function(a){if(!s.rendered)return s;var b=d(A),c=parseInt(M[0].style.zIndex,10),e=t.zindex+b.length,f=d.extend({},a),g;return M.hasClass(C)||R("focus",[e],f)&&(c!==e&&(b.each(function(){this.style.zIndex>c&&(this.style.zIndex=this.style.zIndex-1)}),b.filter("."+C).qtip("blur",f)),M.addClass(C)[0].style.zIndex=e),s},blur:function(a){return M.removeClass(C),R("blur",[M.css("zIndex")],a),s},reposition:function(c,e){if(!s.rendered||K)return s;K=1;var g=i.position.target,h=i.position,j=h.my,k=h.at,q=h.adjust,r=q.method.split(" "),t=M.outerWidth(),w=M.outerHeight(),x=0,y=0,z=M.css("position")==="fixed",A=h.viewport,B={left:0,top:0},C=h.container,D=M[0].offsetWidth>0,E,F,G;if(d.isArray(g)&&g.length===2)k={x:m,y:l},B={left:g[0],top:g[1]};else if(g==="mouse"&&(c&&c.pageX||P.event.pageX))k={x:m,y:l},c=(c&&(c.type==="resize"||c.type==="scroll")?P.event:c&&c.pageX&&c.type==="mousemove"?c:v&&v.pageX&&(q.mouse||!c||!c.pageX)?{pageX:v.pageX,pageY:v.pageY}:!q.mouse&&P.origin&&P.origin.pageX&&i.show.distance?P.origin:c)||c||P.event||v||{},B={top:c.pageY,left:c.pageX};else{g==="event"&&c&&c.target&&c.type!=="scroll"&&c.type!=="resize"?P.target=d(c.target):g!=="event"&&(P.target=d(g.jquery?g:O.target)),g=P.target,g=d(g).eq(0);if(g.length===0)return s;g[0]===b||g[0]===a?(x=u.iOS?a.innerWidth:g.width(),y=u.iOS?a.innerHeight:g.height(),g[0]===a&&(B={top:(A||g).scrollTop(),left:(A||g).scrollLeft()})):u.imagemap&&g.is("area")?E=u.imagemap(s,g,k,u.viewport?r:f):u.svg&&typeof g[0].xmlbase=="string"?E=u.svg(s,g,k,u.viewport?r:f):(x=g.outerWidth(),y=g.outerHeight(),B=u.offset(g,C)),E&&(x=E.width,y=E.height,F=E.offset,B=E.position);if(u.iOS>3.1&&u.iOS<4.1||u.iOS>=4.3&&u.iOS<4.33||!u.iOS&&z)G=d(a),B.left-=G.scrollLeft(),B.top-=G.scrollTop();B.left+=k.x===o?x:k.x===p?x/2:0,B.top+=k.y===n?y:k.y===p?y/2:0}return B.left+=q.x+(j.x===o?-t:j.x===p?-t/2:0),B.top+=q.y+(j.y===n?-w:j.y===p?-w/2:0),u.viewport?(B.adjusted=u.viewport(s,B,h,x,y,t,w),F&&B.adjusted.left&&(B.left+=F.left),F&&B.adjusted.top&&(B.top+=F.top)):B.adjusted={left:0,top:0},R("move",[B,A.elem||A],c)?(delete B.adjusted,e===f||!D||isNaN(B.left)||isNaN(B.top)||g==="mouse"||!d.isFunction(h.effect)?M.css(B):d.isFunction(h.effect)&&(h.effect.call(M,s,d.extend({},B)),M.queue(function(a){d(this).css({opacity:"",height:""}),d.browser.msie&&this.style.removeAttribute("filter"),a()})),K=0,s):s},redraw:function(){if(s.rendered<1||L)return s;var a=i.style,b=i.position.container,c,d,e,f;return L=1,R("redraw"),a.height&&M.css(k,a.height),a.width?M.css(j,a.width):(M.css(j,"").appendTo(H),d=M.width(),d%2<1&&(d+=1),e=M.css("max-width")||"",f=M.css("min-width")||"",c=(e+f).indexOf("%")>-1?b.width()/100:0,e=(e.indexOf("%")>-1?c:1)*parseInt(e,10)||d,f=(f.indexOf("%")>-1?c:1)*parseInt(f,10)||0,d=e+f?Math.min(Math.max(d,f),e):d,M.css(j,Math.round(d)).appendTo(b)),R("redrawn"),L=0,s},disable:function(a){return"boolean"!=typeof a&&(a=!M.hasClass(z)&&!P.disabled),s.rendered?(M.toggleClass(z,a),d.attr(M[0],"aria-disabled",a)):P.disabled=!!a,s},enable:function(){return s.disable(f)},destroy:function(){var a=h[0],b=d.attr(a,F),c=h.data("qtip");s.destroyed=e,s.rendered&&(M.stop(1,0).remove(),d.each(s.plugins,function(){this.destroy&&this.destroy()})),clearTimeout(s.timers.show),clearTimeout(s.timers.hide),_();if(!c||s===c)d.removeData(a,"qtip"),i.suppress&&b&&(d.attr(a,"title",b),h.removeAttr(F)),h.removeAttr("aria-describedby");return h.unbind(".qtip-"+q),delete w[s.id],h}})}function K(a,c){var h,i,j,k,l,m=d(this),n=d(b.body),o=this===b?n:m,p=m.metadata?m.metadata(c.metadata):g,q=c.metadata.type==="html5"&&p?p[c.metadata.name]:g,r=m.data(c.metadata.name||"qtipopts");try{r=typeof r=="string"?d.parseJSON(r):r}catch(s){}k=d.extend(e,{},t.defaults,c,typeof r=="object"?I(r):g,I(q||p)),i=k.position,k.id=a;if("boolean"==typeof k.content.text){j=m.attr(k.content.attr);if(k.content.attr!==f&&j)k.content.text=j;else return f}i.container.length||(i.container=n),i.target===f&&(i.target=o),k.show.target===f&&(k.show.target=o),k.show.solo===e&&(k.show.solo=i.container.closest("body")),k.hide.target===f&&(k.hide.target=o),k.position.viewport===e&&(k.position.viewport=i.container),i.container=i.container.eq(0),i.at=new u.Corner(i.at),i.my=new u.Corner(i.my);if(d.data(this,"qtip"))if(k.overwrite)m.qtip("destroy");else if(k.overwrite===f)return f;return k.suppress&&(l=d.attr(this,"title"))&&d(this).removeAttr("title").attr(F,l).attr("title",""),h=new J(m,k,a,!!j),d.data(this,"qtip",h),m.bind("remove.qtip-"+a+" removeqtip.qtip-"+a,function(){h.destroy()}),h}function L(a){var b=this,c=a.elements.tooltip,g=a.options.content.ajax,h=t.defaults.content.ajax,i=".qtip-ajax",j=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,k=e,l=f,m;a.checks.ajax={"^content.ajax":function(a,d,e){d==="ajax"&&(g=e),d==="once"?b.init():g&&g.url?b.load():c.unbind(i)}},d.extend(b,{init:function(){return g&&g.url&&c.unbind(i)[g.once?"one":"bind"]("tooltipshow"+i,b.load),b},load:function(c){function r(){var b;if(a.destroyed)return;k=f,p&&(l=e,a.show(c.originalEvent)),(b=h.complete||g.complete)&&d.isFunction(b)&&b.apply(g.context||a,arguments)}function s(b,c,e){var f;if(a.destroyed)return;o&&"string"==typeof b&&(b=d("<div/>").append(b.replace(j,"")).find(o)),(f=h.success||g.success)&&d.isFunction(f)?f.call(g.context||a,b,c,e):a.set("content.text",b)}function t(b,c,d){if(a.destroyed||b.status===0)return;a.set("content.text",c+": "+d)}if(l){l=f;return}var i=g.url.lastIndexOf(" "),n=g.url,o,p=!g.loading&&k;if(p)try{c.preventDefault()}catch(q){}else if(c&&c.isDefaultPrevented())return b;m&&m.abort&&m.abort(),i>-1&&(o=n.substr(i),n=n.substr(0,i)),m=d.ajax(d.extend({error:h.error||t,context:a},g,{url:n,success:s,complete:r}))},destroy:function(){m&&m.abort&&m.abort(),a.destroyed=e}}),b.init()}function M(a,b,c){var d=Math.ceil(b/2),e=Math.ceil(c/2),f={bottomright:[[0,0],[b,c],[b,0]],bottomleft:[[0,0],[b,0],[0,c]],topright:[[0,c],[b,0],[b,c]],topleft:[[0,0],[0,c],[b,c]],topcenter:[[0,c],[d,0],[b,c]],bottomcenter:[[0,0],[b,0],[d,c]],rightcenter:[[0,0],[b,e],[0,c]],leftcenter:[[b,0],[b,c],[0,e]]};return f.lefttop=f.bottomright,f.righttop=f.bottomleft,f.leftbottom=f.topright,f.rightbottom=f.topleft,f[a.string()]}function N(a,b){function D(a){var b=v.is(":visible");v.show(),a(),v.toggle(b)}function E(){x.width=r.height,x.height=r.width}function F(){x.width=r.width,x.height=r.height}function G(b,d,g,j){if(!t.tip)return;var k=q.corner.clone(),u=g.adjusted,v=a.options.position.adjust.method.split(" "),x=v[0],y=v[1]||v[0],z={left:f,top:f,x:0,y:0},A,B={},C;q.corner.fixed!==e&&(x===s&&k.precedance===h&&u.left&&k.y!==p?k.precedance=k.precedance===h?i:h:x!==s&&u.left&&(k.x=k.x===p?u.left>0?m:o:k.x===m?o:m),y===s&&k.precedance===i&&u.top&&k.x!==p?k.precedance=k.precedance===i?h:i:y!==s&&u.top&&(k.y=k.y===p?u.top>0?l:n:k.y===l?n:l),k.string()!==w.corner.string()&&(w.top!==u.top||w.left!==u.left)&&q.update(k,f)),A=q.position(k,u),A[k.x]+=I(k,k.x),A[k.y]+=I(k,k.y),A.right!==c&&(A.left=-A.right),A.bottom!==c&&(A.top=-A.bottom),A.user=Math.max(0,r.offset);if(z.left=x===s&&!!u.left)k.x===p?B["margin-left"]=z.x=A["margin-left"]-u.left:(C=A.right!==c?[u.left,-A.left]:[-u.left,A.left],(z.x=Math.max(C[0],C[1]))>C[0]&&(g.left-=u.left,z.left=f),B[A.right!==c?o:m]=z.x);if(z.top=y===s&&!!u.top)k.y===p?B["margin-top"]=z.y=A["margin-top"]-u.top:(C=A.bottom!==c?[u.top,-A.top]:[-u.top,A.top],(z.y=Math.max(C[0],C[1]))>C[0]&&(g.top-=u.top,z.top=f),B[A.bottom!==c?n:l]=z.y);t.tip.css(B).toggle(!(z.x&&z.y||k.x===p&&z.y||k.y===p&&z.x)),g.left-=A.left.charAt?A.user:x!==s||z.top||!z.left&&!z.top?A.left:0,g.top-=A.top.charAt?A.user:y!==s||z.left||!z.left&&!z.top?A.top:0,w.left=u.left,w.top=u.top,w.corner=k.clone()}function H(){var b=r.corner,c=a.options.position,d=c.at,g=c.my.string?c.my.string():c.my;return b===f||g===f&&d===f?f:(b===e?q.corner=new u.Corner(g):b.string||(q.corner=new u.Corner(b),q.corner.fixed=e),w.corner=new u.Corner(q.corner.string()),q.corner.string()!=="centercenter")}function I(a,b,c){b=b?b:a[a.precedance];var d=t.titlebar&&a.y===l,e=d?t.titlebar:v,f="border-"+b+"-width",g=function(a){return parseInt(a.css(f),10)},h;return D(function(){h=(c?g(c):g(t.content)||g(e)||g(v))||0}),h}function J(a){var b=t.titlebar&&a.y===l,c=b?t.titlebar:t.content,e=d.browser.mozilla,f=e?"-moz-":d.browser.webkit?"-webkit-":"",g="border-radius-"+a.y+a.x,h="border-"+a.y+"-"+a.x+"-radius",i=function(a){return parseInt(c.css(a),10)||parseInt(v.css(a),10)},j;return D(function(){j=i(h)||i(f+h)||i(f+g)||i(g)||0}),j}function K(a){function z(a,b,c){var d=a.css(b)||n;return c&&d===a.css(c)?f:j.test(d)?f:d}var b,c,g,h=t.tip.css("cssText",""),i=a||q.corner,j=/rgba?\(0, 0, 0(, 0)?\)|transparent|#123456/i,k="border-"+i[i.precedance]+"-color",m="background-color",n="transparent",o=" !important",s=t.titlebar,u=s&&(i.y===l||i.y===p&&h.position().top+x.height/2+r.offset<s.outerHeight(e)),w=u?s:t.content;D(function(){y.fill=z(h,m)||z(w,m)||z(t.content,m)||z(v,m)||h.css(m),y.border=z(h,k,"color")||z(w,k,"color")||z(t.content,k,"color")||z(v,k,"color")||v.css(k),d("*",h).add(h).css("cssText",m+":"+n+o+";border:0"+o+";")})}function L(a){var b=a.precedance===i,c=x[b?j:k],d=x[b?k:j],e=a.string().indexOf(p)>-1,f=c*(e?.5:1),g=Math.pow,h=Math.round,l,m,n,o=Math.sqrt(g(f,2)+g(d,2)),q=[z/f*o,z/d*o];return q[2]=Math.sqrt(g(q[0],2)-g(z,2)),q[3]=Math.sqrt(g(q[1],2)-g(z,2)),l=o+q[2]+q[3]+(e?0:q[0]),m=l/o,n=[h(m*d),h(m*c)],{height:n[b?0:1],width:n[b?1:0]}}function N(a,b,c){return"<qvml:"+a+' xmlns="urn:schemas-microsoft.com:vml" class="qtip-vml" '+(b||"")+' style="behavior: url(#default#VML); '+(c||"")+'" />'}var q=this,r=a.options.style.tip,t=a.elements,v=t.tooltip,w={top:0,left:0},x={width:r.width,height:r.height},y={},z=r.border||0,A=".qtip-tip",B=!!(d("<canvas />")[0]||{}).getContext,C;q.corner=g,q.mimic=g,q.border=z,q.offset=r.offset,q.size=x,a.checks.tip={"^position.my|style.tip.(corner|mimic|border)$":function(){q.init()||q.destroy(),a.reposition()},"^style.tip.(height|width)$":function(){x={width:r.width,height:r.height},q.create(),q.update(),a.reposition()},"^content.title.text|style.(classes|widget)$":function(){t.tip&&t.tip.length&&q.update()}},d.extend(q,{init:function(){var a=H()&&(B||d.browser.msie);return a&&(q.create(),q.update(),v.unbind(A).bind("tooltipmove"+A,G),B||v.bind("tooltipredraw tooltipredrawn",function(a){a.type==="tooltipredraw"?(C=t.tip.html(),t.tip.html("")):t.tip.html(C)})),a},create:function(){var a=x.width,b=x.height,c;t.tip&&t.tip.remove(),t.tip=d("<div />",{"class":"ui-tooltip-tip"}).css({width:a,height:b}).prependTo(v),B?d("<canvas />").appendTo(t.tip)[0].getContext("2d").save():(c=N("shape",'coordorigin="0,0"',"position:absolute;"),t.tip.html(c+c),d("*",t.tip).bind("click mousedown",function(a){a.stopPropagation()}))},update:function(a,b){var c=t.tip,j=c.children(),k=x.width,s=x.height,A=r.mimic,C=Math.round,D,G,H,J,O;a||(a=w.corner||q.corner),A===f?A=a:(A=new u.Corner(A),A.precedance=a.precedance,A.x==="inherit"?A.x=a.x:A.y==="inherit"?A.y=a.y:A.x===A.y&&(A[a.precedance]=a[a.precedance])),D=A.precedance,a.precedance===h?E():F(),t.tip.css({width:k=x.width,height:s=x.height}),K(a),y.border!=="transparent"?(z=I(a,g),r.border===0&&z>0&&(y.fill=y.border),q.border=z=r.border!==e?r.border:z):q.border=z=0,H=M(A,k,s),q.size=O=L(a),c.css(O),a.precedance===i?J=[C(A.x===m?z:A.x===o?O.width-k-z:(O.width-k)/2),C(A.y===l?O.height-s:0)]:J=[C(A.x===m?O.width-k:0),C(A.y===l?z:A.y===n?O.height-s-z:(O.height-s)/2)],B?(j.attr(O),G=j[0].getContext("2d"),G.restore(),G.save(),G.clearRect(0,0,3e3,3e3),G.fillStyle=y.fill,G.strokeStyle=y.border,G.lineWidth=z*2,G.lineJoin="miter",G.miterLimit=100,G.translate(J[0],J[1]),G.beginPath(),G.moveTo(H[0][0],H[0][1]),G.lineTo(H[1][0],H[1][1]),G.lineTo(H[2][0],H[2][1]),G.closePath(),z&&(v.css("background-clip")==="border-box"&&(G.strokeStyle=y.fill,G.stroke()),G.strokeStyle=y.border,G.stroke()),G.fill()):(H="m"+H[0][0]+","+H[0][1]+" l"+H[1][0]+","+H[1][1]+" "+H[2][0]+","+H[2][1]+" xe",J[2]=z&&/^(r|b)/i.test(a.string())?parseFloat(d.browser.version,10)===8?2:1:0,j.css({coordsize:k+z+" "+(s+z),antialias:""+(A.string().indexOf(p)>-1),left:J[0],top:J[1],width:k+z,height:s+z}).each(function(a){var b=d(this);b[b.prop?"prop":"attr"]({coordsize:k+z+" "+(s+z),path:H,fillcolor:y.fill,filled:!!a,stroked:!a}).toggle(!!z||!!a),!a&&b.html()===""&&b.html(N("stroke",'weight="'+z*2+'px" color="'+y.border+'" miterlimit="1000" joinstyle="miter"'))})),b!==f&&q.position(a)},position:function(a){var b=t.tip,c={},e=Math.max(0,r.offset),g,n,o;return r.corner===f||!b?f:(a=a||q.corner,g=a.precedance,n=L(a),o=[a.x,a.y],g===h&&o.reverse(),d.each(o,function(b,d){var f,h,o;d===p?(f=g===i?m:l,c[f]="50%",c["margin-"+f]=-Math.round(n[g===i?j:k]/2)+e):(f=I(a,d),h=I(a,d,t.content),o=J(a),c[d]=b?h:e+(o>f?o:-f))}),c[a[g]]-=n[g===h?j:k],b.css({top:"",bottom:"",left:"",right:"",margin:""}).css(c),c)},destroy:function(){t.tip&&t.tip.remove(),t.tip=!1,v.unbind(A)}}),q.init()}function O(c){function s(){q=d(p,j).not("[disabled]").map(function(){return typeof this.focus=="function"?this:null})}function t(a){q.length<1&&a.length?a.not("body").blur():q.first().focus()}function v(a){var b=d(a.target),c=b.closest(".qtip"),e;e=c.length<1?f:parseInt(c[0].style.zIndex,10)>parseInt(j[0].style.zIndex,10),!e&&d(a.target).closest(A)[0]!==j[0]&&t(b)}var g=this,h=c.options.show.modal,i=c.elements,j=i.tooltip,k="#qtip-overlay",l=".qtipmodal",m=l+c.id,n="is-modal-qtip",o=d(b.body),p=u.modal.focusable.join(","),q={},r;c.checks.modal={"^show.modal.(on|blur)$":function(){g.init(),i.overlay.toggle(j.is(":visible"))},"^content.text$":function(){s()}},d.extend(g,{init:function(){return h.on?(r=g.create(),j.attr(n,e).css("z-index",u.modal.zindex+d(A+"["+n+"]").length).unbind(l).unbind(m).bind("tooltipshow"+l+" tooltiphide"+l,function(a,b,c){var e=a.originalEvent;if(a.target===j[0])if(e&&a.type==="tooltiphide"&&/mouse(leave|enter)/.test(e.type)&&d(e.relatedTarget).closest(r[0]).length)try{a.preventDefault()}catch(f){}else(!e||e&&!e.solo)&&g[a.type.replace("tooltip","")](a,c)}).bind("tooltipfocus"+l,function(a){if(a.isDefaultPrevented()||a.target!==j[0])return;var b=d(A).filter("["+n+"]"),c=u.modal.zindex+b.length,e=parseInt(j[0].style.zIndex,10);r[0].style.zIndex=c-2,b.each(function(){this.style.zIndex>e&&(this.style.zIndex-=1)}),b.end().filter("."+C).qtip("blur",a.originalEvent),j.addClass(C)[0].style.zIndex=c;try{a.preventDefault()}catch(f){}}).bind("tooltiphide"+l,function(a){a.target===j[0]&&d("["+n+"]").filter(":visible").not(j).last().qtip("focus",a)}),h.escape&&d(b).unbind(m).bind("keydown"+m,function(a){a.keyCode===27&&j.hasClass(C)&&c.hide(a)}),h.blur&&i.overlay.unbind(m).bind("click"+m,function(a){j.hasClass(C)&&c.hide(a)}),s(),g):g},create:function(){function c(){r.css({height:d(a).height(),width:d(a).width()})}var b=d(k);return b.length?i.overlay=b.insertAfter(d(A).last()):(r=i.overlay=d("<div />",{id:k.substr(1),html:"<div></div>",mousedown:function(){return f}}).hide().insertAfter(d(A).last()),d(a).unbind(l).bind("resize"+l,c),c(),r)},toggle:function(a,b,c){if(a&&a.isDefaultPrevented())return g;var i=h.effect,k=b?"show":"hide",l=r.is(":visible"),p=d("["+n+"]").filter(":visible").not(j),q;return r||(r=g.create()),r.is(":animated")&&l===b||!b&&p.length?g:(b?(r.css({left:0,top:0}),r.toggleClass("blurs",h.blur),h.stealfocus!==f&&(o.bind("focusin"+m,v),t(d("body :focus")))):o.unbind("focusin"+m),r.stop(e,f),d.isFunction(i)?i.call(r,b):i===f?r[k]():r.fadeTo(parseInt(c,10)||90,b?1:0,function(){b||d(this).hide()}),b||r.queue(function(a){r.css({left:"",top:""}),a()}),g)},show:function(a,b){return g.toggle(a,e,b)},hide:function(a,b){return g.toggle(a,f,b)},destroy:function(){var a=r;return a&&(a=d("["+n+"]").not(j).length<1,a?(i.overlay.remove(),d(b).unbind(l)):i.overlay.unbind(l+c.id),o.undelegate("*","focusin"+m)),j.removeAttr(n).unbind(l)}}),g.init()}function P(a){var b=this,c=a.elements,e=c.tooltip,f=".bgiframe-"+a.id;d.extend(b,{init:function(){c.bgiframe=d('<iframe class="ui-tooltip-bgiframe" frameborder="0" tabindex="-1" src="javascript:\'\';" style="display:block; position:absolute; z-index:-1; filter:alpha(opacity=0); -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";"></iframe>'),c.bgiframe.appendTo(e),e.bind("tooltipmove"+f,b.adjust)},adjust:function(){var b=a.get("dimensions"),d=a.plugins.tip,f=c.tip,g,h;h=parseInt(e.css("border-left-width"),10)||0,h={left:-h,top:-h},d&&f&&(g=d.corner.precedance==="x"?["width","left"]:["height","top"],h[g[1]]-=f[g[0]]()),c.bgiframe.css(h).css(b)},destroy:function(){c.bgiframe.remove(),e.unbind(f)}}),b.init()}var e=!0,f=!1,g=null,h="x",i="y",j="width",k="height",l="top",m="left",n="bottom",o="right",p="center",q="flip",r="flipinvert",s="shift",t,u,v,w={},x="ui-tooltip",y="ui-widget",z="ui-state-disabled",A="div.qtip."+x,B=x+"-default",C=x+"-focus",D=x+"-hover",E="_replacedByqTip",F="oldtitle",G,H;H=d("<div/>",{id:"qtip-rcontainer"}),d(function(){H.appendTo(b.body)}),t=d.fn.qtip=function(a,b,h){var i=(""+a).toLowerCase(),j=g,k=d.makeArray(arguments).slice(1),l=k[k.length-1],m=this[0]?d.data(this[0],"qtip"):g;if(!arguments.length&&m||i==="api")return m;if("string"==typeof a)return this.each(function(){var a=d.data(this,"qtip");if(!a)return e;l&&l.timeStamp&&(a.cache.event=l);if(i!=="option"&&i!=="options"||!b)a[i]&&a[i].apply(a[i],k);else if(d.isPlainObject(b)||h!==c)a.set(b,h);else return j=a.get(b),f}),j!==g?j:this;if("object"==typeof a||!arguments.length)return m=I(d.extend(e,{},a)),t.bind.call(this,m,l)},t.bind=function(a,b){return this.each(function(g){function n(a){function b(){l.render(typeof a=="object"||h.show.ready),i.show.add(i.hide).unbind(k)}if(l.cache.disabled)return f;l.cache.event=d.extend({},a),l.cache.target=a?d(a.target):[c],h.show.delay>0?(clearTimeout(l.timers.show),l.timers.show=setTimeout(b,h.show.delay),j.show!==j.hide&&i.hide.bind(j.hide,function(){clearTimeout(l.timers.show)})):b()}var h,i,j,k,l,m;m=d.isArray(a.id)?a.id[g]:a.id,m=!m||m===f||m.length<1||w[m]?t.nextid++:w[m]=m,k=".qtip-"+m+"-create",l=K.call(this,m,a);if(l===f)return e;h=l.options,d.each(u,function(){this.initialize==="initialize"&&this(l)}),i={show:h.show.target,hide:h.hide.target},j={show:d.trim(""+h.show.event).replace(/ /g,k+" ")+k,hide:d.trim(""+h.hide.event).replace(/ /g,k+" ")+k},/mouse(over|enter)/i.test(j.show)&&!/mouse(out|leave)/i.test(j.hide)&&(j.hide+=" mouseleave"+k),i.show.bind("mousemove"+k,function(a){v={pageX:a.pageX,pageY:a.pageY,type:"mousemove"},l.cache.onTarget=e}),i.show.bind(j.show,n),(h.show.ready||h.prerender)&&n(b)})},u=t.plugins={Corner:function(a){a=(""+a).replace(/([A-Z])/," $1").replace(/middle/gi,p).toLowerCase(),this.x=(a.match(/left|right/i)||a.match(/center/)||["inherit"])[0].toLowerCase(),this.y=(a.match(/top|bottom|center/i)||["inherit"])[0].toLowerCase();var b=a.charAt(0);this.precedance=b==="t"||b==="b"?i:h,this.string=function(){return this.precedance===i?this.y+this.x:this.x+this.y},this.abbrev=function(){var a=this.x.substr(0,1),b=this.y.substr(0,1);return a===b?a:this.precedance===i?b+a:a+b},this.invertx=function(a){this.x=this.x===m?o:this.x===o?m:a||this.x},this.inverty=function(a){this.y=this.y===l?n:this.y===n?l:a||this.y},this.clone=function(){return{x:this.x,y:this.y,precedance:this.precedance,string:this.string,abbrev:this.abbrev,clone:this.clone,invertx:this.invertx,inverty:this.inverty}}},offset:function(a,b){function j(a,b){c.left+=b*a.scrollLeft(),c.top+=b*a.scrollTop()}var c=a.offset(),e=a.closest("body")[0],f=b,g,h,i;if(f){do f.css("position")!=="static"&&(h=f.position(),c.left-=h.left+(parseInt(f.css("borderLeftWidth"),10)||0)+(parseInt(f.css("marginLeft"),10)||0),c.top-=h.top+(parseInt(f.css("borderTopWidth"),10)||0)+(parseInt(f.css("marginTop"),10)||0),!g&&(i=f.css("overflow"))!=="hidden"&&i!=="visible"&&(g=f));while((f=d(f[0].offsetParent)).length);g&&g[0]!==e&&j(g,1)}return c},iOS:parseFloat((""+(/CPU.*OS ([0-9_]{1,5})|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent)||[0,""])[1]).replace("undefined","3_2").replace("_",".").replace("_",""))||f,fn:{attr:function(a,b){if(this.length){var c=this[0],e="title",f=d.data(c,"qtip");if(a===e&&f&&"object"==typeof f&&f.options.suppress)return arguments.length<2?d.attr(c,F):(f&&f.options.content.attr===e&&f.cache.attr&&f.set("content.text",b),this.attr(F,b))}return d.fn["attr"+E].apply(this,arguments)},clone:function(a){var b=d([]),c="title",e=d.fn["clone"+E].apply(this,arguments);return a||e.filter("["+F+"]").attr("title",function(){return d.attr(this,F)}).removeAttr(F),e}}},d.each(u.fn,function(a,b){if(!b||d.fn[a+E])return e;var c=d.fn[a+E]=d.fn[a];d.fn[a]=function(){return b.apply(this,arguments)||c.apply(this,arguments)}}),d.ui||(d["cleanData"+E]=d.cleanData,d.cleanData=function(a){for(var b=0,e;(e=a[b])!==c;b++)try{d(e).triggerHandler("removeqtip")}catch(f){}d["cleanData"+E](a)}),t.version="@VERSION",t.nextid=0,t.inactiveEvents="click dblclick mousedown mouseup mousemove mouseleave mouseenter".split(" "),t.zindex=15e3,t.defaults={prerender:f,id:f,overwrite:e,suppress:e,content:{text:e,attr:"title",title:{text:f,button:f}},position:{my:"top left",at:"bottom right",target:f,container:f,viewport:f,adjust:{x:0,y:0,mouse:e,resize:e,method:"flip flip"},effect:function(a,b,c){d(this).animate(b,{duration:200,queue:f})}},show:{target:f,event:"mouseenter",effect:e,delay:90,solo:f,ready:f,autofocus:f},hide:{target:f,event:"mouseleave",effect:e,delay:0,fixed:f,inactive:f,leave:"window",distance:f},style:{classes:"",widget:f,width:f,height:f,def:e},events:{render:g,move:g,show:g,hide:g,toggle:g,visible:g,hidden:g,focus:g,blur:g}},u.svg=function(a,c,e,f){var g=d(b),h=c[0],i={width:0,height:0,position:{top:1e10,left:1e10}},j,k,l,m,n;while(!h.getBBox)h=h.parentNode;if(h.getBBox&&h.parentNode){j=h.getBBox(),k=h.getScreenCTM(),l=h.farthestViewportElement||h;if(!l.createSVGPoint)return i;m=l.createSVGPoint(),m.x=j.x,m.y=j.y,n=m.matrixTransform(k),i.position.left=n.x,i.position.top=n.y,m.x+=j.width,m.y+=j.height,n=m.matrixTransform(k),i.width=n.x-i.position.left,i.height=n.y-i.position.top,i.position.left+=g.scrollLeft(),i.position.top+=g.scrollTop()}return i},u.ajax=function(a){var b=a.plugins.ajax;return"object"==typeof b?b:a.plugins.ajax=new L(a)},u.ajax.initialize="render",u.ajax.sanitize=function(a){var b=a.content,c;b&&"ajax"in b&&(c=b.ajax,typeof c!="object"&&(c=a.content.ajax={url:c}),"boolean"!=typeof c.once&&c.once&&(c.once=!!c.once))},d.extend(e,t.defaults,{content:{ajax:{loading:e,once:e}}}),u.tip=function(a){var b=a.plugins.tip;return"object"==typeof b?b:a.plugins.tip=new N(a)},u.tip.initialize="render",u.tip.sanitize=function(a){var b=a.style,c;b&&"tip"in b&&(c=a.style.tip,typeof c!="object"&&(a.style.tip={corner:c}),/string|boolean/i.test(typeof c.corner)||(c.corner=e),typeof c.width!="number"&&delete c.width,typeof c.height!="number"&&delete c.height,typeof c.border!="number"&&c.border!==e&&delete c.border,typeof c.offset!="number"&&delete c.offset)},d.extend(e,t.defaults,{style:{tip:{corner:e,mimic:f,width:6,height:6,border:e,offset:0}}}),u.modal=function(a){var b=a.plugins.modal;return"object"==typeof b?b:a.plugins.modal=new O(a)},u.modal.initialize="render",u.modal.sanitize=function(a){a.show&&(typeof a.show.modal!="object"?a.show.modal={on:!!a.show.modal}:typeof a.show.modal.on=="undefined"&&(a.show.modal.on=e))},u.modal.zindex=t.zindex-200,u.modal.focusable=["a[href]","area[href]","input","select","textarea","button","iframe","object","embed","[tabindex]","[contenteditable]"],d.extend(e,t.defaults,{show:{modal:{on:f,effect:e,blur:e,stealfocus:e,escape:e}}}),u.viewport=function(c,d,e,f,g,q,t){function L(a,b,c,e,f,g,h,i,j){var k=d[f],l=w[a],m=y[a],n=c===s,o=-E.offset[f]+D.offset[f]+D["scroll"+f],q=l===f?j:l===g?-j:-j/2,t=m===f?i:m===g?-i:-i/2,u=G&&G.size?G.size[h]||0:0,v=G&&G.corner&&G.corner.precedance===a&&!n?u:0,x=o-k+v,z=k+j-D[h]-o+v,A=q-(w.precedance===a||l===w[b]?t:0)-(m===p?i/2:0);return n?(v=G&&G.corner&&G.corner.precedance===b?u:0,A=(l===f?1:-1)*q-v,d[f]+=x>0?x:z>0?-z:0,d[f]=Math.max(-E.offset[f]+D.offset[f]+(v&&G.corner[a]===p?G.offset:0),k-A,Math.min(Math.max(-E.offset[f]+D.offset[f]+D[h],k+A),d[f]))):(e*=c===r?2:0,x>0&&(l!==f||z>0)?(d[f]-=A+e,J["invert"+a](f)):z>0&&(l!==g||x>0)&&(d[f]-=(l===p?-A:A)+e,J["invert"+a](g)),d[f]<o&&-d[f]>z&&(d[f]=k,J=w.clone())),d[f]-k}var u=e.target,v=c.elements.tooltip,w=e.my,y=e.at,z=e.adjust,A=z.method.split(" "),B=A[0],C=A[1]||A[0],D=e.viewport,E=e.container,F=c.cache,G=c.plugins.tip,H={left:0,top:0},I,J,K;if(!D.jquery||u[0]===a||u[0]===b.body||z.method==="none")return H;I=v.css("position")==="fixed",D={elem:D,height:D[(D[0]===a?"h":"outerH")+"eight"](),width:D[(D[0]===a?"w":"outerW")+"idth"](),scrollleft:I?0:D.scrollLeft(),scrolltop:I?0:D.scrollTop(),offset:D.offset()||{left:0,top:0}},E={elem:E,scrollLeft:E.scrollLeft(),scrollTop:E.scrollTop(),offset:E.offset()||{left:0,top:0}};if(B!=="shift"||C!=="shift")J=w.clone();return H={left:B!=="none"?L(h,i,B,z.x,m,o,j,f,q):0,top:C!=="none"?L(i,h,C,z.y,l,n,k,g,t):0},J&&F.lastClass!==(K=x+"-pos-"+J.abbrev())&&v.removeClass(c.cache.lastClass).addClass(c.cache.lastClass=K),H},u.imagemap=function(a,b,c,e){function v(a,b,c){var d=0,e=1,f=1,g=0,h=0,i=a.width,j=a.height;while(i>0&&j>0&&e>0&&f>0){i=Math.floor(i/2),j=Math.floor(j/2),c.x===m?e=i:c.x===o?e=a.width-i:e+=Math.floor(i/2),c.y===l?f=j:c.y===n?f=a.height-j:f+=Math.floor(j/2),d=b.length;while(d--){if(b.length<2)break;g=b[d][0]-a.position.left,h=b[d][1]-a.position.top,(c.x===m&&g>=e||c.x===o&&g<=e||c.x===p&&(g<e||g>a.width-e)||c.y===l&&h>=f||c.y===n&&h<=f||c.y===p&&(h<f||h>a.height-f))&&b.splice(d,1)}}return{left:b[0][0],top:b[0][1]}}b.jquery||(b=d(b));var f=a.cache.areas={},g=(b[0].shape||b.attr("shape")).toLowerCase(),h=b[0].coords||b.attr("coords"),i=h.split(","),j=[],k=d('img[usemap="#'+b.parent("map").attr("name")+'"]'),q=k.offset(),r={width:0,height:0,position:{top:1e10,right:0,bottom:0,left:1e10}},s=0,t=0,u;q.left+=Math.ceil((k.outerWidth()-k.width())/2),q.top+=Math.ceil((k.outerHeight()-k.height())/2);if(g==="poly"){s=i.length;while(s--)t=[parseInt(i[--s],10),parseInt(i[s+1],10)],t[0]>r.position.right&&(r.position.right=t[0]),t[0]<r.position.left&&(r.position.left=t[0]),t[1]>r.position.bottom&&(r.position.bottom=t[1]),t[1]<r.position.top&&(r.position.top=t[1]),j.push(t)}else{s=-1;while(s++<i.length)j.push(parseInt(i[s],10))}switch(g){case"rect":r={width:Math.abs(j[2]-j[0]),height:Math.abs(j[3]-j[1]),position:{left:Math.min(j[0],j[2]),top:Math.min(j[1],j[3])}};break;case"circle":r={width:j[2]+2,height:j[2]+2,position:{left:j[0],top:j[1]}};break;case"poly":r.width=Math.abs(r.position.right-r.position.left),r.height=Math.abs(r.position.bottom-r.position.top),c.abbrev()==="c"?r.position={left:r.position.left+r.width/2,top:r.position.top+r.height/2}:(f[c+h]||(r.position=v(r,j.slice(),c),e&&(e[0]==="flip"||e[1]==="flip")&&(r.offset=v(r,j.slice(),{x:c.x===m?o:c.x===o?m:p,y:c.y===l?n:c.y===n?l:p}),r.offset.left-=r.position.left,r.offset.top-=r.position.top),f[c+h]=r),r=f[c+h]),r.width=r.height=0}return r.position.left+=q.left,r.position.top+=q.top,r},u.bgiframe=function(a){var b=d.browser,c=a.plugins.bgiframe;return d("select, object").length<1||!b.msie||(""+b.version).charAt(0)!=="6"?f:"object"==typeof c?c:a.plugins.bgiframe=new P(a)},u.bgiframe.initialize="render"})})(window,document);
js/menu-editor.js CHANGED
@@ -1,51 +1,277 @@
1
  //(c) W-Shadow
2
 
 
3
  /** @namespace wsEditorData */
4
 
5
  var wsIdCounter = 0;
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  (function ($){
8
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  /*
10
  * Utility function for generating pseudo-random alphanumeric menu IDs.
11
  * Rationale: Simpler than atomically auto-incrementing or globally unique IDs.
12
  */
13
- function randomMenuId(size){
14
- if ( typeof size == 'undefined' ){
15
- size = 5;
16
- }
17
-
18
- var text = "";
19
  var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
20
 
21
- for( var i=0; i < size; i++ )
22
- text += possible.charAt(Math.floor(Math.random() * possible.length));
 
23
 
24
- return text;
25
  }
26
-
27
  function outputWpMenu(menu){
 
 
 
28
  //Remove the current menu data
29
- $('#ws_menu_box').empty();
30
  $('#ws_submenu_box').empty();
31
- //Kill autocomplete boxes
32
- $('.ac_results').remove();
33
-
34
  //Display the new menu
35
  var i = 0;
36
- for (var filename in menu){
37
- outputTopMenu(menu[filename]);
 
 
 
38
  i++;
39
  }
40
-
41
  //Automatically select the first top-level menu
42
- $('#ws_menu_box .ws_menu:first').click();
43
  }
44
 
45
  /*
46
  * Create edit widgets for a top-level menu and its submenus and append them all to the DOM.
47
  *
48
- * Inputs :
49
  * menu - an object containing menu data
50
  * afterNode - if specified, the new menu widget will be inserted after this node. Otherwise,
51
  * it will be added to the end of the list.
@@ -55,205 +281,315 @@ function outputWpMenu(menu){
55
  function outputTopMenu(menu, afterNode){
56
  //Create a container for menu items, even if there are none
57
  var submenu = buildSubmenu(menu.items);
58
-
59
  //Create the menu widget
60
- var menu_obj = buildTopMenu(menu);
61
  menu_obj.data('submenu_id', submenu.attr('id'));
62
-
 
63
  //Display
64
  submenu.appendTo('#ws_submenu_box');
65
- if ( typeof afterNode != 'undefined' ){
 
66
  $(afterNode).after(menu_obj);
67
  } else {
68
  menu_obj.appendTo('#ws_menu_box');
69
  }
70
-
71
  return {
72
  'menu' : menu_obj,
73
  'submenu' : submenu
74
  };
75
  }
76
 
77
- /*
78
- * Create an edit widget for a top-level menu.
79
- */
80
- function buildTopMenu(menu){
81
- var subclass = '';
82
- if ( menu.separator ) {
83
- subclass = subclass + ' ws_menu_separator';
84
- }
85
-
86
- //Create the menu HTML
87
- var menu_obj = $('<div></div>')
88
- .attr('class', "ws_container ws_menu "+subclass)
89
- .attr('id', 'ws-topmenu-'+(wsIdCounter++))
90
- .data('defaults', menu.defaults)
91
- .data('initial_values', getCurrentFieldValues(menu))
92
- .data('field_editors_created', false);
93
-
94
- //Add a header and a container for property editors (to improve performance
95
- //the editors themselves are created later, when the user tries to access them
96
- //for the first time).
97
- var contents = [];
98
- contents.push(
99
- '<div class="ws_item_head">',
100
- menu.separator ? '' : '<a class="ws_edit_link"> </a><div class="ws_flag_container"> </div>',
101
- '<span class="ws_item_title">',
102
- ((menu.menu_title!=null) ? menu.menu_title : menu.defaults.menu_title),
103
- '&nbsp;</span>',
104
- '</div>',
105
- '<div class="ws_editbox" style="display: none;"></div>'
106
- );
107
- menu_obj.append(contents.join(''));
108
-
109
- //Apply flags based on the item's state
110
- if (menu.missing && !getFieldValue(menu, 'custom', false)) {
111
- addMenuFlag(menu_obj, 'missing');
112
- }
113
- if (menu.hidden) {
114
- addMenuFlag(menu_obj, 'hidden');
115
- }
116
- if (menu.unused) {
117
- addMenuFlag(menu_obj, 'unused');
118
- }
119
- if (getFieldValue(menu, 'custom', false)) {
120
- addMenuFlag(menu_obj, 'custom_item');
121
- }
122
-
123
- if ( !menu.separator ){
124
- //Allow the user to drag menu items to top-level menus
125
- menu_obj.droppable({
126
- 'hoverClass' : 'ws_menu_drop_hover',
127
-
128
- 'accept' : (function(thing){
129
- return thing.hasClass('ws_item');
130
- }),
131
-
132
- 'drop' : (function(event, ui){
133
- var droppedItemData = readItemState(ui.draggable);
134
- var new_item = buildMenuItem(droppedItemData);
135
- var submenu = $('#' + menu_obj.data('submenu_id'));
136
- submenu.append(new_item);
137
- ui.draggable.remove();
138
- })
139
- });
140
- }
141
-
142
- return menu_obj;
143
- }
144
-
145
  /*
146
  * Create and populate a submenu container.
147
  */
148
  function buildSubmenu(items){
149
  //Create a container for menu items, even if there are none
150
- var submenu = $('<div class="ws_submenu"style="display:none;"></div>');
151
  submenu.attr('id', 'ws-submenu-'+(wsIdCounter++));
152
-
153
- //Only show menus that have items.
154
  //Skip arrays (with a length) because filled menus are encoded as custom objects.
155
  var entry = null;
156
- if (items && (typeof items != 'Array')){
157
- for (var item_file in items){
158
- entry = buildMenuItem(items[item_file]);
159
  if ( entry ){
 
160
  submenu.append(entry);
161
  }
162
- }
163
  }
164
-
165
  //Make the submenu sortable
166
  makeBoxSortable(submenu);
167
-
168
  return submenu;
169
  }
170
 
171
- /*
172
- * Create an edit widget for a menu entry and return it.
 
 
 
 
173
  */
174
- function buildMenuItem(entry){
175
- if (!entry.defaults) {
176
- return null
177
- };
178
-
179
- var item = $('<div class="ws_container ws_item">')
180
- .data('defaults', entry.defaults)
181
- .data('initial_values', getCurrentFieldValues(entry))
182
  .data('field_editors_created', false);
183
-
 
 
 
 
 
184
  //Add a header and a container for property editors (to improve performance
185
  //the editors themselves are created later, when the user tries to access them
186
  //for the first time).
187
  var contents = [];
188
  contents.push(
189
  '<div class="ws_item_head">',
190
- '<a class="ws_edit_link"> </a><div class="ws_flag_container"> </div>',
 
191
  '<span class="ws_item_title">',
192
- ((entry.menu_title!=null)?entry.menu_title:entry.defaults.menu_title),
193
  '&nbsp;</span>',
 
194
  '</div>',
195
  '<div class="ws_editbox" style="display: none;"></div>'
196
  );
197
  item.append(contents.join(''));
198
-
199
  //Apply flags based on the item's state
200
- if (entry.missing && !getFieldValue(entry, 'custom', false)) {
201
- addMenuFlag(item, 'missing');
202
- }
203
- if (entry.hidden) {
204
- addMenuFlag(item, 'hidden');
205
- }
206
- if (entry.unused) {
207
- addMenuFlag(item, 'unused');
208
  }
209
- if (getFieldValue(entry, 'custom', false)) {
210
- addMenuFlag(item, 'custom_item');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  }
212
-
213
  return item;
214
  }
215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  /*
217
  * List of all menu fields that have an associated editor
218
- */
219
  var knownMenuFields = {
220
- 'menu_title' : {
221
  caption : 'Menu title',
222
- standardCaption : true,
223
- advanced : false,
224
- type : 'text',
225
- defaultValue: '',
226
- visible: true
227
- },
228
- 'access_level' : {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  caption: 'Required capability',
230
- standardCaption : true,
231
- advanced : false,
232
- type : 'text',
233
  defaultValue: 'read',
234
- addDropdown : true,
235
- visible: true
236
- },
237
- 'file' : {
238
- caption: 'URL',
239
- advanced : false,
240
- standardCaption : true,
241
- type : 'text',
242
- defaultValue: '',
243
- addDropdown : 'ws_page_selector',
244
- visible: true
245
- },
246
- 'page_title' : {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  caption: "Window title",
248
  standardCaption : true,
249
- advanced : true,
250
- type : 'text',
251
- defaultValue: '',
252
- visible: true
253
- },
254
- 'open_in' : {
255
  caption: 'Open in',
256
- standardCaption : true,
257
  advanced : true,
258
  type : 'select',
259
  options : {
@@ -263,77 +599,94 @@ var knownMenuFields = {
263
  },
264
  defaultValue: 'same_window',
265
  visible: false
266
- },
267
- 'css_class' : {
 
268
  caption: 'CSS classes',
269
- standardCaption : true,
270
  advanced : true,
271
- type : 'text',
272
- defaultValue: '',
273
- visible: true
274
- },
275
- 'icon_url' : {
276
  caption: 'Icon URL',
277
- standardCaption : true,
278
- advanced : true,
279
  type : 'icon_selector',
 
280
  defaultValue: 'div',
281
- visible: true
282
- },
283
- 'hookname' : {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  caption: 'Hook name',
285
- standardCaption : true,
286
  advanced : true,
287
- type : 'text',
288
- defaultValue: '',
289
- visible: true
290
- },
291
- 'custom' : {
292
- caption : 'Custom',
293
- standardCaption : false,
294
- advanced: true,
295
- type: 'checkbox',
296
- defaultValue: false,
297
- visible: false
298
- }
299
  };
300
 
301
  /*
302
  * Create editors for the visible fields of a menu entry and append them to the specified node.
303
  */
304
- function buildEditboxFields(containerNode, entry){
305
- var fields = knownMenuFields;
306
-
307
- var basicFields = $('<div class="ws_edit_panel ws_basic"></div>').appendTo(containerNode);
308
- var advancedFields = $('<div class="ws_edit_panel ws_advanced"></div>').appendTo(containerNode);
309
-
310
- if ( hideAdvancedSettings ){
311
  advancedFields.css('display', 'none');
312
  }
313
-
314
- for (var field_name in fields){
315
- if (!fields.hasOwnProperty(field_name)) {
 
 
 
 
 
316
  continue;
317
  }
318
- var field = buildEditboxField(entry, field_name, fields[field_name]);
 
319
  if (field){
320
- if (fields[field_name].advanced){
321
  advancedFields.append(field);
322
  } else {
323
  basicFields.append(field);
324
  }
325
-
326
- if (field_name == 'icon_url') {
327
- updateIconField(field.find('.ws_field_value'));
328
- }
329
  }
330
  }
331
-
332
  //Add a link that shows/hides advanced fields
333
- containerNode.append(
334
  '<div class="ws_toggle_container"><a href="#" class="ws_toggle_advanced_fields"'+
335
- (hideAdvancedSettings?'':' style="display:none;"')+'>'+
336
- (hideAdvancedSettings?captionShowAdvanced:captionHideAdvanced)
337
  +'</a></div>'
338
  );
339
  }
@@ -345,65 +698,60 @@ function buildEditboxField(entry, field_name, field_settings){
345
  if (typeof entry[field_name] === 'undefined') {
346
  return null; //skip fields this entry doesn't have
347
  }
348
-
349
- var default_value = (typeof entry.defaults[field_name] != 'undefined')?entry.defaults[field_name]:field_settings.defaultValue;
350
- var value = (entry[field_name]!=null)?entry[field_name]:default_value;
351
-
352
- //Build a form field of the appropriate type
353
  var inputBox = null;
354
  var basicTextField = '<input type="text" class="ws_field_value">';
 
355
  switch(field_settings.type){
356
  case 'select':
357
  inputBox = $('<select class="ws_field_value">');
358
  var option = null;
359
  for( var optionTitle in field_settings.options ){
 
 
 
360
  option = $('<option>')
361
  .val(field_settings.options[optionTitle])
362
  .text(optionTitle);
363
- if ( field_settings.options[optionTitle] == value ){
364
- option.prop('selected', 'selected');
365
- }
366
  option.appendTo(inputBox);
367
  }
368
  break;
369
-
370
  case 'checkbox':
371
- inputBox = $('<label><input type="checkbox"'+(value?' checked="checked"':'')+ ' class="ws_field_value"> '+
372
  field_settings.caption+'</label>'
373
  );
374
  break;
375
 
 
 
 
 
 
376
  case 'icon_selector':
377
- inputBox = $(basicTextField).val(value)
378
  .add('<button class="button ws_select_icon" title="Select icon"><div class="icon16 icon-settings"></div><img src="" style="display:none;"></button>');
379
  break;
380
-
381
- case 'text':
382
  default:
383
- inputBox = $(basicTextField).val(value);
384
  }
385
-
386
-
387
  var className = "ws_edit_field ws_edit_field-"+field_name;
388
- if(entry[field_name]==null){
389
- className += ' ws_input_default';
390
- }
391
-
392
- var hasDropdown = (typeof(field_settings['addDropdown']) != 'undefined') && field_settings.addDropdown;
393
- if ( hasDropdown ){
394
  className += ' ws_has_dropdown';
395
  }
396
-
397
- var editField = $('<div>' + (field_settings.standardCaption?(field_settings.caption+'<br>'):'') + '</div>')
398
  .attr('class', className)
399
  .append(inputBox);
400
-
401
- if ( hasDropdown ){
402
  //Add a dropdown button
403
- var dropdownId = 'ws_cap_selector';
404
- if ( typeof(field_settings.addDropdown) == 'string' ){
405
- dropdownId = field_settings.addDropdown;
406
- }
407
  editField.append(
408
  $('<input type="button" value="&#9660;">')
409
  .addClass('button ws_dropdown_button')
@@ -411,70 +759,157 @@ function buildEditboxField(entry, field_name, field_settings){
411
  .data('dropdownId', dropdownId)
412
  );
413
  }
414
-
415
  editField
416
- .append('<img src="'+imagesUrl+'/transparent16.png" class="ws_reset_button" title="Reset to default value">&nbsp;</img>')
417
- .data('field_name', field_name)
418
- .data('default_value', default_value);
419
-
420
  if ( !field_settings.visible ){
421
  editField.css('display', 'none');
422
  }
423
-
424
- return editField;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  }
426
 
427
- function updateIconField(input) {
428
- //Display the current icon in the selector.
429
- var container = input.closest('.ws_container');
430
- var cssClass = container.find('.ws_edit_field-css_class .ws_field_value').val();
431
- var iconUrl = input.val();
432
-
433
- var selectButton = input.closest('.ws_edit_field').find('.ws_select_icon');
434
- var cssIcon = selectButton.find('.icon16');
435
- var imageIcon = selectButton.find('img');
436
-
437
- var matches = cssClass.match(/\bmenu-icon-([^\s]+)\b/);
438
- //Icon URL take precedence over icon class.
439
- if ( iconUrl && iconUrl !== 'none' && iconUrl !== 'div' ) {
440
- cssIcon.hide();
441
- imageIcon.prop('src', iconUrl).show();
442
- } else if ( matches ) {
443
- imageIcon.hide();
444
- cssIcon.removeClass().addClass('icon16 icon-' + matches[1]).show();
445
  } else {
446
- //This menu has no icon at all. This is actually a valid state
447
- //and WordPress will display a menu like that correctly.
448
- imageIcon.hide();
449
- cssIcon.removeClass().addClass('icon16').show();
 
 
450
  }
451
  }
452
 
453
- /*
454
- * Get the current values of all menu fields (ignores defaults).
455
- * Returns an object containing each field as a separate property.
 
456
  */
457
- function getCurrentFieldValues(entry){
458
- var values = {};
459
-
460
- for (var field_name in knownMenuFields){
461
- if (typeof entry[field_name] === 'undefined') {
462
- continue; //skip fields this entry doesn't have
463
- }
464
- values[field_name] = entry[field_name];
465
  }
466
-
467
- values.defaults = entry.defaults;
468
-
469
- return values;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  }
471
 
472
  /*
473
- * Get the current value of a single menu field.
474
  *
475
- * If the specified field is not set, this function will attempt to retrieve it
476
  * from the "defaults" property of the menu object. If *that* fails, it will return
477
- * the value of the optional third argument defaultValue.
478
  */
479
  function getFieldValue(entry, fieldName, defaultValue){
480
  if ( (typeof entry[fieldName] === 'undefined') || (entry[fieldName] === null) ) {
@@ -505,91 +940,74 @@ function makeBoxSortable(menuBox){
505
  Parsing & encoding menu inputs
506
  ***************************************************************************/
507
 
508
- /*
509
  * Encode the current menu structure as JSON
510
  *
511
- * Returns :
512
- * A JSON-encoded string representing the current menu tree loaded in the editor.
513
  */
514
- function encodeMenuAsJSON(){
515
- var tree = readMenuTreeState();
 
 
 
 
 
 
516
  return $.toJSON(tree);
517
  }
518
 
519
  function readMenuTreeState(){
520
  var tree = {};
521
  var menu_position = 0;
522
-
523
  //Gather all menus and their items
524
- $('#ws_menu_box .ws_menu').each(function(i) {
525
- var menu = readMenuState(this, menu_position++);
526
-
527
  //Attach the current menu to the main struct
528
  var filename = (menu.file !== null)?menu.file:menu.defaults.file;
529
  tree[filename] = menu;
530
  });
531
-
532
- return tree;
533
- }
534
 
535
- /*
536
- * Extract the settings of a top-level menu from its editor widget(s).
537
- *
538
- * Inputs :
539
- * menu_div - DOM node, typically one with the .ws_menu class.
540
- * position - The current menu position (int).
541
- *
542
- * Output :
543
- * A menu object in the tree format.
544
- *
545
- */
546
- function readMenuState(menu_div, position){
547
- menu_div = $(menu_div);
548
- var menu = readAllFields(menu_div);
549
-
550
- menu.defaults = menu_div.data('defaults');
551
-
552
- menu.position = position;
553
- menu.defaults.position = position; //the real default value will later overwrite this
554
-
555
- menu.separator = menu_div.hasClass('ws_menu_separator');
556
- menu.hidden = menu_div.hasClass('ws_hidden');
557
-
558
- //Gather the menu's items, if any
559
- menu.items = {};
560
- var item_position = 0;
561
- $('#'+menu_div.data('submenu_id')).find('.ws_item').each(function (i) {
562
- var item = readItemState(this, item_position++);
563
- menu.items[ (item.file?item.file:item.defaults.file) ] = item;
564
- });
565
-
566
- return menu;
567
  }
568
 
569
- /*
570
  * Extract the current menu item settings from its editor widget.
571
  *
572
- * Inputs :
573
- * item_div - DOM node containing the editor widget, usually with the .ws_item class.
574
- * position - Menu item position among its sibling menu items.
575
- *
576
  */
577
- function readItemState(item_div, position){
578
- var item_div = $(item_div);
579
- var item = readAllFields(item_div);
580
-
581
- item.defaults = item_div.data('defaults');
582
-
 
 
583
  //Save the position data
584
- if ( typeof position == 'undefined' ){
585
- position = 0;
586
- }
587
  item.position = position;
588
- item.defaults.position = position;
589
-
590
- //Check if the item is hidden
591
- item.hidden = item_div.hasClass('ws_hidden');
592
-
 
 
 
 
 
 
 
 
 
 
 
 
593
  return item;
594
  }
595
 
@@ -597,131 +1015,216 @@ function readItemState(item_div, position){
597
  * Extract the values of all menu/item fields present in a container node
598
  *
599
  * Inputs:
600
- * container - a jQuery collection representing the node to read.
601
  */
602
  function readAllFields(container){
603
  if ( !container.hasClass('ws_container') ){
604
- container = container.parents('ws_container').first();
605
  }
606
-
607
  if ( !container.data('field_editors_created') ){
608
- return container.data('initial_values');
609
  }
610
-
611
  var state = {};
612
-
613
  //Iterate over all fields of the item
614
  container.find('.ws_edit_field').each(function() {
615
  var field = $(this);
616
-
617
  //Get the name of this field
618
- field_name = field.data('field_name');
619
  //Skip if unnamed
620
- if (!field_name) return true;
621
-
 
 
622
  //Find the field (usually an input or select element).
623
- input_box = field.find('.ws_field_value');
624
-
625
  //Save null if default used, custom value otherwise
626
  if (field.hasClass('ws_input_default')){
627
  state[field_name] = null;
628
  } else {
629
- if ( input_box.attr('type') == 'checkbox' ){
630
- state[field_name] = input_box.is(':checked');
631
- } else {
632
- state[field_name] = input_box.val();
633
- }
634
  }
 
635
  });
636
-
 
 
 
 
637
  return state;
638
  }
639
 
640
 
641
  /***************************************************************************
642
- Flag manipulation
643
  ***************************************************************************/
644
 
645
  var item_flags = {
646
- 'custom_item' : 'This is a custom menu item',
647
- 'unused' : 'This item was automatically (re)inserted into your custom menu because it is present in the default WordPress menu',
648
- 'missing' : 'This item is not present in the default WordPress menu.',
649
- 'hidden' : 'This item is hidden'
650
- }
651
-
652
- function addMenuFlag(item, flag){
653
  item = $(item);
654
-
655
  var item_class = 'ws_' + flag;
656
  var img_class = 'ws_' + flag + '_flag';
657
-
658
- item.addClass(item_class);
659
- //Add the flag image
660
- var flag_container = item.find('.ws_flag_container');
661
- if ( flag_container.find('.' + img_class).length == 0 ){
662
- flag_container.append('<div class="ws_flag '+img_class+'" title="'+item_flags[flag]+'"></div>');
 
 
 
 
 
663
  }
664
  }
665
 
666
- function removeMenuFlag(item, flag){
667
- item = $(item);
668
- var item_class = 'ws_' + flag;
669
- var img_class = 'ws_' + flag + '_flag';
670
-
671
- item.removeClass('ws_' + flag);
672
- item.find('.' + img_class).remove();
673
  }
674
 
675
- function toggleMenuFlag(item, flag){
676
- if (menuHasFlag(item, flag)){
677
- removeMenuFlag(item, flag);
 
 
 
 
 
 
 
 
 
 
 
 
678
  } else {
679
- addMenuFlag(item, flag);
680
  }
 
681
  }
682
 
683
- function menuHasFlag(item, flag){
684
- return $(item).hasClass('ws_'+flag);
 
 
 
 
 
 
 
 
685
  }
686
 
687
- function clearMenuFlags(item){
688
- item = $(item);
689
- item.find('.ws_flag').remove();
690
- for(var flag in item_flags){
691
- item.removeClass('ws_'+flag);
692
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693
  }
694
 
 
 
 
 
695
  //Cut & paste stuff
696
  var menu_in_clipboard = null;
697
- var submenu_in_clipboard = null;
698
- var item_in_clipboard = null;
699
  var ws_paste_count = 0;
700
 
701
  $(document).ready(function(){
702
- if (window.wsMenuEditorPro) {
 
703
  knownMenuFields['open_in'].visible = true;
 
 
 
704
  }
705
-
706
  //Make the top menu box sortable (we only need to do this once)
707
  var mainMenuBox = $('#ws_menu_box');
708
  makeBoxSortable(mainMenuBox);
709
-
710
  /***************************************************************************
711
  Event handlers for editor widgets
712
  ***************************************************************************/
713
  var menuEditorNode = $('#ws_menu_editor');
714
-
715
  //Highlight the clicked menu item and show it's submenu
716
  var currentVisibleSubmenu = null;
717
- $('#ws_menu_editor .ws_container').live('click', (function () {
718
  var container = $(this);
719
  if ( container.hasClass('ws_active') ){
720
  return;
721
  }
722
-
723
  //Highlight the active item and un-highlight the previous one
724
- container.addClass('ws_active')
725
  container.siblings('.ws_active').removeClass('ws_active');
726
  if ( container.hasClass('ws_menu') ){
727
  //Show/hide the appropriate submenu
@@ -731,19 +1234,20 @@ $(document).ready(function(){
731
  currentVisibleSubmenu = $('#'+container.data('submenu_id')).show();
732
  }
733
  }));
734
-
735
  //Show/hide a menu's properties
736
- $('#ws_menu_editor .ws_edit_link').live('click', (function () {
737
- var container = $(this).parents('.ws_container').first();
738
  var box = container.find('.ws_editbox');
739
-
740
- //For performance, the property editors for each menu are only created
741
  //when the user tries to access access them for the first time.
742
  if ( !container.data('field_editors_created') ){
743
- buildEditboxFields(box, container.data('initial_values'));
744
  container.data('field_editors_created', true);
 
745
  }
746
-
747
  $(this).toggleClass('ws_edit_link_expanded');
748
  //show/hide the editbox
749
  if ($(this).hasClass('ws_edit_link_expanded')){
@@ -754,235 +1258,385 @@ $(document).ready(function(){
754
  box.hide();
755
  }
756
  }));
757
-
758
  //The "Default" button : Reset to default value when clicked
759
- $('#ws_menu_editor .ws_reset_button').live('click', (function () {
760
- //Find the field div (it holds the default value)
761
- var field = $(this).parent();
 
762
  //Find the related input field
763
  var input = field.find('.ws_field_value');
764
- if ( (input.length > 0) && (field.length > 0) ) {
765
- //Set the value to the default
766
- if (input.attr('type') == 'checkbox'){
767
- if ( field.data('default_value') ){
768
- input.attr('checked', 'checked');
769
- } else {
770
- input.removeAttr('checked');
771
- }
772
- } else {
773
- input.val(field.data('default_value'));
774
  }
775
- field.addClass('ws_input_default');
776
- //Trigger the change event to ensure consistency
777
- input.change();
778
- }
 
 
 
779
  }));
780
 
781
  //When a field is edited, change it's appearance if it's contents don't match the default value.
782
  function fieldValueChange(){
783
  var input = $(this);
784
  var field = input.parents('.ws_edit_field').first();
785
-
786
- if ( input.attr('type') == 'checkbox' ){
787
- var value = input.is(':checked');
788
- } else {
789
- var value = input.val();
 
790
  }
791
-
792
- if ( field.data('default_value') != value ) {
793
- field.removeClass('ws_input_default');
794
- }
795
-
796
- var fieldName = field.data('field_name');
797
- if ( fieldName == 'menu_title' ){
798
- //If the changed field is the menu title, update the header
799
- field.parents('.ws_container').first().find('.ws_item_title').html(input.val()+'&nbsp;');
800
- } else if ( fieldName == 'custom' ){
801
- //Show/hide the custom flag
802
- var myContainer = field.parents('.ws_container').first();
803
- if ( value ){
804
- addMenuFlag(myContainer, 'custom_item');
805
- } else {
806
- removeMenuFlag(myContainer, 'custom_item');
807
- }
808
- } else if (fieldName == 'file' ){
809
- //A menu must always have a non-empty URL. If the user deletes the current value,
810
- //reset back to the default.
811
- if ( value == '' ){
812
- field.find('.ws_reset_button').click();
813
- }
814
- } else if (fieldName == 'icon_url') {
815
- updateIconField(input);
816
- } else if (fieldName == 'css_class') {
817
- updateIconField(input.closest('.ws_container').find('.ws_edit_field-icon_url .ws_field_value'));
818
- }
 
 
 
 
 
819
  }
820
  menuEditorNode.on('click change', '.ws_field_value', fieldValueChange);
821
 
822
  //Show/hide advanced fields
823
- $('#ws_menu_editor .ws_toggle_advanced_fields').live('click', function(){
824
  var self = $(this);
825
  var advancedFields = self.parents('.ws_container').first().find('.ws_advanced');
826
-
827
  if ( advancedFields.is(':visible') ){
828
  advancedFields.hide();
829
- self.text(captionShowAdvanced);
830
  } else {
831
  advancedFields.show();
832
- self.text(captionHideAdvanced);
833
  }
834
-
835
  return false;
836
  });
837
-
838
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
839
  /***************************************************************************
840
- Dropdown list for combobox fields
841
  ***************************************************************************/
842
 
843
- var availableDropdowns = {
844
- 'ws_cap_selector' : {
845
- list : $('#ws_cap_selector'),
846
- currentOwner : null,
847
- timeoutForgetOwner : 0
848
- },
849
- 'ws_page_selector' : {
850
- list : $('#ws_page_selector'),
851
- currentOwner : null,
852
- timeoutForgetOwner : 0
853
- }
854
- };
855
-
856
- //Show/hide the capability dropdown list when the button is clicked
857
- $('#ws_menu_editor input.ws_dropdown_button').live('click',function(event){
858
  var button = $(this);
859
- var inputBox = button.parent().find('input.ws_field_value');
860
-
861
- var dropdown = availableDropdowns[button.data('dropdownId')];
862
-
863
- clearTimeout(dropdown.timeoutForgetOwner);
864
- dropdown.timeoutForgetOwner = 0;
865
-
866
- //If we already own the list, hide it and rescind ownership.
867
- if ( dropdown.currentOwner == this ){
868
- dropdown.list.hide();
869
-
870
- dropdown.currentOwner = null;
871
- inputBox.focus();
872
-
 
 
 
873
  return;
874
  }
875
- dropdown.currentOwner = this; //Got ye now!
876
 
877
- //Pre-select the current capability (will clear selection if there's no match)
878
- dropdown.list.val(inputBox.val());
 
 
 
 
 
 
879
 
880
- //Show the list before moving it into place. This ensures the position will be calc. properly.
881
- dropdown.list.show();
882
 
883
- //Move the dropdown near to the button
884
  var inputPos = inputBox.offset();
885
- dropdown.list
886
- .css({
887
- position: 'absolute'
888
- })
889
- .offset({
890
- left: inputPos.left,
891
- top: inputPos.top + inputBox.outerHeight()
892
- });
893
-
894
- dropdown.list.focus();
895
- });
896
-
897
- //Also show it when the user presses the down arrow in the input field
898
- $('#ws_menu_editor .ws_has_dropdown input.ws_field_value').live('keyup', function(event){
 
 
 
899
  if ( event.which == 40 ){
900
- $(this).parent().find('input.ws_dropdown_button').click();
901
  }
902
  });
903
-
904
- //Event handlers for the dropdowns themselves
905
  var dropdownNodes = $('.ws_dropdown');
906
-
907
- //Hide capability dropdown when it loses focus
908
  dropdownNodes.blur(function(event){
909
- var dropdown = availableDropdowns[$(this).attr('id')];
910
-
911
- dropdown.list.hide();
912
- /*
913
- * Hackiness : make sure the list doesn't disappear & immediately reappear
914
- * when the event that caused it to lose focus was the user clicking on the
915
- * dropdown button.
916
- */
917
- dropdown.timeoutForgetOwner = setTimeout(
918
- (function(){
919
- dropdown.currentOwner = null;
920
- }),
921
- 200
922
- );
923
  });
924
-
925
  dropdownNodes.keydown(function(event){
926
- var dropdown = availableDropdowns[$(this).attr('id')];
927
-
928
- //Also hide it when the user presses Esc
929
  if ( event.which == 27 ){
930
- var inputBox = $(dropdown.currentOwner).parent().find('input.ws_field_value');
931
-
932
- dropdown.list.hide();
933
- if ( dropdown.currentOwner ){
934
- $(dropdown.currentOwner).parent().find('input.ws_field_value').focus();
935
  }
936
- dropdown.currentOwner = null;
937
-
938
  //Select an item & hide the list when the user presses Enter or Tab
939
  } else if ( (event.which == 13) || (event.which == 9) ){
940
- dropdown.list.hide();
941
-
942
- var inputBox = $(dropdown.currentOwner).parent().find('input.ws_field_value');
943
- if ( dropdown.list.val() ){
944
- inputBox.val(dropdown.list.val());
945
- inputBox.change();
 
946
  }
947
-
948
- inputBox.focus();
949
- dropdown.currentOwner = null;
950
-
951
  event.preventDefault();
952
  }
953
  });
954
-
955
- //Eat Tab keys to prevent focus theft. Required to make the "select item on Tab" thing work.
956
  dropdownNodes.keyup(function(event){
957
  if ( event.which == 9 ){
958
  event.preventDefault();
959
  }
960
- })
961
-
962
-
963
  //Update the input & hide the list when an option is clicked
964
  dropdownNodes.click(function(){
965
- var dropdown = availableDropdowns[$(this).attr('id')];
966
-
967
- if ( !dropdown.currentOwner || !dropdown.list.val() ){
968
- return;
 
969
  }
970
- dropdown.list.hide();
971
-
972
- var inputBox = $(dropdown.currentOwner).parent().find('input.ws_field_value');
973
- inputBox.val(dropdown.list.val()).change().focus();
974
- dropdown.currentOwner = null;
975
  });
976
-
977
  //Highlight an option when the user mouses over it (doesn't work in IE)
978
  dropdownNodes.mousemove(function(event){
979
  if ( !event.target ){
980
  return;
981
  }
982
-
983
- var option = $(event.target);
984
- if ( !option.attr('selected') && option.attr('value')){
985
- option.attr('selected', 'selected');
986
  }
987
  });
988
 
@@ -1000,23 +1654,23 @@ $(document).ready(function(){
1000
  //Assign the selected icon to the menu.
1001
  if (currentIconButton) {
1002
  var container = currentIconButton.closest('.ws_container');
1003
- var cssClassField = container.find('.ws_edit_field-css_class .ws_field_value');
1004
- var iconUrlField = container.find('.ws_edit_field-icon_url .ws_field_value');
1005
 
1006
  //Remove the existing icon class, if any.
1007
- var cssClass = cssClassField.val();
1008
  cssClass = jsTrim( cssClass.replace(/\bmenu-icon-[^\s]+\b/, '') );
1009
 
1010
  if (selectedIcon.data('icon-class')) {
1011
  //Add the new class.
1012
  cssClass = selectedIcon.data('icon-class') + ' ' + cssClass;
1013
  //Can't have both a class and an image or we'll get two overlapping icons.
1014
- iconUrlField.val('');
1015
  } else if (selectedIcon.data('icon-url')) {
1016
- iconUrlField.val(selectedIcon.data('icon-url'));
1017
  }
1018
- cssClassField.val(cssClass).change();
1019
- iconUrlField.change();
 
1020
  }
1021
 
1022
  currentIconButton = null;
@@ -1035,11 +1689,9 @@ $(document).ready(function(){
1035
 
1036
  currentIconButton = button;
1037
 
1038
- var container = currentIconButton.closest('.ws_container');
1039
- var cssClassField = container.find('.ws_edit_field-css_class .ws_field_value');
1040
- var iconUrlField = container.find('.ws_edit_field-icon_url .ws_field_value');
1041
- var cssClass = cssClassField.val();
1042
- var iconUrl = iconUrlField.val();
1043
 
1044
  var customImageOption = iconSelector.find('.ws_custom_image_icon').hide();
1045
 
@@ -1112,20 +1764,16 @@ $(document).ready(function(){
1112
  //Set the menu icon to the attachment URL.
1113
  if (currentIconButton) {
1114
  var container = currentIconButton.closest('.ws_container');
1115
- var cssClassField = container.find('.ws_edit_field-css_class .ws_field_value');
1116
- var iconUrlField = container.find('.ws_edit_field-icon_url .ws_field_value');
1117
- var cssClass = cssClassField.val();
1118
- var iconUrl = iconUrlField.val();
1119
 
1120
  //Remove the existing icon class, if any.
1121
- cssClass = jsTrim( cssClass.replace(/\bmenu-icon-[^\s]+\b/, '') );
1122
- cssClassField.val(cssClass);
1123
 
1124
  //Set the new icon URL.
1125
- iconUrlField.val(attachment.attributes.url);
1126
 
1127
- cssClassField.change();
1128
- iconUrlField.change();
1129
  }
1130
 
1131
  currentIconButton = null;
@@ -1156,39 +1804,40 @@ $(document).ready(function(){
1156
  currentIconButton = null;
1157
  }
1158
  });
1159
-
1160
-
1161
  /*************************************************************************
1162
  Menu toolbar buttons
1163
  *************************************************************************/
 
 
 
 
1164
  //Show/Hide menu
1165
  $('#ws_hide_menu').click(function () {
1166
  //Get the selected menu
1167
- var selection = $('#ws_menu_box .ws_active');
1168
  if (!selection.length) return;
1169
-
1170
  //Mark the menu as hidden/visible
1171
- //selection.toggleClass('ws_hidden');
1172
- toggleMenuFlag(selection, 'hidden');
1173
-
 
1174
  //Also mark all of it's submenus as hidden/visible
1175
- if ( menuHasFlag(selection,'hidden') ){
1176
- $('#' + selection.data('submenu_id') + ' .ws_item').each(function(){
1177
- addMenuFlag(this, 'hidden');
1178
- });
1179
- } else {
1180
- $('#' + selection.data('submenu_id') + ' .ws_item').each(function(){
1181
- removeMenuFlag(this, 'hidden');
1182
- });
1183
- }
1184
  });
1185
-
1186
  //Delete menu
1187
  $('#ws_delete_menu').click(function () {
1188
  //Get the selected menu
1189
- var selection = $('#ws_menu_box .ws_active');
1190
  if (!selection.length) return;
1191
-
1192
  if (confirm('Delete this menu?')){
1193
  //Delete the submenu first
1194
  $('#' + selection.data('submenu_id')).remove();
@@ -1196,343 +1845,421 @@ $(document).ready(function(){
1196
  selection.remove();
1197
  }
1198
  });
1199
-
1200
  //Copy menu
1201
  $('#ws_copy_menu').click(function () {
1202
  //Get the selected menu
1203
- var selection = $('#ws_menu_box .ws_active');
1204
  if (!selection.length) return;
1205
-
1206
  //Store a copy of the current menu state in clipboard
1207
- menu_in_clipboard = readMenuState(selection);
1208
  });
1209
-
1210
  //Cut menu
1211
  $('#ws_cut_menu').click(function () {
1212
  //Get the selected menu
1213
- var selection = $('#ws_menu_box .ws_active');
1214
  if (!selection.length) return;
1215
-
1216
  //Store a copy of the current menu state in clipboard
1217
- menu_in_clipboard = readMenuState(selection);
1218
-
1219
- //Remove the original menu and submenu
1220
- selection.remove();
1221
  $('#'+selection.data('submenu_id')).remove();
 
1222
  });
1223
-
1224
- //Paste menu
1225
- $('#ws_paste_menu').click(function () {
1226
- //Check if anything has been copied/cut
1227
- if (!menu_in_clipboard) return;
1228
-
1229
- var menu = $.extend(true, {}, menu_in_clipboard);
1230
 
 
 
1231
  //The user shouldn't need to worry about giving separators a unique filename.
1232
  if (menu.separator) {
1233
- menu.defaults.file = 'separator_'+randomMenuId();
1234
  }
1235
 
1236
- //Get the selected menu
1237
- var selection = $('#ws_menu_box .ws_active');
1238
-
1239
- if (selection.length > 0) {
1240
- //If a menu is selected add the pasted item after it
1241
- outputTopMenu(menu, selection);
 
 
 
 
 
 
 
 
 
1242
  } else {
1243
- //Otherwise add the pasted item at the end
1244
  outputTopMenu(menu);
1245
  }
 
 
 
 
 
 
 
 
 
 
 
 
1246
  });
1247
-
1248
  //New menu
1249
  $('#ws_new_menu').click(function () {
1250
  ws_paste_count++;
1251
-
1252
  //The new menu starts out rather bare
1253
- var randomId = 'custom_menu_'+randomMenuId();
1254
- var menu = {
1255
- menu_title : null,
1256
- page_title : null,
1257
- access_level : null,
1258
- file : null,
1259
- css_class : null,
1260
- icon_url : null,
1261
- hookname : null,
1262
- position : null,
1263
- custom : null,
1264
- open_in: null,
1265
- items : {}, //No items
1266
- defaults : {
1267
- menu_title : 'Custom Menu '+ws_paste_count,
1268
- page_title : '',
1269
- access_level : 'read',
1270
- file : randomId,
1271
- css_class : 'menu-top',
1272
- icon_url : 'images/generic.png',
1273
- hookname : randomId,
1274
- open_in : 'same_window',
1275
- custom: true, //Important : flag the new menu as custom, or it won't show up after saving.
1276
- position : 0,
1277
- separator: false
1278
- }
1279
- };
1280
-
1281
  //Insert the new menu
1282
- result = outputTopMenu(menu);
1283
-
 
1284
  //The menus's editbox is always open
1285
  result.menu.find('.ws_edit_link').click();
1286
  });
1287
-
1288
  //New separator
1289
- $('#ws_new_separator').click(function () {
1290
  ws_paste_count++;
1291
-
1292
  //The new menu starts out rather bare
1293
- var randomId = 'separator_'+randomMenuId();
1294
- var menu = {
1295
- menu_title : null,
1296
- page_title : null,
1297
- access_level : null,
1298
- file : null,
1299
- css_class : null,
1300
- icon_url : null,
1301
- hookname : null,
1302
- position : null,
1303
- separator : true, //Flag as a separator
1304
- custom : null,
1305
- open_in : null,
1306
- items : {}, //No items
1307
- defaults : {
1308
- menu_title : '',
1309
- page_title : '',
1310
  access_level : 'read',
1311
  file : randomId,
1312
- css_class : 'wp-menu-separator',
1313
- icon_url : '',
1314
- hookname : '',
1315
- position : 0,
1316
- custom: false, //Separators don't need to flagged as custom to be retained.
1317
- open_in: 'same_window',
1318
- separator: true
1319
  }
1320
- };
1321
-
1322
- //Insert the new menu
1323
- result = outputTopMenu(menu);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1324
  });
1325
-
1326
  /*************************************************************************
1327
  Item toolbar buttons
1328
  *************************************************************************/
 
 
 
 
1329
  //Show/Hide item
1330
  $('#ws_hide_item').click(function () {
1331
  //Get the selected item
1332
- var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
1333
  if (!selection.length) return;
1334
-
1335
  //Mark the item as hidden/visible
1336
- toggleMenuFlag(selection, 'hidden');
 
 
1337
  });
1338
-
1339
- //Delete menu
1340
  $('#ws_delete_item').click(function () {
1341
  //Get the selected menu
1342
- var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
1343
  if (!selection.length) return;
1344
-
1345
  if (confirm('Delete this menu item?')){
 
1346
  //Delete the item
1347
  selection.remove();
 
1348
  }
1349
  });
1350
-
1351
  //Copy item
1352
  $('#ws_copy_item').click(function () {
1353
  //Get the selected item
1354
- var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
1355
  if (!selection.length) return;
1356
-
1357
  //Store a copy of item state in the clipboard
1358
- item_in_clipboard = readItemState(selection);
1359
  });
1360
-
1361
  //Cut item
1362
  $('#ws_cut_item').click(function () {
1363
  //Get the selected item
1364
- var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
1365
  if (!selection.length) return;
1366
-
1367
  //Store a copy of item state in the clipboard
1368
- item_in_clipboard = readItemState(selection);
1369
-
1370
- //Remove the original item
 
1371
  selection.remove();
 
1372
  });
1373
-
1374
  //Paste item
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1375
  $('#ws_paste_item').click(function () {
1376
  //Check if anything has been copied/cut
1377
- if (!item_in_clipboard) return;
1378
 
1379
- //Create a new editor widget for the copied item
1380
- var item = $.extend(true, {}, item_in_clipboard);
1381
- var new_item = buildMenuItem(item);
1382
-
1383
- //Get the selected menu
1384
- var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
1385
- if (selection.length > 0) {
1386
- //If an item is selected add the pasted item after it
1387
- selection.after(new_item);
1388
- } else {
1389
- //Otherwise add the pasted item at the end
1390
- $('#ws_submenu_box .ws_submenu:visible').append(new_item);
1391
  }
1392
-
1393
- new_item.show();
 
 
1394
  });
1395
-
1396
  //New item
1397
  $('#ws_new_item').click(function () {
1398
- if ($('.ws_submenu:visible').length < 1) {
1399
- return; //Abort if no submenu visible
1400
  }
1401
-
1402
  ws_paste_count++;
1403
- var entry = {
1404
- menu_title : null,
1405
- page_title : null,
1406
- access_level : null,
1407
- file : null,
1408
- custom : null,
1409
- open_in : null,
1410
- defaults : {
1411
- menu_title : 'Custom Item ' + ws_paste_count,
1412
- page_title : '',
1413
- access_level : 'read',
1414
- file : 'custom_item_'+randomMenuId(),
1415
- is_plugin_page : false,
1416
- custom: true,
1417
- open_in : 'same_window'
1418
- }
1419
- };
1420
-
1421
  var menu = buildMenuItem(entry);
1422
-
1423
- //Insert the item into the box
1424
- $('#ws_submenu_box .ws_submenu:visible').append(menu);
1425
-
 
 
 
 
 
 
 
1426
  //The items's editbox is always open
1427
  menu.find('.ws_edit_link').click();
 
 
1428
  });
1429
 
1430
- function jsTrim(str){
1431
- return str.replace(/^\s+|\s+$/g, "");
1432
- }
1433
-
1434
  function compareMenus(a, b){
1435
- function jsTrim(str){
1436
- return str.replace(/^\s+|\s+$/g, "");
1437
- }
1438
-
1439
  var aTitle = jsTrim( $(a).find('.ws_item_title').text() );
1440
  var bTitle = jsTrim( $(b).find('.ws_item_title').text() );
1441
-
1442
  aTitle = aTitle.toLowerCase();
1443
  bTitle = bTitle.toLowerCase();
1444
-
1445
  return aTitle > bTitle ? 1 : -1;
1446
  }
1447
-
1448
  //Sort items in ascending order
1449
  $('#ws_sort_ascending').click(function () {
1450
- var submenu = $('#ws_submenu_box .ws_submenu:visible');
1451
- if (submenu.length < 1) {
1452
- return; //Abort if no submenu visible
1453
  }
1454
-
1455
  submenu.find('.ws_container').sort(compareMenus);
1456
  });
1457
-
1458
  //Sort items in descending order
1459
  $('#ws_sort_descending').click(function () {
1460
- var submenu = $('#ws_submenu_box .ws_submenu:visible');
1461
- if (submenu.length < 1) {
1462
- return; //Abort if no submenu visible
1463
  }
1464
-
1465
  submenu.find('.ws_container').sort((function(a, b){
1466
  return -compareMenus(a, b);
1467
  }));
1468
  });
1469
-
1470
  //==============================================
1471
  // Main buttons
1472
  //==============================================
1473
-
1474
  //Save Changes - encode the current menu as JSON and save
1475
  $('#ws_save_menu').click(function () {
1476
- var data = encodeMenuAsJSON();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1477
  $('#ws_data').val(data);
 
 
1478
  $('#ws_main_form').submit();
1479
  });
1480
-
1481
  //Load default menu - load the default WordPress menu
1482
  $('#ws_load_menu').click(function () {
1483
  if (confirm('Are you sure you want to load the default WordPress menu?')){
1484
- outputWpMenu(defaultMenu);
1485
  }
1486
  });
1487
-
1488
  //Reset menu - re-load the custom menu. Discards any changes made by user.
1489
  $('#ws_reset_menu').click(function () {
1490
  if (confirm('Undo all changes made in the current editing session?')){
1491
- outputWpMenu(customMenu);
1492
  }
1493
  });
1494
-
1495
  //Export menu - download the current menu as a file
1496
- $('#export_dialog').dialog({
1497
  autoOpen: false,
1498
  closeText: ' ',
1499
  modal: true,
1500
  minHeight: 100
1501
  });
1502
-
1503
  $('#ws_export_menu').click(function(){
1504
  var button = $(this);
1505
  button.attr('disabled', 'disabled');
1506
  button.val('Exporting...');
1507
-
1508
  $('#export_complete_notice, #download_menu_button').hide();
1509
  $('#export_progress_notice').show();
1510
  $('#export_dialog').dialog('open');
1511
-
1512
  //Encode and store the menu for download
1513
- var menu = readMenuTreeState();
1514
- var exportData = {
1515
- 'format' : exportFormatString,
1516
- 'menu' : menu
1517
- };
1518
- exportData = $.toJSON(exportData);
1519
-
1520
  $.post(
1521
- adminAjaxUrl,
1522
  {
1523
  'data' : exportData,
1524
  'action' : 'export_custom_menu',
1525
- '_ajax_nonce' : exportMenuNonce
1526
  },
1527
- function(data, textStatus){
1528
  button.val('Export');
1529
  button.removeAttr('disabled');
1530
-
1531
  if ( typeof data['error'] != 'undefined' ){
1532
  $('#export_dialog').dialog('close');
1533
  alert(data.error);
1534
  }
1535
-
1536
  if ( (typeof data['download_url'] != 'undefined') && data.download_url ){
1537
  //window.location = data.download_url;
1538
  $('#download_menu_button').attr('href', data.download_url);
@@ -1543,50 +2270,46 @@ $(document).ready(function(){
1543
  'json'
1544
  );
1545
  });
1546
-
1547
  $('#ws_cancel_export').click(function(){
1548
  $('#export_dialog').dialog('close');
1549
  });
1550
-
1551
  $('#download_menu_button').click(function(){
1552
  $('#export_dialog').dialog('close');
1553
  });
1554
-
1555
  //Import menu - upload an exported menu and show it in the editor
1556
- $('#import_dialog').dialog({
1557
  autoOpen: false,
1558
  closeText: ' ',
1559
  modal: true
1560
  });
1561
-
1562
  $('#ws_cancel_import').click(function(){
1563
  $('#import_dialog').dialog('close');
1564
  });
1565
-
1566
  $('#ws_import_menu').click(function(){
1567
  $('#import_progress_notice, #import_progress_notice2, #import_complete_notice').hide();
1568
  $('#import_menu_form').resetForm();
1569
  //The "Upload" button is disabled until the user selects a file
1570
- $('#ws_start_import').attr('disabled', 'disabled');
1571
-
1572
- $('#import_dialog .hide-when-uploading').show();
1573
-
1574
- $('#import_dialog').dialog('open');
1575
- })
1576
-
1577
  $('#import_file_selector').change(function(){
1578
- if ( $(this).val() ){
1579
- $('#ws_start_import').removeAttr('disabled');
1580
- } else {
1581
- $('#ws_start_import').attr('disabled', 'disabled');
1582
- };
1583
  });
1584
-
1585
  //AJAXify the upload form
1586
  $('#import_menu_form').ajaxForm({
1587
  dataType : 'json',
1588
- beforeSubmit: function(formData, $form, options) {
1589
-
1590
  //Check if the user has selected a file
1591
  for(var i = 0; i < formData.length; i++){
1592
  if ( formData[i].name == 'menu' ){
@@ -1596,44 +2319,97 @@ $(document).ready(function(){
1596
  }
1597
  }
1598
  }
1599
-
1600
- $('#import_dialog .hide-when-uploading').hide();
1601
  $('#import_progress_notice').show();
1602
-
1603
  $('#ws_start_import').attr('disabled', 'disabled');
 
1604
  },
1605
  success: function(data){
1606
- if ( !$('#import_dialog').dialog('isOpen') ){
 
1607
  //Whoops, the user closed the dialog while the upload was in progress.
1608
  //Discard the response silently.
1609
- return;
1610
- };
1611
-
1612
  if ( typeof data['error'] != 'undefined' ){
1613
  alert(data.error);
1614
  //Let the user try again
1615
  $('#import_menu_form').resetForm();
1616
- $('#import_dialog .hide-when-uploading').show();
1617
  }
1618
  $('#import_progress_notice').hide();
1619
-
1620
- if ( (typeof data['menu'] != 'undefined') && data.menu ){
1621
  //Whee, we got back a (seemingly) valid menu. A veritable miracle!
1622
  //Lets load it into the editor.
1623
- $('#import_progress_notice2').show();
1624
- outputWpMenu(data.menu);
1625
- $('#import_progress_notice2').hide();
1626
  //Display a success notice, then automatically close the window after a few moments
1627
  $('#import_complete_notice').show();
1628
  setTimeout((function(){
1629
  //Close the import dialog
1630
  $('#import_dialog').dialog('close');
1631
- }), 500);
1632
  }
1633
-
1634
  }
1635
  });
1636
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1637
  //Flag closed hints as hidden by sending the appropriate AJAX request to the backend.
1638
  $('.ws_hint_close').click(function() {
1639
  var hint = $(this).parents('.ws_hint').first();
@@ -1647,11 +2423,53 @@ $(document).ready(function(){
1647
  }
1648
  );
1649
  });
1650
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1651
  //Finally, show the menu
1652
- outputWpMenu(customMenu);
1653
  });
1654
-
1655
  })(jQuery);
1656
 
1657
  //==============================================
@@ -1661,34 +2479,35 @@ $(document).ready(function(){
1661
  jQuery(function($){
1662
  var screenOptions = $('#ws-ame-screen-meta-contents');
1663
  var checkbox = screenOptions.find('#ws-hide-advanced-settings');
1664
-
1665
- if ( hideAdvancedSettings ){
1666
  checkbox.attr('checked', 'checked');
1667
  } else {
1668
  checkbox.removeAttr('checked');
1669
  }
1670
-
1671
  //Update editor state when settings change
1672
  checkbox.click(function(){
1673
- hideAdvancedSettings = $(this).attr('checked'); //Using '$(this)' instead of 'checkbox' due to jQuery bugs
1674
- if ( hideAdvancedSettings ){
1675
- $('#ws_menu_editor div.ws_advanced').hide();
1676
- $('#ws_menu_editor a.ws_toggle_advanced_fields').text(captionShowAdvanced).show();
 
1677
  } else {
1678
- $('#ws_menu_editor div.ws_advanced').show();
1679
- $('#ws_menu_editor a.ws_toggle_advanced_fields').text(captionHideAdvanced).hide();
1680
  }
1681
-
1682
  $.post(
1683
- adminAjaxUrl,
1684
  {
1685
  'action' : 'ws_ame_save_screen_options',
1686
- 'hide_advanced_settings' : hideAdvancedSettings?1:0,
1687
- '_ajax_nonce' : hideAdvancedSettingsNonce
1688
  }
1689
  );
1690
  });
1691
-
1692
  //Move our options into the screen meta panel
1693
  $('#adv-settings').empty().append(screenOptions.show());
1694
  });
1
  //(c) W-Shadow
2
 
3
+ /*global wsEditorData, defaultMenu, customMenu */
4
  /** @namespace wsEditorData */
5
 
6
  var wsIdCounter = 0;
7
 
8
+ var AmeCapabilityManager = (function(roles, users) {
9
+ var me = {};
10
+ users = users || {};
11
+
12
+ function parseActorString(actor) {
13
+ var separator = actor.indexOf(':');
14
+ if (separator == -1) {
15
+ throw {
16
+ name: 'InvalidActorException',
17
+ message: "Actor string does not contain a colon.",
18
+ value: actor
19
+ };
20
+ }
21
+
22
+ return {
23
+ 'type' : actor.substring(0, separator),
24
+ 'id' : actor.substring(separator + 1)
25
+ }
26
+ }
27
+
28
+ me.hasCap = function(actor, capability, context) {
29
+ context = context || {};
30
+ var actorData = parseActorString(actor);
31
+
32
+ //Super admins have access to everything, unless specifically denied.
33
+ if ( actor == 'special:super_admin' ) {
34
+ return (capability != 'do_not_allow');
35
+ }
36
+
37
+ if (actorData.type == 'role') {
38
+ return me.roleHasCap(actorData.id, capability);
39
+ } else if (actorData.type == 'user') {
40
+ return me.userHasCap(actorData.id, capability, context);
41
+ }
42
+
43
+ throw {
44
+ name: 'InvalidActorTypeException',
45
+ message: "The specified actor type is not supported",
46
+ value: actor,
47
+ 'actorType': actorData.type
48
+ };
49
+ };
50
+
51
+ me.roleHasCap = function(roleId, capability) {
52
+ if (!roles.hasOwnProperty(roleId)) {
53
+ throw {
54
+ name: 'UnknownRoleException',
55
+ message: 'Can not check capabilities for an unknown role',
56
+ value: roleId,
57
+ requireCapability: capability
58
+ };
59
+ }
60
+
61
+ var role = roles[roleId];
62
+ if ( role.capabilities.hasOwnProperty(capability) ) {
63
+ return role.capabilities[capability];
64
+ } else if (roleId == capability) {
65
+ return true;
66
+ }
67
+ return false;
68
+ };
69
+
70
+ me.userHasCap = function(login, capability, context) {
71
+ context = context || {};
72
+ if (!users.hasOwnProperty(login)) {
73
+ throw {
74
+ name: 'UnknownUserException',
75
+ message: 'Can not check capabilities for an unknown user',
76
+ value: login,
77
+ requireCapability: capability
78
+ };
79
+ }
80
+
81
+ var user = users[login];
82
+ if ( user.capabilities.hasOwnProperty(capability) ) {
83
+ return user.capabilities[capability];
84
+ } else {
85
+ //Super Admins have all capabilities, except those explicitly denied.
86
+ //We also need to check if the Super Admin actor is allowed in this context.
87
+ if (user.is_super_admin ) {
88
+ if (context.hasOwnProperty('special:super_admin')) {
89
+ return context['special:super_admin'];
90
+ }
91
+ return (capability != 'do_not_allow');
92
+ }
93
+
94
+ //Check if any of the user's roles have the capability.
95
+ for(var index = 0; index < user.roles.length; index++) {
96
+ var roleId = user.roles[index];
97
+
98
+ //Skip roles that are disabled in this context (i.e. via grant_access).
99
+ if (context.hasOwnProperty('role:' + roleId) && !context['role:' + roleId]) {
100
+ continue;
101
+ }
102
+
103
+ if (me.roleHasCap(roleId, capability)) {
104
+ return true;
105
+ }
106
+ }
107
+ }
108
+
109
+ return false;
110
+ };
111
+
112
+ me.roleExists = function(roleId) {
113
+ return roles.hasOwnProperty(roleId);
114
+ };
115
+
116
+ /**
117
+ * Compare the specificity of two actors.
118
+ *
119
+ * Returns 1 if the first actor is more specific than the second, 0 if they're both
120
+ * equally specific, and -1 if the second actor is more specific.
121
+ *
122
+ * @param {String} actor1
123
+ * @param {String} actor2
124
+ * @return {Number}
125
+ */
126
+ me.compareActorSpecificity = function(actor1, actor2) {
127
+ var delta = me.getActorSpecificity(actor1) - me.getActorSpecificity(actor2);
128
+ if (delta !== 0) {
129
+ delta = (delta > 0) ? 1 : -1;
130
+ }
131
+ return delta;
132
+ };
133
+
134
+ me.getActorSpecificity = function(actorString) {
135
+ var actor = parseActorString(actorString);
136
+ var specificity = 0;
137
+ switch(actor.type) {
138
+ case 'role':
139
+ specificity = 1;
140
+ break;
141
+ case 'special':
142
+ specificity = 2;
143
+ break;
144
+ case 'user':
145
+ specificity = 10;
146
+ break;
147
+ }
148
+ return specificity;
149
+ };
150
+
151
+
152
+ return me;
153
+ })(wsEditorData.roles, wsEditorData.users);
154
+
155
  (function ($){
156
+
157
+ var selectedActor = null;
158
+
159
+ var itemTemplates = {
160
+ templates: wsEditorData.itemTemplates,
161
+
162
+ getTemplateById: function(templateId) {
163
+ if (wsEditorData.itemTemplates.hasOwnProperty(templateId)) {
164
+ return wsEditorData.itemTemplates[templateId];
165
+ } else if ((templateId == '') || (templateId == 'custom')) {
166
+ return wsEditorData.customItemTemplate;
167
+ }
168
+ return null;
169
+ },
170
+
171
+ getDefaults: function (templateId) {
172
+ var template = this.getTemplateById(templateId);
173
+ if (template) {
174
+ return template.defaults;
175
+ } else {
176
+ return null;
177
+ }
178
+ },
179
+
180
+ getDefaultValue: function (templateId, fieldName) {
181
+ if (fieldName == 'template_id') {
182
+ return null;
183
+ }
184
+
185
+ var defaults = this.getDefaults(templateId);
186
+ if (defaults && (typeof defaults[fieldName] != 'undefined')) {
187
+ return defaults[fieldName];
188
+ }
189
+ return null;
190
+ },
191
+
192
+ hasDefaultValue: function(templateId, fieldName) {
193
+ return (this.getDefaultValue(templateId, fieldName) !== null);
194
+ }
195
+ };
196
+
197
+ /**
198
+ * Set an input field to a value. The only difference from jQuery.val() is that
199
+ * setting a checkbox to true/false will check/clear it.
200
+ *
201
+ * @param input
202
+ * @param value
203
+ */
204
+ function setInputValue(input, value) {
205
+ if (input.attr('type') == 'checkbox'){
206
+ if (value){
207
+ input.attr('checked', 'checked');
208
+ } else {
209
+ input.removeAttr('checked');
210
+ }
211
+ } else {
212
+ input.val(value);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Get the value of an input field. The only difference from jQuery.val() is that
218
+ * checked/unchecked checkboxes will return true/false.
219
+ *
220
+ * @param input
221
+ * @return {*}
222
+ */
223
+ function getInputValue(input) {
224
+ if (input.attr('type') == 'checkbox'){
225
+ return input.is(':checked');
226
+ }
227
+ return input.val();
228
+ }
229
+
230
+
231
  /*
232
  * Utility function for generating pseudo-random alphanumeric menu IDs.
233
  * Rationale: Simpler than atomically auto-incrementing or globally unique IDs.
234
  */
235
+ function randomMenuId(prefix, size){
236
+ prefix = (typeof prefix == 'undefined') ? 'custom_item_' : prefix;
237
+ size = (typeof size == 'undefined') ? 5 : size;
238
+
239
+ var suffix = "";
 
240
  var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
241
 
242
+ for( var i=0; i < size; i++ ) {
243
+ suffix += possible.charAt(Math.floor(Math.random() * possible.length));
244
+ }
245
 
246
+ return prefix + suffix;
247
  }
248
+
249
  function outputWpMenu(menu){
250
+ var menuCopy = $.extend(true, {}, menu);
251
+ var menuBox = $('#ws_menu_box');
252
+
253
  //Remove the current menu data
254
+ menuBox.empty();
255
  $('#ws_submenu_box').empty();
256
+
 
 
257
  //Display the new menu
258
  var i = 0;
259
+ for (var filename in menuCopy){
260
+ if (!menuCopy.hasOwnProperty(filename)){
261
+ continue;
262
+ }
263
+ outputTopMenu(menuCopy[filename]);
264
  i++;
265
  }
266
+
267
  //Automatically select the first top-level menu
268
+ menuBox.find('.ws_menu:first').click();
269
  }
270
 
271
  /*
272
  * Create edit widgets for a top-level menu and its submenus and append them all to the DOM.
273
  *
274
+ * Inputs :
275
  * menu - an object containing menu data
276
  * afterNode - if specified, the new menu widget will be inserted after this node. Otherwise,
277
  * it will be added to the end of the list.
281
  function outputTopMenu(menu, afterNode){
282
  //Create a container for menu items, even if there are none
283
  var submenu = buildSubmenu(menu.items);
284
+
285
  //Create the menu widget
286
+ var menu_obj = buildMenuItem(menu, true);
287
  menu_obj.data('submenu_id', submenu.attr('id'));
288
+ submenu.data('parent_menu_id', menu_obj.attr('id'));
289
+
290
  //Display
291
  submenu.appendTo('#ws_submenu_box');
292
+ updateItemEditor(menu_obj);
293
+ if ( (typeof afterNode != 'undefined') && (afterNode != null) ){
294
  $(afterNode).after(menu_obj);
295
  } else {
296
  menu_obj.appendTo('#ws_menu_box');
297
  }
298
+
299
  return {
300
  'menu' : menu_obj,
301
  'submenu' : submenu
302
  };
303
  }
304
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  /*
306
  * Create and populate a submenu container.
307
  */
308
  function buildSubmenu(items){
309
  //Create a container for menu items, even if there are none
310
+ var submenu = $('<div class="ws_submenu" style="display:none;"></div>');
311
  submenu.attr('id', 'ws-submenu-'+(wsIdCounter++));
312
+
313
+ //Only show menus that have items.
314
  //Skip arrays (with a length) because filled menus are encoded as custom objects.
315
  var entry = null;
316
+ if (items) {
317
+ $.each(items, function(index, item) {
318
+ entry = buildMenuItem(item, false);
319
  if ( entry ){
320
+ updateItemEditor(entry);
321
  submenu.append(entry);
322
  }
323
+ });
324
  }
325
+
326
  //Make the submenu sortable
327
  makeBoxSortable(submenu);
328
+
329
  return submenu;
330
  }
331
 
332
+ /**
333
+ * Create an edit widget for a menu item.
334
+ *
335
+ * @param {Object} itemData
336
+ * @param {Boolean} [isTopLevel] Specify if this is a top-level menu or a sub-menu item. Defaults to false (= sub-item).
337
+ * @return {*} The created widget as a jQuery object.
338
  */
339
+ function buildMenuItem(itemData, isTopLevel) {
340
+ isTopLevel = (typeof isTopLevel == 'undefined') ? false : isTopLevel;
341
+
342
+ //Create the menu HTML
343
+ var item = $('<div></div>')
344
+ .attr('class', "ws_container")
345
+ .attr('id', 'ws-menu-item-' + (wsIdCounter++))
346
+ .data('menu_item', itemData)
347
  .data('field_editors_created', false);
348
+
349
+ item.addClass(isTopLevel ? 'ws_menu' : 'ws_item');
350
+ if ( itemData.separator ) {
351
+ item.addClass('ws_menu_separator');
352
+ }
353
+
354
  //Add a header and a container for property editors (to improve performance
355
  //the editors themselves are created later, when the user tries to access them
356
  //for the first time).
357
  var contents = [];
358
  contents.push(
359
  '<div class="ws_item_head">',
360
+ itemData.separator ? '' : '<a class="ws_edit_link"> </a><div class="ws_flag_container"> </div>',
361
+ '<input type="checkbox" class="ws_actor_access_checkbox">',
362
  '<span class="ws_item_title">',
363
+ ((itemData.menu_title != null) ? itemData.menu_title : itemData.defaults.menu_title),
364
  '&nbsp;</span>',
365
+
366
  '</div>',
367
  '<div class="ws_editbox" style="display: none;"></div>'
368
  );
369
  item.append(contents.join(''));
370
+
371
  //Apply flags based on the item's state
372
+ var flags = ['hidden', 'unused', 'custom'];
373
+ for (var i = 0; i < flags.length; i++) {
374
+ setMenuFlag(item, flags[i], getFieldValue(itemData, flags[i], false));
 
 
 
 
 
375
  }
376
+
377
+ if ( isTopLevel && !itemData.separator ){
378
+ //Allow the user to drag menu items to top-level menus
379
+ item.droppable({
380
+ 'hoverClass' : 'ws_menu_drop_hover',
381
+
382
+ 'accept' : (function(thing){
383
+ return thing.hasClass('ws_item');
384
+ }),
385
+
386
+ 'drop' : (function(event, ui){
387
+ var droppedItemData = readItemState(ui.draggable);
388
+ var new_item = buildMenuItem(droppedItemData, false);
389
+
390
+ var sourceSubmenu = ui.draggable.parent();
391
+ var submenu = $('#' + item.data('submenu_id'));
392
+ submenu.append(new_item);
393
+
394
+ if ( !event.ctrlKey ) {
395
+ ui.draggable.remove();
396
+ }
397
+
398
+ updateItemEditor(new_item);
399
+
400
+ //Moving an item can change aggregate menu permissions. Update the UI accordingly.
401
+ updateParentAccessUi(submenu);
402
+ updateParentAccessUi(sourceSubmenu);
403
+ })
404
+ });
405
  }
406
+
407
  return item;
408
  }
409
 
410
+ function jsTrim(str){
411
+ return str.replace(/^\s+|\s+$/g, "");
412
+ }
413
+
414
+ //Editor field spec template.
415
+ var baseField = {
416
+ caption : '[No caption]',
417
+ standardCaption : true,
418
+ advanced : false,
419
+ type : 'text',
420
+ defaultValue: '',
421
+ onlyForTopMenus: false,
422
+ addDropdown : false,
423
+ visible: true,
424
+
425
+ write: null,
426
+ display: null
427
+ };
428
+
429
  /*
430
  * List of all menu fields that have an associated editor
431
+ */
432
  var knownMenuFields = {
433
+ 'menu_title' : $.extend({}, baseField, {
434
  caption : 'Menu title',
435
+ display: function(menuItem, displayValue, input, containerNode) {
436
+ //Update the header as well.
437
+ containerNode.find('.ws_item_title').html(displayValue);
438
+ return displayValue;
439
+ },
440
+ write: function(menuItem, value, input, containerNode) {
441
+ menuItem.menu_title = value;
442
+ containerNode.find('.ws_item_title').html(input.val() + '&nbsp;');
443
+ }
444
+ }),
445
+
446
+ 'template_id' : $.extend({}, baseField, {
447
+ caption : 'Target page',
448
+ type : 'select',
449
+ options : (function(){
450
+ //Generate name => id mappings for all item templates + the special "Custom" template.
451
+ var itemTemplateIds = {};
452
+ itemTemplateIds[wsEditorData.customItemTemplate.name] = '';
453
+ for (var template_id in wsEditorData.itemTemplates) {
454
+ if (wsEditorData.itemTemplates.hasOwnProperty(template_id)) {
455
+ itemTemplateIds[wsEditorData.itemTemplates[template_id].name] = template_id;
456
+ }
457
+ }
458
+ return itemTemplateIds;
459
+ })(),
460
+
461
+ write: function(menuItem, value, input, containerNode) {
462
+ menuItem.template_id = value;
463
+ menuItem.defaults = itemTemplates.getDefaults(menuItem.template_id);
464
+ menuItem.custom = (menuItem.template_id == '');
465
+
466
+ // The file/URL of non-custom items is read-only and equal to the default
467
+ // value. Rationale: simplifies menu generation, prevents some user mistakes.
468
+ if (menuItem.template_id !== '') {
469
+ menuItem.file = null;
470
+ }
471
+
472
+ // The new template might not have default values for some of the fields
473
+ // currently set to null (= "default"). In those cases, we need to make
474
+ // the current values explicit.
475
+ containerNode.find('.ws_edit_field').each(function(index, field){
476
+ field = $(field);
477
+ var fieldName = field.data('field_name');
478
+ var isSetToDefault = (menuItem[fieldName] === null);
479
+ var hasDefaultValue = itemTemplates.hasDefaultValue(menuItem.template_id, fieldName);
480
+
481
+ if (isSetToDefault && !hasDefaultValue) {
482
+ menuItem[fieldName] = getInputValue(field.find('.ws_field_value'));
483
+ }
484
+ });
485
+ }
486
+ }),
487
+
488
+ 'file' : $.extend({}, baseField, {
489
+ caption: 'URL',
490
+ display: function(menuItem, displayValue, input) {
491
+ // The URL/file field is read-only for default menus. Also, since the "file"
492
+ // field is usually set to a page slug or plugin filename for plugin/hook pages,
493
+ // we display the dynamically generated "url" field here (i.e. the actual URL) instead.
494
+ if (menuItem.template_id !== '') {
495
+ input.attr('readonly', 'readonly');
496
+ displayValue = itemTemplates.getDefaultValue(menuItem.template_id, 'url');
497
+ } else {
498
+ input.removeAttr('readonly');
499
+ }
500
+ return displayValue;
501
+ },
502
+
503
+ write: function(menuItem, value) {
504
+ // A menu must always have a non-empty URL. If the user deletes the current value,
505
+ // reset it to the old value.
506
+ if (value === '') {
507
+ value = menuItem.file;
508
+ }
509
+ // Default menus always point to the default file/URL.
510
+ if (menuItem.template_id !== '') {
511
+ value = null;
512
+ }
513
+ menuItem.file = value;
514
+ }
515
+ }),
516
+
517
+ 'access_level' : $.extend({}, baseField, {
518
+ caption: 'Permissions',
519
+ defaultValue: 'read',
520
+ type: 'access_editor',
521
+ visible: false, //Will be set to visible only in Pro version.
522
+
523
+ display: function(menuItem) {
524
+ //Permissions display is a little complicated and could use improvement.
525
+ var requiredCap = getFieldValue(menuItem, 'access_level', '');
526
+ var extraCap = getFieldValue(menuItem, 'extra_capability', '');
527
+
528
+ var displayValue = (menuItem.template_id === '') ? '< Custom >' : requiredCap;
529
+ if (extraCap !== '') {
530
+ if (menuItem.template_id === '') {
531
+ displayValue = extraCap;
532
+ } else {
533
+ displayValue = displayValue + '+' + extraCap;
534
+ }
535
+ }
536
+
537
+ return displayValue;
538
+ },
539
+
540
+ write: function(menuItem) {
541
+ //The required capability can't be directly edited and always equals the default.
542
+ menuItem.access_level = null;
543
+ }
544
+ }),
545
+
546
+ 'extra_capability' : $.extend({}, baseField, {
547
  caption: 'Required capability',
 
 
 
548
  defaultValue: 'read',
549
+ type: 'text',
550
+ addDropdown: 'ws_cap_selector',
551
+
552
+ display: function(menuItem) {
553
+ //Permissions display is a little complicated and could use improvement.
554
+ var requiredCap = getFieldValue(menuItem, 'access_level', '');
555
+ var extraCap = getFieldValue(menuItem, 'extra_capability', '');
556
+
557
+ var displayValue = extraCap;
558
+ if ((extraCap === '') || (extraCap === null)) {
559
+ displayValue = requiredCap;
560
+ }
561
+
562
+ return displayValue;
563
+ },
564
+
565
+ write: function(menuItem, value) {
566
+ value = jsTrim(value);
567
+
568
+ //Reset to default if the user clears the input.
569
+ if (value === '') {
570
+ menuItem.extra_capability = null;
571
+ return;
572
+ }
573
+
574
+ //It would be redundant to set an extra_capability that it matches access_level.
575
+ var requiredCap = getFieldValue(menuItem, 'access_level', '');
576
+ var extraCap = getFieldValue(menuItem, 'extra_capability', '');
577
+ if (extraCap === '' && value === requiredCap) {
578
+ return;
579
+ }
580
+
581
+ menuItem.extra_capability = value;
582
+ }
583
+ }),
584
+
585
+ 'page_title' : $.extend({}, baseField, {
586
  caption: "Window title",
587
  standardCaption : true,
588
+ advanced : true
589
+ }),
590
+
591
+ 'open_in' : $.extend({}, baseField, {
 
 
592
  caption: 'Open in',
 
593
  advanced : true,
594
  type : 'select',
595
  options : {
599
  },
600
  defaultValue: 'same_window',
601
  visible: false
602
+ }),
603
+
604
+ 'css_class' : $.extend({}, baseField, {
605
  caption: 'CSS classes',
 
606
  advanced : true,
607
+ onlyForTopMenus: true
608
+ }),
609
+
610
+ 'icon_url' : $.extend({}, baseField, {
 
611
  caption: 'Icon URL',
 
 
612
  type : 'icon_selector',
613
+ advanced : true,
614
  defaultValue: 'div',
615
+ onlyForTopMenus: true,
616
+
617
+ display: function(menuItem, displayValue, input) {
618
+ //Display the current icon in the selector.
619
+ var cssClass = getFieldValue(menuItem, 'css_class', '');
620
+ var iconUrl = getFieldValue(menuItem, 'icon_url', '');
621
+
622
+ var selectButton = input.closest('.ws_edit_field').find('.ws_select_icon');
623
+ var cssIcon = selectButton.find('.icon16');
624
+ var imageIcon = selectButton.find('img');
625
+
626
+ var matches = cssClass.match(/\bmenu-icon-([^\s]+)\b/);
627
+ //Icon URL take precedence over icon class.
628
+ if ( iconUrl && iconUrl !== 'none' && iconUrl !== 'div' ) {
629
+ cssIcon.hide();
630
+ imageIcon.prop('src', iconUrl).show();
631
+ } else if ( matches ) {
632
+ imageIcon.hide();
633
+ cssIcon.removeClass().addClass('icon16 icon-' + matches[1]).show();
634
+ } else {
635
+ //This menu has no icon at all. This is actually a valid state
636
+ //and WordPress will display a menu like that correctly.
637
+ imageIcon.hide();
638
+ cssIcon.removeClass().addClass('icon16').show();
639
+ }
640
+
641
+ return displayValue;
642
+ }
643
+ }),
644
+
645
+ 'hookname' : $.extend({}, baseField, {
646
  caption: 'Hook name',
 
647
  advanced : true,
648
+ onlyForTopMenus: true
649
+ })
 
 
 
 
 
 
 
 
 
 
650
  };
651
 
652
  /*
653
  * Create editors for the visible fields of a menu entry and append them to the specified node.
654
  */
655
+ function buildEditboxFields(fieldContainer, entry, isTopLevel){
656
+ isTopLevel = (typeof isTopLevel == 'undefined') ? false : isTopLevel;
657
+
658
+ var basicFields = $('<div class="ws_edit_panel ws_basic"></div>').appendTo(fieldContainer);
659
+ var advancedFields = $('<div class="ws_edit_panel ws_advanced"></div>').appendTo(fieldContainer);
660
+
661
+ if ( wsEditorData.hideAdvancedSettings ){
662
  advancedFields.css('display', 'none');
663
  }
664
+
665
+ for (var field_name in knownMenuFields){
666
+ if (!knownMenuFields.hasOwnProperty(field_name)) {
667
+ continue;
668
+ }
669
+
670
+ var fieldSpec = knownMenuFields[field_name];
671
+ if (fieldSpec.onlyForTopMenus && !isTopLevel) {
672
  continue;
673
  }
674
+
675
+ var field = buildEditboxField(entry, field_name, fieldSpec);
676
  if (field){
677
+ if (fieldSpec.advanced){
678
  advancedFields.append(field);
679
  } else {
680
  basicFields.append(field);
681
  }
 
 
 
 
682
  }
683
  }
684
+
685
  //Add a link that shows/hides advanced fields
686
+ fieldContainer.append(
687
  '<div class="ws_toggle_container"><a href="#" class="ws_toggle_advanced_fields"'+
688
+ (wsEditorData.hideAdvancedSettings ? '' : ' style="display:none;"')+'>'+
689
+ (wsEditorData.hideAdvancedSettings ? wsEditorData.captionShowAdvanced : wsEditorData.captionHideAdvanced)
690
  +'</a></div>'
691
  );
692
  }
698
  if (typeof entry[field_name] === 'undefined') {
699
  return null; //skip fields this entry doesn't have
700
  }
701
+
702
+ //Build a form field of the appropriate type
 
 
 
703
  var inputBox = null;
704
  var basicTextField = '<input type="text" class="ws_field_value">';
705
+ //noinspection FallthroughInSwitchStatementJS
706
  switch(field_settings.type){
707
  case 'select':
708
  inputBox = $('<select class="ws_field_value">');
709
  var option = null;
710
  for( var optionTitle in field_settings.options ){
711
+ if (!field_settings.options.hasOwnProperty(optionTitle)) {
712
+ continue;
713
+ }
714
  option = $('<option>')
715
  .val(field_settings.options[optionTitle])
716
  .text(optionTitle);
 
 
 
717
  option.appendTo(inputBox);
718
  }
719
  break;
720
+
721
  case 'checkbox':
722
+ inputBox = $('<label><input type="checkbox" class="ws_field_value"> '+
723
  field_settings.caption+'</label>'
724
  );
725
  break;
726
 
727
+ case 'access_editor':
728
+ inputBox = $('<input type="text" class="ws_field_value" readonly="readonly">')
729
+ .add('<input type="button" class="button ws_launch_access_editor" value="Edit...">');
730
+ break;
731
+
732
  case 'icon_selector':
733
+ inputBox = $(basicTextField)
734
  .add('<button class="button ws_select_icon" title="Select icon"><div class="icon16 icon-settings"></div><img src="" style="display:none;"></button>');
735
  break;
736
+
737
+ case 'text': //Intentional fall-through.
738
  default:
739
+ inputBox = $(basicTextField);
740
  }
741
+
742
+
743
  var className = "ws_edit_field ws_edit_field-"+field_name;
744
+ if (field_settings.addDropdown){
 
 
 
 
 
745
  className += ' ws_has_dropdown';
746
  }
747
+
748
+ var editField = $('<div>' + (field_settings.standardCaption ? (field_settings.caption+'<br>') : '') + '</div>')
749
  .attr('class', className)
750
  .append(inputBox);
751
+
752
+ if (field_settings.addDropdown) {
753
  //Add a dropdown button
754
+ var dropdownId = field_settings.addDropdown;
 
 
 
755
  editField.append(
756
  $('<input type="button" value="&#9660;">')
757
  .addClass('button ws_dropdown_button')
759
  .data('dropdownId', dropdownId)
760
  );
761
  }
762
+
763
  editField
764
+ .append('<img src="' + wsEditorData.imagesUrl + '/transparent16.png" class="ws_reset_button" title="Reset to default value">&nbsp;</img>')
765
+ .data('field_name', field_name);
766
+
 
767
  if ( !field_settings.visible ){
768
  editField.css('display', 'none');
769
  }
770
+
771
+ return editField;
772
+ }
773
+
774
+ /**
775
+ * Update the UI elements that that indicate whether the currently selected
776
+ * actor can access a menu item.
777
+ *
778
+ * @param containerNode
779
+ */
780
+ function updateActorAccessUi(containerNode) {
781
+ //Update the permissions checkbox & UI
782
+ if (selectedActor != null) {
783
+ var menuItem = containerNode.data('menu_item');
784
+ var hasAccess = actorCanAccessMenu(menuItem, selectedActor);
785
+
786
+ var checkbox = containerNode.find('.ws_actor_access_checkbox');
787
+ checkbox.prop('checked', hasAccess);
788
+
789
+ //Display the checkbox differently if some items of this menu are hidden and some are visible,
790
+ //or if their permissions don't match this menu's permissions.
791
+ var submenuId = containerNode.data('submenu_id');
792
+ var submenuItems = submenuId ? $('#' + submenuId).children('.ws_container') : [];
793
+ if (!submenuId || submenuItems.length === 0) {
794
+ //This menu doesn't contain any items.
795
+ checkbox.prop('indeterminate', false);
796
+ } else {
797
+ var differentPermissions = false;
798
+ submenuItems.each(function() {
799
+ var item = $(this).data('menu_item');
800
+ if ( !item ) { //Skip placeholder items created by drag & drop operations.
801
+ return true;
802
+ }
803
+ var hasSubmenuAccess = actorCanAccessMenu(item, selectedActor);
804
+ if (hasSubmenuAccess !== hasAccess) {
805
+ differentPermissions = true;
806
+ return false;
807
+ }
808
+ return true;
809
+ });
810
+
811
+ checkbox.prop('indeterminate', differentPermissions);
812
+ }
813
+
814
+ containerNode.toggleClass('ws_is_hidden_for_actor', !hasAccess);
815
+ } else {
816
+ containerNode.removeClass('ws_is_hidden_for_actor');
817
+ }
818
  }
819
 
820
+ /**
821
+ * Like updateActorAccessUi() except it updates the specified menu's parent, not the menu itself.
822
+ * If the menu has no parent (i.e. it's a top-level menu), this function does nothing.
823
+ *
824
+ * @param containerNode Either a menu item or a submenu container.
825
+ */
826
+ function updateParentAccessUi(containerNode) {
827
+ var submenu;
828
+ if ( containerNode.is('.ws_submenu') ) {
829
+ submenu = containerNode;
 
 
 
 
 
 
 
 
830
  } else {
831
+ submenu = containerNode.parent();
832
+ }
833
+
834
+ var parentId = submenu.data('parent_menu_id');
835
+ if (parentId) {
836
+ updateActorAccessUi($('#' + parentId));
837
  }
838
  }
839
 
840
+ /**
841
+ * Update an edit widget with the current menu item settings.
842
+ *
843
+ * @param containerNode
844
  */
845
+ function updateItemEditor(containerNode) {
846
+ var menuItem = containerNode.data('menu_item');
847
+
848
+ //Apply flags based on the item's state.
849
+ var flags = ['hidden', 'unused', 'custom'];
850
+ for (var i = 0; i < flags.length; i++) {
851
+ setMenuFlag(containerNode, flags[i], getFieldValue(menuItem, flags[i], false));
 
852
  }
853
+
854
+ //Update the permissions checkbox & other actor-specific UI
855
+ updateActorAccessUi(containerNode);
856
+
857
+ //Update all input fields with the current values.
858
+ containerNode.find('.ws_edit_field').each(function(index, field) {
859
+ field = $(field);
860
+ var fieldName = field.data('field_name');
861
+ var input = field.find('.ws_field_value').first();
862
+
863
+ var hasADefaultValue = itemTemplates.hasDefaultValue(menuItem.template_id, fieldName);
864
+ var defaultValue = itemTemplates.getDefaultValue(menuItem.template_id, fieldName);
865
+ var isDefault = hasADefaultValue && (menuItem[fieldName] === null);
866
+
867
+ if (fieldName == 'access_level') {
868
+ isDefault = (getFieldValue(menuItem, 'extra_capability', '') === '') && isEmptyObject(menuItem.grant_access);
869
+ }
870
+
871
+ field.toggleClass('ws_has_no_default', !hasADefaultValue);
872
+ field.toggleClass('ws_input_default', isDefault);
873
+
874
+ var displayValue = isDefault ? defaultValue : menuItem[fieldName];
875
+ if (knownMenuFields[fieldName].display !== null) {
876
+ displayValue = knownMenuFields[fieldName].display(menuItem, displayValue, input, containerNode);
877
+ }
878
+
879
+ if (fieldName == 'access_level') {
880
+ //Permissions display is a little complicated and could use improvement.
881
+ var requiredCap = getFieldValue(menuItem, 'access_level', '');
882
+ var extraCap = getFieldValue(menuItem, 'extra_capability', '');
883
+
884
+ displayValue = (menuItem.template_id === '') ? '< Custom >' : requiredCap;
885
+ if (extraCap !== '') {
886
+ if (menuItem.template_id === '') {
887
+ displayValue = extraCap;
888
+ } else {
889
+ displayValue = displayValue + '+' + extraCap;
890
+ }
891
+ }
892
+ }
893
+
894
+ setInputValue(input, displayValue);
895
+ });
896
+ }
897
+
898
+ function isEmptyObject(obj) {
899
+ for (var prop in obj) {
900
+ if (obj.hasOwnProperty(prop)) {
901
+ return false;
902
+ }
903
+ }
904
+ return true;
905
  }
906
 
907
  /*
908
+ * Get the current value of a single menu field.
909
  *
910
+ * If the specified field is not set, this function will attempt to retrieve it
911
  * from the "defaults" property of the menu object. If *that* fails, it will return
912
+ * the value of the optional third argument defaultValue.
913
  */
914
  function getFieldValue(entry, fieldName, defaultValue){
915
  if ( (typeof entry[fieldName] === 'undefined') || (entry[fieldName] === null) ) {
940
  Parsing & encoding menu inputs
941
  ***************************************************************************/
942
 
943
+ /**
944
  * Encode the current menu structure as JSON
945
  *
946
+ * @return {String} A JSON-encoded string representing the current menu tree loaded in the editor.
 
947
  */
948
+ function encodeMenuAsJSON(tree){
949
+ if (typeof tree == 'undefined' || !tree) {
950
+ tree = readMenuTreeState();
951
+ }
952
+ tree.format = {
953
+ name: wsEditorData.menuFormatName,
954
+ version: wsEditorData.menuFormatVersion
955
+ };
956
  return $.toJSON(tree);
957
  }
958
 
959
  function readMenuTreeState(){
960
  var tree = {};
961
  var menu_position = 0;
962
+
963
  //Gather all menus and their items
964
+ $('#ws_menu_box').find('.ws_menu').each(function() {
965
+ var menu = readItemState(this, menu_position++);
966
+
967
  //Attach the current menu to the main struct
968
  var filename = (menu.file !== null)?menu.file:menu.defaults.file;
969
  tree[filename] = menu;
970
  });
 
 
 
971
 
972
+ return {
973
+ tree: tree
974
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
975
  }
976
 
977
+ /**
978
  * Extract the current menu item settings from its editor widget.
979
  *
980
+ * @param itemDiv DOM node containing the editor widget, usually with the .ws_item or .ws_menu class.
981
+ * @param {Number} [position] Menu item position among its sibling menu items. Defaults to zero.
982
+ * @return {Object} A menu object in the tree format.
 
983
  */
984
+ function readItemState(itemDiv, position){
985
+ position = (typeof position == 'undefined') ? 0 : position;
986
+
987
+ itemDiv = $(itemDiv);
988
+ var item = $.extend({}, wsEditorData.blankMenuItem, itemDiv.data('menu_item'), readAllFields(itemDiv));
989
+
990
+ item.defaults = itemDiv.data('menu_item').defaults;
991
+
992
  //Save the position data
 
 
 
993
  item.position = position;
994
+ item.defaults.position = position; //The real default value will later overwrite this
995
+
996
+ item.separator = itemDiv.hasClass('ws_menu_separator');
997
+ item.hidden = menuHasFlag(itemDiv, 'hidden');
998
+ item.custom = menuHasFlag(itemDiv, 'custom');
999
+
1000
+ //Gather the menu's sub-items, if any
1001
+ item.items = [];
1002
+ var subMenuId = itemDiv.data('submenu_id');
1003
+ if (subMenuId) {
1004
+ var itemPosition = 0;
1005
+ $('#' + subMenuId).find('.ws_item').each(function () {
1006
+ var sub_item = readItemState(this, itemPosition++);
1007
+ item.items.push(sub_item);
1008
+ });
1009
+ }
1010
+
1011
  return item;
1012
  }
1013
 
1015
  * Extract the values of all menu/item fields present in a container node
1016
  *
1017
  * Inputs:
1018
+ * container - a jQuery collection representing the node to read.
1019
  */
1020
  function readAllFields(container){
1021
  if ( !container.hasClass('ws_container') ){
1022
+ container = container.closest('.ws_container');
1023
  }
1024
+
1025
  if ( !container.data('field_editors_created') ){
1026
+ return container.data('menu_item');
1027
  }
1028
+
1029
  var state = {};
1030
+
1031
  //Iterate over all fields of the item
1032
  container.find('.ws_edit_field').each(function() {
1033
  var field = $(this);
1034
+
1035
  //Get the name of this field
1036
+ var field_name = field.data('field_name');
1037
  //Skip if unnamed
1038
+ if (!field_name) {
1039
+ return true;
1040
+ }
1041
+
1042
  //Find the field (usually an input or select element).
1043
+ var input_box = field.find('.ws_field_value');
1044
+
1045
  //Save null if default used, custom value otherwise
1046
  if (field.hasClass('ws_input_default')){
1047
  state[field_name] = null;
1048
  } else {
1049
+ state[field_name] = getInputValue(input_box);
 
 
 
 
1050
  }
1051
+ return true;
1052
  });
1053
+
1054
+ //Permission settings are not stored in the visible access_level field (that's just for show),
1055
+ //so do not attempt to read them from there.
1056
+ state['access_level'] = null;
1057
+
1058
  return state;
1059
  }
1060
 
1061
 
1062
  /***************************************************************************
1063
+ Flag manipulation
1064
  ***************************************************************************/
1065
 
1066
  var item_flags = {
1067
+ 'custom':'This is a custom menu item',
1068
+ 'unused':'This item was automatically (re)inserted into your custom menu because it is present in the default WordPress menu',
1069
+ 'hidden':'This item is hidden'
1070
+ };
1071
+
1072
+ function setMenuFlag(item, flag, state) {
 
1073
  item = $(item);
1074
+
1075
  var item_class = 'ws_' + flag;
1076
  var img_class = 'ws_' + flag + '_flag';
1077
+
1078
+ item.toggleClass(item_class, state);
1079
+ if (state) {
1080
+ //Add the flag image,
1081
+ var flag_container = item.find('.ws_flag_container');
1082
+ if ( flag_container.find('.' + img_class).length == 0 ){
1083
+ flag_container.append('<div class="ws_flag '+img_class+'" title="'+item_flags[flag]+'"></div>');
1084
+ }
1085
+ } else {
1086
+ //Remove the flag image.
1087
+ item.find('.' + img_class).remove();
1088
  }
1089
  }
1090
 
1091
+ function menuHasFlag(item, flag){
1092
+ return $(item).hasClass('ws_'+flag);
 
 
 
 
 
1093
  }
1094
 
1095
+ /***********************************************************
1096
+ Capability manipulation
1097
+ ************************************************************/
1098
+
1099
+ function actorCanAccessMenu(menuItem, actor) {
1100
+ if (!$.isPlainObject(menuItem.grant_access)) {
1101
+ menuItem.grant_access = {};
1102
+ }
1103
+
1104
+ //By default, any actor that has the required cap has access to the menu.
1105
+ //Users can override this on a per-menu basis.
1106
+ var requiredCap = getFieldValue(menuItem, 'access_level', '< Error: access_level is missing! >');
1107
+ var actorHasAccess = false;
1108
+ if (menuItem.grant_access.hasOwnProperty(actor)) {
1109
+ actorHasAccess = menuItem.grant_access[actor];
1110
  } else {
1111
+ actorHasAccess = AmeCapabilityManager.hasCap(actor, requiredCap, menuItem.grant_access);
1112
  }
1113
+ return actorHasAccess;
1114
  }
1115
 
1116
+ function setActorAccess(containerNode, actor, allowAccess) {
1117
+ var menuItem = containerNode.data('menu_item');
1118
+
1119
+ //grant_access comes from PHP, which JSON-encodes empty assoc. arrays as arrays.
1120
+ //However, we want it to be a dictionary.
1121
+ if (!$.isPlainObject(menuItem.grant_access)) {
1122
+ menuItem.grant_access = {};
1123
+ }
1124
+
1125
+ menuItem.grant_access[actor] = allowAccess;
1126
  }
1127
 
1128
+ function setSelectedActor(actor) {
1129
+ //Check if the specified actor really exists. The actor ID
1130
+ //could be invalid if it was supplied by the user.
1131
+ if (actor !== null) {
1132
+ var newSelectedItem = $('a[href$="#'+ actor +'"]');
1133
+ if (newSelectedItem.length === 0) {
1134
+ return;
1135
+ }
1136
+ }
1137
+
1138
+ selectedActor = actor;
1139
+
1140
+ //Highlight the actor.
1141
+ var actorSelector = $('#ws_actor_selector');
1142
+ $('.current', actorSelector).removeClass('current');
1143
+
1144
+ if (selectedActor == null) {
1145
+ $('a.ws_no_actor').addClass('current');
1146
+ } else {
1147
+ newSelectedItem.addClass('current');
1148
+ }
1149
+
1150
+ //There are some UI elements that can be visible or hidden depending on whether an actor is selected.
1151
+ var editorNode = $('#ws_menu_editor');
1152
+ editorNode.toggleClass('ws_is_actor_view', (selectedActor != null));
1153
+
1154
+ //Update the menu item states to indicate whether they're accessible.
1155
+ if (selectedActor != null) {
1156
+ editorNode.find('.ws_container').each(function() {
1157
+ updateActorAccessUi($(this));
1158
+ });
1159
+ } else {
1160
+ editorNode.find('.ws_is_hidden_for_actor').removeClass('ws_is_hidden_for_actor');
1161
+ }
1162
+ }
1163
+
1164
+ /**
1165
+ * Make a menu item inaccessible to everyone except a particular actor.
1166
+ *
1167
+ * Will not change access settings for actors that are more specific than the input actor.
1168
+ * For example, if the input actor is a "role:", this function will only disable other roles,
1169
+ * but will leave "user:" actors untouched.
1170
+ *
1171
+ * @param {Object} menuItem
1172
+ * @param {String} actor
1173
+ * @return {Object}
1174
+ */
1175
+ function denyAccessForAllExcept(menuItem, actor) {
1176
+ //grant_access comes from PHP, which JSON-encodes empty assoc. arrays as arrays.
1177
+ //However, we want it to be a dictionary.
1178
+ if (!$.isPlainObject(menuItem.grant_access)) {
1179
+ menuItem.grant_access = {};
1180
+ }
1181
+
1182
+ $.each(wsEditorData.actors, function(otherActor) {
1183
+ //If the input actor is more or equally specific...
1184
+ if (AmeCapabilityManager.compareActorSpecificity(actor, otherActor) >= 0) {
1185
+ menuItem.grant_access[otherActor] = false;
1186
+ }
1187
+ });
1188
+ menuItem.grant_access[actor] = true;
1189
+ return menuItem;
1190
  }
1191
 
1192
+ /***************************************************************************
1193
+ Event handlers
1194
+ ***************************************************************************/
1195
+
1196
  //Cut & paste stuff
1197
  var menu_in_clipboard = null;
 
 
1198
  var ws_paste_count = 0;
1199
 
1200
  $(document).ready(function(){
1201
+ //Some editor elements are only available in the Pro version.
1202
+ if (wsEditorData.wsMenuEditorPro) {
1203
  knownMenuFields['open_in'].visible = true;
1204
+ knownMenuFields['access_level'].visible = true;
1205
+ knownMenuFields['extra_capability'].visible = false; //Superseded by the "access_level" field.
1206
+ $('.ws_hide_if_pro').hide();
1207
  }
1208
+
1209
  //Make the top menu box sortable (we only need to do this once)
1210
  var mainMenuBox = $('#ws_menu_box');
1211
  makeBoxSortable(mainMenuBox);
1212
+
1213
  /***************************************************************************
1214
  Event handlers for editor widgets
1215
  ***************************************************************************/
1216
  var menuEditorNode = $('#ws_menu_editor');
1217
+
1218
  //Highlight the clicked menu item and show it's submenu
1219
  var currentVisibleSubmenu = null;
1220
+ menuEditorNode.on('click', '.ws_container', (function () {
1221
  var container = $(this);
1222
  if ( container.hasClass('ws_active') ){
1223
  return;
1224
  }
1225
+
1226
  //Highlight the active item and un-highlight the previous one
1227
+ container.addClass('ws_active');
1228
  container.siblings('.ws_active').removeClass('ws_active');
1229
  if ( container.hasClass('ws_menu') ){
1230
  //Show/hide the appropriate submenu
1234
  currentVisibleSubmenu = $('#'+container.data('submenu_id')).show();
1235
  }
1236
  }));
1237
+
1238
  //Show/hide a menu's properties
1239
+ menuEditorNode.on('click', '.ws_edit_link', (function () {
1240
+ var container = $(this).parents('.ws_container').first();
1241
  var box = container.find('.ws_editbox');
1242
+
1243
+ //For performance, the property editors for each menu are only created
1244
  //when the user tries to access access them for the first time.
1245
  if ( !container.data('field_editors_created') ){
1246
+ buildEditboxFields(box, container.data('menu_item'), container.hasClass('ws_menu'));
1247
  container.data('field_editors_created', true);
1248
+ updateItemEditor(container);
1249
  }
1250
+
1251
  $(this).toggleClass('ws_edit_link_expanded');
1252
  //show/hide the editbox
1253
  if ($(this).hasClass('ws_edit_link_expanded')){
1258
  box.hide();
1259
  }
1260
  }));
1261
+
1262
  //The "Default" button : Reset to default value when clicked
1263
+ menuEditorNode.on('click', '.ws_reset_button', (function () {
1264
+ //Find the field div (it holds the field name)
1265
+ var field = $(this).parents('.ws_edit_field');
1266
+ var fieldName = field.data('field_name');
1267
  //Find the related input field
1268
  var input = field.find('.ws_field_value');
1269
+
1270
+ if ( (input.length > 0) && (field.length > 0) && fieldName ) {
1271
+ //Extract the default value from the menu item.
1272
+ var containerNode = field.closest('.ws_container');
1273
+ var menuItem = containerNode.data('menu_item');
1274
+
1275
+ if (fieldName == 'access_level') {
1276
+ //This is a pretty nasty hack.
1277
+ menuItem.grant_access = {};
1278
+ menuItem.extra_capability = null;
1279
  }
1280
+
1281
+ if (itemTemplates.hasDefaultValue(menuItem.template_id, fieldName)) {
1282
+ menuItem[fieldName] = null;
1283
+ updateItemEditor(containerNode);
1284
+ updateParentAccessUi(containerNode);
1285
+ }
1286
+ }
1287
  }));
1288
 
1289
  //When a field is edited, change it's appearance if it's contents don't match the default value.
1290
  function fieldValueChange(){
1291
  var input = $(this);
1292
  var field = input.parents('.ws_edit_field').first();
1293
+ var fieldName = field.data('field_name');
1294
+
1295
+ if (fieldName == 'access_level') {
1296
+ //This field is read-only and can never be directly edited by the user.
1297
+ //Ignore spurious change events.
1298
+ return;
1299
  }
1300
+
1301
+ var containerNode = field.parents('.ws_container').first();
1302
+ var menuItem = containerNode.data('menu_item');
1303
+
1304
+ var oldValue = menuItem[fieldName];
1305
+ var value = getInputValue(input);
1306
+ var defaultValue = itemTemplates.getDefaultValue(menuItem.template_id, fieldName);
1307
+ var hasADefaultValue = (defaultValue !== null);
1308
+
1309
+ //Some fields/templates have no default values.
1310
+ field.toggleClass('ws_has_no_default', !hasADefaultValue);
1311
+ if (!hasADefaultValue) {
1312
+ field.removeClass('ws_input_default');
1313
+ }
1314
+
1315
+ if (field.hasClass('ws_input_default') && (value == defaultValue)) {
1316
+ value = null; //null = use default.
1317
+ }
1318
+
1319
+ //Ignore changes where the new value is the same as the old one.
1320
+ if (value === oldValue) {
1321
+ return;
1322
+ }
1323
+
1324
+ //Update the item.
1325
+ if (knownMenuFields[fieldName].write !== null) {
1326
+ knownMenuFields[fieldName].write(menuItem, value, input, containerNode);
1327
+ } else {
1328
+ menuItem[fieldName] = value;
1329
+ }
1330
+
1331
+ updateItemEditor(containerNode);
1332
+ updateParentAccessUi(containerNode)
1333
  }
1334
  menuEditorNode.on('click change', '.ws_field_value', fieldValueChange);
1335
 
1336
  //Show/hide advanced fields
1337
+ menuEditorNode.on('click', '.ws_toggle_advanced_fields', function(){
1338
  var self = $(this);
1339
  var advancedFields = self.parents('.ws_container').first().find('.ws_advanced');
1340
+
1341
  if ( advancedFields.is(':visible') ){
1342
  advancedFields.hide();
1343
+ self.text(wsEditorData.captionShowAdvanced);
1344
  } else {
1345
  advancedFields.show();
1346
+ self.text(wsEditorData.captionHideAdvanced);
1347
  }
1348
+
1349
  return false;
1350
  });
1351
+
1352
+ //Allow/forbid items in actor-specific views
1353
+ menuEditorNode.on('click', 'input.ws_actor_access_checkbox', function() {
1354
+ if (selectedActor == null) {
1355
+ return;
1356
+ }
1357
+
1358
+ var checked = $(this).is(':checked');
1359
+ var containerNode = $(this).closest('.ws_container');
1360
+
1361
+ setActorAccessForTreeAndUpdateUi(containerNode, selectedActor, checked);
1362
+ });
1363
+
1364
+ /**
1365
+ * This confusingly named function sets actor access for the specified menu item
1366
+ * and all of its children (if any). It also updates the UI with the new settings.
1367
+ *
1368
+ * (And it violates SRP in a particularly egregious manner.)
1369
+ *
1370
+ * @param containerNode
1371
+ * @param {String} actor
1372
+ * @param {Boolean} allowAccess
1373
+ */
1374
+ function setActorAccessForTreeAndUpdateUi(containerNode, actor, allowAccess) {
1375
+ setActorAccess(containerNode, actor, allowAccess);
1376
+
1377
+ //Apply the same permissions to sub-menus.
1378
+ var subMenuId = containerNode.data('submenu_id');
1379
+ if (subMenuId && containerNode.hasClass('ws_menu')) {
1380
+ $('.ws_item', '#' + subMenuId).each(function() {
1381
+ var node = $(this);
1382
+ setActorAccess(node, actor, allowAccess);
1383
+ updateItemEditor(node);
1384
+ });
1385
+ }
1386
+
1387
+ updateItemEditor(containerNode);
1388
+ updateParentAccessUi(containerNode);
1389
+ }
1390
+
1391
+ /*************************************************************************
1392
+ Access editor dialog
1393
+ *************************************************************************/
1394
+
1395
+ var accessEditorState = {
1396
+ containerNode : null,
1397
+ menuItem: null,
1398
+ rowPrefix: 'access_settings_for-'
1399
+ };
1400
+
1401
+ $('#ws_menu_access_editor').dialog({
1402
+ autoOpen: false,
1403
+ closeText: ' ',
1404
+ modal: true,
1405
+ minHeight: 100,
1406
+ draggable: false
1407
+ });
1408
+
1409
+ menuEditorNode.on('click', '.ws_launch_access_editor', function() {
1410
+ var containerNode = $(this).parents('.ws_container').first();
1411
+ var menuItem = containerNode.data('menu_item');
1412
+
1413
+ //Write the values of this item to the editor fields.
1414
+ var editor = $('#ws_menu_access_editor');
1415
+
1416
+ var requiredCap = getFieldValue(menuItem, 'access_level', '< Error: access_level is missing! >');
1417
+ var requiredCapField = editor.find('#ws_required_capability').empty();
1418
+ if (menuItem.template_id === '') {
1419
+ //Custom items have no required caps, only what users set.
1420
+ requiredCapField.empty().append('<em>None</em>');
1421
+ } else {
1422
+ requiredCapField.text(requiredCap);
1423
+ }
1424
+
1425
+ editor.find('#ws_extra_capability').val(getFieldValue(menuItem, 'extra_capability', ''));
1426
+
1427
+ //Generate the actor list.
1428
+ var table = editor.find('.ws_role_table_body tbody').empty();
1429
+ var alternate = '';
1430
+ for(var actor in wsEditorData.actors) {
1431
+ if (!wsEditorData.actors.hasOwnProperty(actor)) {
1432
+ continue;
1433
+ }
1434
+ var actorName = wsEditorData.actors[actor];
1435
+
1436
+ var checkboxId = 'allow_' + actor.replace(/[^a-zA-Z0-9_]/g, '_');
1437
+ var checkbox = $('<input type="checkbox">').addClass('ws_role_access').attr('id', checkboxId);
1438
+
1439
+ var actorHasAccess = actorCanAccessMenu(menuItem, actor);
1440
+ if (actorHasAccess) {
1441
+ checkbox.attr('checked', 'checked');
1442
+ }
1443
+
1444
+ alternate = (alternate == '') ? 'alternate' : '';
1445
+
1446
+ var cell = '<td>';
1447
+ var row = $('<tr>').data('actor', actor).attr('class', alternate).append(
1448
+ $(cell).addClass('ws_column_role post-title').append(
1449
+ $('<label>').attr('for', checkboxId).append(
1450
+ $('<strong>').text(actorName)
1451
+ )
1452
+ ),
1453
+ $(cell).addClass('ws_column_access').append(checkbox)
1454
+ );
1455
+
1456
+ table.append(row);
1457
+ }
1458
+
1459
+ accessEditorState.containerNode = containerNode;
1460
+ accessEditorState.menuItem = menuItem;
1461
+
1462
+ //Show/hide the hint about sub menus overriding menu permissions.
1463
+ var itemHasSubmenus = containerNode.data('submenu_id') &&
1464
+ $('#' + containerNode.data('submenu_id')).find('.ws_item').length > 0;
1465
+ var hintIsEnabled = !wsEditorData.showHints.hasOwnProperty('ws_hint_menu_permissions') || wsEditorData.showHints['ws_hint_menu_permissions'];
1466
+ $('#ws_hint_menu_permissions').toggle(hintIsEnabled && itemHasSubmenus);
1467
+
1468
+ //Warn the user if the required capability == role. Can't make it less restrictive.
1469
+ var roleError = $('#ws_hardcoded_role_error');
1470
+ if (requiredCap && AmeCapabilityManager.roleExists(requiredCap)) {
1471
+ roleError.show();
1472
+ $('#ws_hardcoded_role_name').text(requiredCap);
1473
+ } else {
1474
+ roleError.hide();
1475
+ }
1476
+
1477
+ editor.dialog('open');
1478
+ });
1479
+
1480
+ $('#ws_save_access_settings').click(function() {
1481
+ //Save the new settings.
1482
+ accessEditorState.menuItem.extra_capability = $('#ws_extra_capability').val();
1483
+
1484
+ var grantAccess = accessEditorState.menuItem.grant_access;
1485
+ if (!$.isPlainObject(grantAccess)) {
1486
+ grantAccess = {};
1487
+ }
1488
+ var editor = $('#ws_menu_access_editor');
1489
+ editor.find('.ws_role_table_body tbody tr').each(function() {
1490
+ var row = $(this);
1491
+ var actor = row.data('actor');
1492
+ grantAccess[actor] = row.find('input.ws_role_access').is(':checked');
1493
+ });
1494
+ accessEditorState.menuItem.grant_access = grantAccess;
1495
+
1496
+ updateItemEditor(accessEditorState.containerNode);
1497
+ editor.dialog('close');
1498
+ });
1499
+
1500
+ /***************************************************************************
1501
+ General dialog handlers
1502
+ ***************************************************************************/
1503
+
1504
+ $(document).on('click', '.ws_close_dialog', function() {
1505
+ $(this).parents('.ui-dialog-content').dialog('close');
1506
+ });
1507
+
1508
+
1509
  /***************************************************************************
1510
+ Drop-down list for combo-box fields
1511
  ***************************************************************************/
1512
 
1513
+ var capSelectorDropdown = $('#ws_cap_selector');
1514
+ var currentDropdownOwner = null; //The input element that the dropdown is currently associated with.
1515
+ var isDropdownBeingHidden = false;
1516
+
1517
+ //Show/hide the capability drop-down list when the trigger button is clicked
1518
+ $('#ws_trigger_capability_dropdown').on('mousedown click', onDropdownTriggerClicked);
1519
+ menuEditorNode.on('mousedown click', '.ws_dropdown_button', onDropdownTriggerClicked);
1520
+
1521
+ function onDropdownTriggerClicked(event){
1522
+ var inputBox = null;
 
 
 
 
 
1523
  var button = $(this);
1524
+
1525
+ //Find the input associated with the button that was clicked.
1526
+ if ( button.attr('id') == 'ws_trigger_capability_dropdown' ) {
1527
+ inputBox = $('#ws_extra_capability');
1528
+ } else {
1529
+ inputBox = button.closest('.ws_edit_field').find('.ws_field_value').first();
1530
+ }
1531
+
1532
+ //If the user clicks the same button again while the dropdown is already visible,
1533
+ //ignore the click. The dropdown will be hidden by its "blur" handler.
1534
+ if (event.type == 'mousedown') {
1535
+ if ( capSelectorDropdown.is(':visible') && inputBox.is(currentDropdownOwner) ) {
1536
+ isDropdownBeingHidden = true;
1537
+ }
1538
+ return;
1539
+ } else if (isDropdownBeingHidden) {
1540
+ isDropdownBeingHidden = false; //Ignore the click event.
1541
  return;
1542
  }
 
1543
 
1544
+ //A jQuery UI dialog widget will prevent focus from leaving the dialog. So if we want
1545
+ //the dropdown to be properly focused when displaying it in a dialog, we must make it
1546
+ //a child of the dialog's DOM node (and vice versa when it's not in a dialog).
1547
+ var parentContainer = $(this).closest('.ui-dialog, #ws_menu_editor');
1548
+ if ((parentContainer.length > 0) && (capSelectorDropdown.closest(parentContainer).length == 0)) {
1549
+ var oldHeight = capSelectorDropdown.height(); //Height seems to reset when moving to a new parent.
1550
+ capSelectorDropdown.detach().appendTo(parentContainer).height(oldHeight);
1551
+ }
1552
 
1553
+ //Pre-select the current capability (will clear selection if there's no match).
1554
+ capSelectorDropdown.val(inputBox.val()).show();
1555
 
1556
+ //Move the drop-down near the input box.
1557
  var inputPos = inputBox.offset();
1558
+ capSelectorDropdown
1559
+ .css({
1560
+ position: 'absolute',
1561
+ zIndex: 1010 //Must be higher than the permissions dialog overlay.
1562
+ })
1563
+ .offset({
1564
+ left: inputPos.left,
1565
+ top : inputPos.top + inputBox.outerHeight()
1566
+ }).
1567
+ width(inputBox.outerWidth());
1568
+
1569
+ currentDropdownOwner = inputBox;
1570
+ capSelectorDropdown.focus();
1571
+ }
1572
+
1573
+ //Also show it when the user presses the down arrow in the input field (doesn't work in Opera).
1574
+ $('#ws_extra_capability').bind('keyup', function(event){
1575
  if ( event.which == 40 ){
1576
+ $('#ws_trigger_capability_dropdown').click();
1577
  }
1578
  });
1579
+
1580
+ //Event handlers for the drop-down lists themselves
1581
  var dropdownNodes = $('.ws_dropdown');
1582
+
1583
+ // Hide capability drop-down when it loses focus.
1584
  dropdownNodes.blur(function(event){
1585
+ console.log('Hiding dropdown because it lost focus.', event);
1586
+ capSelectorDropdown.hide();
 
 
 
 
 
 
 
 
 
 
 
 
1587
  });
1588
+
1589
  dropdownNodes.keydown(function(event){
1590
+
1591
+ //Hide it when the user presses Esc
 
1592
  if ( event.which == 27 ){
1593
+ capSelectorDropdown.hide();
1594
+ if (currentDropdownOwner) {
1595
+ currentDropdownOwner.focus();
 
 
1596
  }
1597
+
 
1598
  //Select an item & hide the list when the user presses Enter or Tab
1599
  } else if ( (event.which == 13) || (event.which == 9) ){
1600
+ capSelectorDropdown.hide();
1601
+
1602
+ if (currentDropdownOwner) {
1603
+ if ( capSelectorDropdown.val() ){
1604
+ currentDropdownOwner.val(capSelectorDropdown.val()).change();
1605
+ }
1606
+ currentDropdownOwner.focus();
1607
  }
1608
+
 
 
 
1609
  event.preventDefault();
1610
  }
1611
  });
1612
+
1613
+ //Eat Tab keys to prevent focus theft. Required to make the "select item on Tab" thing work.
1614
  dropdownNodes.keyup(function(event){
1615
  if ( event.which == 9 ){
1616
  event.preventDefault();
1617
  }
1618
+ });
1619
+
1620
+
1621
  //Update the input & hide the list when an option is clicked
1622
  dropdownNodes.click(function(){
1623
+ if (capSelectorDropdown.val()){
1624
+ capSelectorDropdown.hide();
1625
+ if (currentDropdownOwner) {
1626
+ currentDropdownOwner.val(capSelectorDropdown.val()).change().focus();
1627
+ }
1628
  }
 
 
 
 
 
1629
  });
1630
+
1631
  //Highlight an option when the user mouses over it (doesn't work in IE)
1632
  dropdownNodes.mousemove(function(event){
1633
  if ( !event.target ){
1634
  return;
1635
  }
1636
+
1637
+ var option = event.target;
1638
+ if ( (typeof option['selected'] !== 'undefined') && !option.selected && option.value ){
1639
+ option.selected = true;
1640
  }
1641
  });
1642
 
1654
  //Assign the selected icon to the menu.
1655
  if (currentIconButton) {
1656
  var container = currentIconButton.closest('.ws_container');
1657
+ var item = container.data('menu_item');
 
1658
 
1659
  //Remove the existing icon class, if any.
1660
+ var cssClass = getFieldValue(item, 'css_class', '');
1661
  cssClass = jsTrim( cssClass.replace(/\bmenu-icon-[^\s]+\b/, '') );
1662
 
1663
  if (selectedIcon.data('icon-class')) {
1664
  //Add the new class.
1665
  cssClass = selectedIcon.data('icon-class') + ' ' + cssClass;
1666
  //Can't have both a class and an image or we'll get two overlapping icons.
1667
+ item.icon_url = '';
1668
  } else if (selectedIcon.data('icon-url')) {
1669
+ item.icon_url = selectedIcon.data('icon-url');
1670
  }
1671
+ item.css_class = cssClass;
1672
+
1673
+ updateItemEditor(container);
1674
  }
1675
 
1676
  currentIconButton = null;
1689
 
1690
  currentIconButton = button;
1691
 
1692
+ var menuItem = currentIconButton.closest('.ws_container').data('menu_item');
1693
+ var cssClass = getFieldValue(menuItem, 'css_class', '');
1694
+ var iconUrl = getFieldValue(menuItem, 'icon_url', '');
 
 
1695
 
1696
  var customImageOption = iconSelector.find('.ws_custom_image_icon').hide();
1697
 
1764
  //Set the menu icon to the attachment URL.
1765
  if (currentIconButton) {
1766
  var container = currentIconButton.closest('.ws_container');
1767
+ var item = container.data('menu_item');
 
 
 
1768
 
1769
  //Remove the existing icon class, if any.
1770
+ var cssClass = getFieldValue(item, 'css_class', '');
1771
+ item.css_class = jsTrim( cssClass.replace(/\bmenu-icon-[^\s]+\b/, '') );
1772
 
1773
  //Set the new icon URL.
1774
+ item.icon_url = attachment.attributes.url;
1775
 
1776
+ updateItemEditor(container);
 
1777
  }
1778
 
1779
  currentIconButton = null;
1804
  currentIconButton = null;
1805
  }
1806
  });
1807
+
1808
+
1809
  /*************************************************************************
1810
  Menu toolbar buttons
1811
  *************************************************************************/
1812
+ function getSelectedMenu() {
1813
+ return $('#ws_menu_box').find('.ws_active');
1814
+ }
1815
+
1816
  //Show/Hide menu
1817
  $('#ws_hide_menu').click(function () {
1818
  //Get the selected menu
1819
+ var selection = getSelectedMenu();
1820
  if (!selection.length) return;
1821
+
1822
  //Mark the menu as hidden/visible
1823
+ var menuItem = selection.data('menu_item');
1824
+ menuItem.hidden = !menuItem.hidden;
1825
+ setMenuFlag(selection, 'hidden', menuItem.hidden);
1826
+
1827
  //Also mark all of it's submenus as hidden/visible
1828
+ $('#' + selection.data('submenu_id') + ' .ws_item').each(function(){
1829
+ var submenuItem = $(this).data('menu_item');
1830
+ submenuItem.hidden = menuItem.hidden;
1831
+ setMenuFlag(this, 'hidden', submenuItem.hidden);
1832
+ });
 
 
 
 
1833
  });
1834
+
1835
  //Delete menu
1836
  $('#ws_delete_menu').click(function () {
1837
  //Get the selected menu
1838
+ var selection = getSelectedMenu();
1839
  if (!selection.length) return;
1840
+
1841
  if (confirm('Delete this menu?')){
1842
  //Delete the submenu first
1843
  $('#' + selection.data('submenu_id')).remove();
1845
  selection.remove();
1846
  }
1847
  });
1848
+
1849
  //Copy menu
1850
  $('#ws_copy_menu').click(function () {
1851
  //Get the selected menu
1852
+ var selection = $('#ws_menu_box').find('.ws_active');
1853
  if (!selection.length) return;
1854
+
1855
  //Store a copy of the current menu state in clipboard
1856
+ menu_in_clipboard = readItemState(selection);
1857
  });
1858
+
1859
  //Cut menu
1860
  $('#ws_cut_menu').click(function () {
1861
  //Get the selected menu
1862
+ var selection = $('#ws_menu_box').find('.ws_active');
1863
  if (!selection.length) return;
1864
+
1865
  //Store a copy of the current menu state in clipboard
1866
+ menu_in_clipboard = readItemState(selection);
1867
+
1868
+ //Remove the original menu and submenu
 
1869
  $('#'+selection.data('submenu_id')).remove();
1870
+ selection.remove();
1871
  });
 
 
 
 
 
 
 
1872
 
1873
+ //Paste menu
1874
+ function pasteMenu(menu, afterMenu) {
1875
  //The user shouldn't need to worry about giving separators a unique filename.
1876
  if (menu.separator) {
1877
+ menu.defaults.file = randomMenuId('separator_');
1878
  }
1879
 
1880
+ //If we're pasting from a sub-menu, we may need to fix some properties
1881
+ //that are blank for sub-menu items but required for top-level menus.
1882
+ if (getFieldValue(menu, 'css_class', '') == '') {
1883
+ menu.css_class = 'menu-top';
1884
+ }
1885
+ if (getFieldValue(menu, 'icon_url', '') == '') {
1886
+ menu.icon_url = 'images/generic.png';
1887
+ }
1888
+ if (getFieldValue(menu, 'hookname', '') == '') {
1889
+ menu.hookname = randomMenuId();
1890
+ }
1891
+
1892
+ //Paste the menu after the specified one, or at the end of the list.
1893
+ if (afterMenu) {
1894
+ outputTopMenu(menu, afterMenu);
1895
  } else {
 
1896
  outputTopMenu(menu);
1897
  }
1898
+ }
1899
+
1900
+ $('#ws_paste_menu').click(function () {
1901
+ //Check if anything has been copied/cut
1902
+ if (!menu_in_clipboard) return;
1903
+
1904
+ var menu = $.extend(true, {}, menu_in_clipboard);
1905
+
1906
+ //Get the selected menu
1907
+ var selection = $('#ws_menu_box').find('.ws_active');
1908
+ //Paste the menu after the selection.
1909
+ pasteMenu(menu, (selection.length > 0) ? selection : null);
1910
  });
1911
+
1912
  //New menu
1913
  $('#ws_new_menu').click(function () {
1914
  ws_paste_count++;
1915
+
1916
  //The new menu starts out rather bare
1917
+ var randomId = randomMenuId();
1918
+ var menu = $.extend({}, wsEditorData.blankMenuItem, {
1919
+ custom: true, //Important : flag the new menu as custom, or it won't show up after saving.
1920
+ template_id : '',
1921
+ menu_title : 'Custom Menu ' + ws_paste_count,
1922
+ file : randomId,
1923
+ items: [],
1924
+ defaults: itemTemplates.getDefaults('')
1925
+ });
1926
+
1927
+ //Make it accessible only to the current actor if one is selected.
1928
+ if (selectedActor != null) {
1929
+ denyAccessForAllExcept(menu, selectedActor);
1930
+ }
1931
+
 
 
 
 
 
 
 
 
 
 
 
 
 
1932
  //Insert the new menu
1933
+ var selection = $('#ws_menu_box').find('.ws_active');
1934
+ var result = outputTopMenu(menu, (selection.length > 0) ? selection : null);
1935
+
1936
  //The menus's editbox is always open
1937
  result.menu.find('.ws_edit_link').click();
1938
  });
1939
+
1940
  //New separator
1941
+ $('#ws_new_separator, #ws_new_submenu_separator').click(function () {
1942
  ws_paste_count++;
1943
+
1944
  //The new menu starts out rather bare
1945
+ var randomId = randomMenuId('separator_');
1946
+ var menu = $.extend(true, {}, wsEditorData.blankMenuItem, {
1947
+ separator: true, //Flag as a separator
1948
+ custom: false, //Separators don't need to flagged as custom to be retained.
1949
+ items: [],
1950
+ defaults: {
1951
+ separator: true,
1952
+ css_class : 'wp-menu-separator',
 
 
 
 
 
 
 
 
 
1953
  access_level : 'read',
1954
  file : randomId,
1955
+ hookname : randomId
 
 
 
 
 
 
1956
  }
1957
+ });
1958
+
1959
+ if ( $(this).attr('id').indexOf('submenu') == -1 ) {
1960
+ //Insert in the top-level menu.
1961
+ var selection = $('#ws_menu_box').find('.ws_active');
1962
+ outputTopMenu(menu, (selection.length > 0) ? selection : null);
1963
+ } else {
1964
+ //Insert in the currently visible submenu.
1965
+ pasteItem(menu);
1966
+ }
1967
+ });
1968
+
1969
+ //Toggle all menus for the currently selected actor
1970
+ $('#ws_toggle_all_menus').click(function() {
1971
+ if ( selectedActor == null ) {
1972
+ alert("This button enables/disables all menus for the selected role. To use it, click a role and then click this button again.");
1973
+ return;
1974
+ }
1975
+
1976
+ var topMenuNodes = $('.ws_menu', '#ws_menu_box');
1977
+ //Look at the first menu's permissions and set everything to the opposite.
1978
+ var allow = ! actorCanAccessMenu(topMenuNodes.eq(0).data('menu_item'), selectedActor);
1979
+
1980
+ topMenuNodes.each(function() {
1981
+ var containerNode = $(this);
1982
+ setActorAccessForTreeAndUpdateUi(containerNode, selectedActor, allow);
1983
+ });
1984
  });
1985
+
1986
  /*************************************************************************
1987
  Item toolbar buttons
1988
  *************************************************************************/
1989
+ function getSelectedSubmenuItem() {
1990
+ return $('#ws_submenu_box').find('.ws_submenu:visible .ws_active');
1991
+ }
1992
+
1993
  //Show/Hide item
1994
  $('#ws_hide_item').click(function () {
1995
  //Get the selected item
1996
+ var selection = getSelectedSubmenuItem();
1997
  if (!selection.length) return;
1998
+
1999
  //Mark the item as hidden/visible
2000
+ var menuItem = selection.data('menu_item');
2001
+ menuItem.hidden = !menuItem.hidden;
2002
+ setMenuFlag(selection, 'hidden', menuItem.hidden);
2003
  });
2004
+
2005
+ //Delete item
2006
  $('#ws_delete_item').click(function () {
2007
  //Get the selected menu
2008
+ var selection = getSelectedSubmenuItem();
2009
  if (!selection.length) return;
2010
+
2011
  if (confirm('Delete this menu item?')){
2012
+ var submenu = selection.parent();
2013
  //Delete the item
2014
  selection.remove();
2015
+ updateParentAccessUi(submenu);
2016
  }
2017
  });
2018
+
2019
  //Copy item
2020
  $('#ws_copy_item').click(function () {
2021
  //Get the selected item
2022
+ var selection = getSelectedSubmenuItem();
2023
  if (!selection.length) return;
2024
+
2025
  //Store a copy of item state in the clipboard
2026
+ menu_in_clipboard = readItemState(selection);
2027
  });
2028
+
2029
  //Cut item
2030
  $('#ws_cut_item').click(function () {
2031
  //Get the selected item
2032
+ var selection = getSelectedSubmenuItem();
2033
  if (!selection.length) return;
2034
+
2035
  //Store a copy of item state in the clipboard
2036
+ menu_in_clipboard = readItemState(selection);
2037
+
2038
+ var submenu = selection.parent();
2039
+ //Remove the original item
2040
  selection.remove();
2041
+ updateParentAccessUi(submenu);
2042
  });
2043
+
2044
  //Paste item
2045
+ function pasteItem(item) {
2046
+ //We're pasting this item into a sub-menu, so it can't have a sub-menu of its own.
2047
+ //Instead, any sub-menu items belonging to this item will be pasted after the item.
2048
+ var newItems = [];
2049
+ for (var file in item.items) {
2050
+ if (item.items.hasOwnProperty(file)) {
2051
+ newItems.push(buildMenuItem(item.items[file], false));
2052
+ }
2053
+ }
2054
+ item.items = [];
2055
+
2056
+ newItems.unshift(buildMenuItem(item, false));
2057
+
2058
+ //Get the selected menu
2059
+ var visibleSubmenu = $('#ws_submenu_box').find('.ws_submenu:visible');
2060
+ var selection = visibleSubmenu.find('.ws_active');
2061
+ for(var i = 0; i < newItems.length; i++) {
2062
+ if (selection.length > 0) {
2063
+ //If an item is selected add the pasted items after it
2064
+ selection.after(newItems[i]);
2065
+ } else {
2066
+ //Otherwise add the pasted items at the end
2067
+ visibleSubmenu.append(newItems[i]);
2068
+ }
2069
+
2070
+ updateItemEditor(newItems[i]);
2071
+ newItems[i].show();
2072
+ }
2073
+
2074
+ updateParentAccessUi(visibleSubmenu);
2075
+ }
2076
+
2077
  $('#ws_paste_item').click(function () {
2078
  //Check if anything has been copied/cut
2079
+ if (!menu_in_clipboard) return;
2080
 
2081
+ //You can only add separators to submenus in the Pro version.
2082
+ if ( menu_in_clipboard.separator && !wsEditorData.wsMenuEditorPro ) {
2083
+ return;
 
 
 
 
 
 
 
 
 
2084
  }
2085
+
2086
+ //Paste it.
2087
+ var item = $.extend(true, {}, menu_in_clipboard);
2088
+ pasteItem(item);
2089
  });
2090
+
2091
  //New item
2092
  $('#ws_new_item').click(function () {
2093
+ if ($('.ws_submenu:visible').length < 1) {
2094
+ return; //Abort if no submenu visible
2095
  }
2096
+
2097
  ws_paste_count++;
2098
+
2099
+ var entry = $.extend({}, wsEditorData.blankMenuItem, {
2100
+ custom: true,
2101
+ template_id : '',
2102
+ menu_title : 'Custom Item ' + ws_paste_count,
2103
+ file : randomMenuId(),
2104
+ items: [],
2105
+ defaults: itemTemplates.getDefaults('')
2106
+ });
2107
+
2108
+ //Make it accessible to only the currently selected actor.
2109
+ if (selectedActor != null) {
2110
+ denyAccessForAllExcept(entry, selectedActor);
2111
+ }
2112
+
 
 
 
2113
  var menu = buildMenuItem(entry);
2114
+ updateItemEditor(menu);
2115
+
2116
+ //Insert the item into the currently open submenu.
2117
+ var visibleSubmenu = $('#ws_submenu_box').find('.ws_submenu:visible');
2118
+ var selection = visibleSubmenu.find('.ws_active');
2119
+ if (selection.length > 0) {
2120
+ selection.after(menu);
2121
+ } else {
2122
+ visibleSubmenu.append(menu);
2123
+ }
2124
+
2125
  //The items's editbox is always open
2126
  menu.find('.ws_edit_link').click();
2127
+
2128
+ updateParentAccessUi(menu);
2129
  });
2130
 
 
 
 
 
2131
  function compareMenus(a, b){
 
 
 
 
2132
  var aTitle = jsTrim( $(a).find('.ws_item_title').text() );
2133
  var bTitle = jsTrim( $(b).find('.ws_item_title').text() );
2134
+
2135
  aTitle = aTitle.toLowerCase();
2136
  bTitle = bTitle.toLowerCase();
2137
+
2138
  return aTitle > bTitle ? 1 : -1;
2139
  }
2140
+
2141
  //Sort items in ascending order
2142
  $('#ws_sort_ascending').click(function () {
2143
+ var submenu = $('#ws_submenu_box').find('.ws_submenu:visible');
2144
+ if (submenu.length < 1) {
2145
+ return; //Abort if no submenu visible
2146
  }
2147
+
2148
  submenu.find('.ws_container').sort(compareMenus);
2149
  });
2150
+
2151
  //Sort items in descending order
2152
  $('#ws_sort_descending').click(function () {
2153
+ var submenu = $('#ws_submenu_box').find('.ws_submenu:visible');
2154
+ if (submenu.length < 1) {
2155
+ return; //Abort if no submenu visible
2156
  }
2157
+
2158
  submenu.find('.ws_container').sort((function(a, b){
2159
  return -compareMenus(a, b);
2160
  }));
2161
  });
2162
+
2163
  //==============================================
2164
  // Main buttons
2165
  //==============================================
2166
+
2167
  //Save Changes - encode the current menu as JSON and save
2168
  $('#ws_save_menu').click(function () {
2169
+ var tree = readMenuTreeState();
2170
+
2171
+ function findItemByTemplateId(items, templateId) {
2172
+ var foundItem = null;
2173
+
2174
+ $.each(items, function(index, item) {
2175
+ if (item.template_id == templateId) {
2176
+ foundItem = item;
2177
+ return false;
2178
+ }
2179
+ if (item.hasOwnProperty('items') && (item.items.length > 0)) {
2180
+ foundItem = findItemByTemplateId(item.items, templateId);
2181
+ if (foundItem != null) {
2182
+ return false;
2183
+ }
2184
+ }
2185
+ return true;
2186
+ });
2187
+
2188
+ return foundItem;
2189
+ }
2190
+
2191
+ //Abort the save if it would make the editor inaccessible.
2192
+ if (wsEditorData.wsMenuEditorPro) {
2193
+ var myMenuItem = findItemByTemplateId(tree.tree, 'options-general.php>menu_editor');
2194
+ if (myMenuItem == null) {
2195
+ //This is OK - the missing menu item will be re-inserted automatically.
2196
+ } else if (!actorCanAccessMenu(myMenuItem, 'user:' + wsEditorData.currentUserLogin)) {
2197
+ alert(
2198
+ "Error: This configuration would make you unable to access the menu editor!\n\n" +
2199
+ "Please click either your role name or \"Current user (" + wsEditorData.currentUserLogin + ")\" "+
2200
+ "and enable the \"Menu Editor Pro\" menu item."
2201
+ );
2202
+ return;
2203
+ }
2204
+ }
2205
+
2206
+ var data = encodeMenuAsJSON(tree);
2207
  $('#ws_data').val(data);
2208
+ $('#ws_data_length').val(data.length);
2209
+ $('#ws_selected_actor').val(selectedActor === null ? '' : selectedActor);
2210
  $('#ws_main_form').submit();
2211
  });
2212
+
2213
  //Load default menu - load the default WordPress menu
2214
  $('#ws_load_menu').click(function () {
2215
  if (confirm('Are you sure you want to load the default WordPress menu?')){
2216
+ outputWpMenu(defaultMenu.tree);
2217
  }
2218
  });
2219
+
2220
  //Reset menu - re-load the custom menu. Discards any changes made by user.
2221
  $('#ws_reset_menu').click(function () {
2222
  if (confirm('Undo all changes made in the current editing session?')){
2223
+ outputWpMenu(customMenu.tree);
2224
  }
2225
  });
2226
+
2227
  //Export menu - download the current menu as a file
2228
+ $('#export_dialog').dialog({
2229
  autoOpen: false,
2230
  closeText: ' ',
2231
  modal: true,
2232
  minHeight: 100
2233
  });
2234
+
2235
  $('#ws_export_menu').click(function(){
2236
  var button = $(this);
2237
  button.attr('disabled', 'disabled');
2238
  button.val('Exporting...');
2239
+
2240
  $('#export_complete_notice, #download_menu_button').hide();
2241
  $('#export_progress_notice').show();
2242
  $('#export_dialog').dialog('open');
2243
+
2244
  //Encode and store the menu for download
2245
+ var exportData = encodeMenuAsJSON();
2246
+
 
 
 
 
 
2247
  $.post(
2248
+ wsEditorData.adminAjaxUrl,
2249
  {
2250
  'data' : exportData,
2251
  'action' : 'export_custom_menu',
2252
+ '_ajax_nonce' : wsEditorData.exportMenuNonce
2253
  },
2254
+ function(data){
2255
  button.val('Export');
2256
  button.removeAttr('disabled');
2257
+
2258
  if ( typeof data['error'] != 'undefined' ){
2259
  $('#export_dialog').dialog('close');
2260
  alert(data.error);
2261
  }
2262
+
2263
  if ( (typeof data['download_url'] != 'undefined') && data.download_url ){
2264
  //window.location = data.download_url;
2265
  $('#download_menu_button').attr('href', data.download_url);
2270
  'json'
2271
  );
2272
  });
2273
+
2274
  $('#ws_cancel_export').click(function(){
2275
  $('#export_dialog').dialog('close');
2276
  });
2277
+
2278
  $('#download_menu_button').click(function(){
2279
  $('#export_dialog').dialog('close');
2280
  });
2281
+
2282
  //Import menu - upload an exported menu and show it in the editor
2283
+ $('#import_dialog').dialog({
2284
  autoOpen: false,
2285
  closeText: ' ',
2286
  modal: true
2287
  });
2288
+
2289
  $('#ws_cancel_import').click(function(){
2290
  $('#import_dialog').dialog('close');
2291
  });
2292
+
2293
  $('#ws_import_menu').click(function(){
2294
  $('#import_progress_notice, #import_progress_notice2, #import_complete_notice').hide();
2295
  $('#import_menu_form').resetForm();
2296
  //The "Upload" button is disabled until the user selects a file
2297
+ $('#ws_start_import').attr('disabled', 'disabled');
2298
+
2299
+ var importDialog = $('#import_dialog');
2300
+ importDialog.find('.hide-when-uploading').show();
2301
+ importDialog.dialog('open');
2302
+ });
2303
+
2304
  $('#import_file_selector').change(function(){
2305
+ $('#ws_start_import').prop('disabled', ! $(this).val() );
 
 
 
 
2306
  });
2307
+
2308
  //AJAXify the upload form
2309
  $('#import_menu_form').ajaxForm({
2310
  dataType : 'json',
2311
+ beforeSubmit: function(formData) {
2312
+
2313
  //Check if the user has selected a file
2314
  for(var i = 0; i < formData.length; i++){
2315
  if ( formData[i].name == 'menu' ){
2319
  }
2320
  }
2321
  }
2322
+
2323
+ $('#import_dialog').find('.hide-when-uploading').hide();
2324
  $('#import_progress_notice').show();
2325
+
2326
  $('#ws_start_import').attr('disabled', 'disabled');
2327
+ return true;
2328
  },
2329
  success: function(data){
2330
+ var importDialog = $('#import_dialog');
2331
+ if ( !importDialog.dialog('isOpen') ){
2332
  //Whoops, the user closed the dialog while the upload was in progress.
2333
  //Discard the response silently.
2334
+ return;
2335
+ }
2336
+
2337
  if ( typeof data['error'] != 'undefined' ){
2338
  alert(data.error);
2339
  //Let the user try again
2340
  $('#import_menu_form').resetForm();
2341
+ importDialog.find('.hide-when-uploading').show();
2342
  }
2343
  $('#import_progress_notice').hide();
2344
+
2345
+ if ( (typeof data['tree'] != 'undefined') && data.tree ){
2346
  //Whee, we got back a (seemingly) valid menu. A veritable miracle!
2347
  //Lets load it into the editor.
2348
+ var progressNotice = $('#import_progress_notice2').show();
2349
+ outputWpMenu(data.tree);
2350
+ progressNotice.hide();
2351
  //Display a success notice, then automatically close the window after a few moments
2352
  $('#import_complete_notice').show();
2353
  setTimeout((function(){
2354
  //Close the import dialog
2355
  $('#import_dialog').dialog('close');
2356
+ }), 500);
2357
  }
2358
+
2359
  }
2360
  });
2361
 
2362
+ /*************************************************************************
2363
+ Drag & drop items between menu levels
2364
+ *************************************************************************/
2365
+
2366
+ if (wsEditorData.wsMenuEditorPro) {
2367
+ //Allow the user to drag sub-menu items to the top level.
2368
+ $('#ws_top_menu_dropzone').droppable({
2369
+ 'hoverClass' : 'ws_dropzone_hover',
2370
+
2371
+ 'accept' : (function(thing){
2372
+ return thing.hasClass('ws_item');
2373
+ }),
2374
+
2375
+ 'drop' : (function(event, ui){
2376
+ var droppedItemData = readItemState(ui.draggable);
2377
+ pasteMenu(droppedItemData);
2378
+ if ( !event.ctrlKey ) {
2379
+ ui.draggable.remove();
2380
+ }
2381
+ })
2382
+ });
2383
+
2384
+ //...and to drag top level menus to a sub-menu.
2385
+ $('#ws_sub_menu_dropzone').droppable({
2386
+ 'hoverClass' : 'ws_dropzone_hover',
2387
+
2388
+ 'accept' : (function(thing){
2389
+ var visibleSubmenu = $('#ws_submenu_box').find('.ws_submenu:visible');
2390
+ return (
2391
+ //Accept top-level menus
2392
+ thing.hasClass('ws_menu') &&
2393
+
2394
+ //Prevent users from dropping a menu on its own sub-menu.
2395
+ (visibleSubmenu.attr('id') != thing.data('submenu_id'))
2396
+ );
2397
+ }),
2398
+
2399
+ 'drop' : (function(event, ui){
2400
+ var droppedItemData = readItemState(ui.draggable);
2401
+ pasteItem(droppedItemData);
2402
+ if ( !event.ctrlKey ) {
2403
+ ui.draggable.remove();
2404
+ }
2405
+ })
2406
+ });
2407
+ }
2408
+
2409
+
2410
+ //Set up tooltips
2411
+ $('.ws_tooltip_trigger').qtip();
2412
+
2413
  //Flag closed hints as hidden by sending the appropriate AJAX request to the backend.
2414
  $('.ws_hint_close').click(function() {
2415
  var hint = $(this).parents('.ws_hint').first();
2423
  }
2424
  );
2425
  });
2426
+
2427
+
2428
+ /******************************************************************
2429
+ Actor views
2430
+ ******************************************************************/
2431
+
2432
+ //Build the list of available actors
2433
+ var actorSelector = $('#ws_actor_selector').empty();
2434
+ actorSelector.append('<li><a href="#" class="current ws_no_actor">All</a></li>');
2435
+
2436
+ if (wsEditorData.wsMenuEditorPro) {
2437
+ for(var actor in wsEditorData.actors) {
2438
+ if (!wsEditorData.actors.hasOwnProperty(actor)) {
2439
+ continue;
2440
+ }
2441
+ actorSelector.append(
2442
+ $('<li></li>').append(
2443
+ $('<a></a>')
2444
+ .attr('href', '#' + actor)
2445
+ .text(wsEditorData.actors[actor])
2446
+ )
2447
+ );
2448
+ }
2449
+ actorSelector.show();
2450
+
2451
+ if ( wsEditorData.hasOwnProperty('selectedActor') && wsEditorData.selectedActor ) {
2452
+ setSelectedActor(wsEditorData.selectedActor);
2453
+ } else {
2454
+ setSelectedActor(null);
2455
+ }
2456
+ }
2457
+
2458
+ $('li a', actorSelector).click(function(event) {
2459
+ var actor = $(this).attr('href').substring(1);
2460
+ if (actor == '') {
2461
+ actor = null;
2462
+ }
2463
+
2464
+ setSelectedActor(actor);
2465
+
2466
+ event.preventDefault();
2467
+ });
2468
+
2469
  //Finally, show the menu
2470
+ outputWpMenu(customMenu.tree);
2471
  });
2472
+
2473
  })(jQuery);
2474
 
2475
  //==============================================
2479
  jQuery(function($){
2480
  var screenOptions = $('#ws-ame-screen-meta-contents');
2481
  var checkbox = screenOptions.find('#ws-hide-advanced-settings');
2482
+
2483
+ if ( wsEditorData.hideAdvancedSettings ){
2484
  checkbox.attr('checked', 'checked');
2485
  } else {
2486
  checkbox.removeAttr('checked');
2487
  }
2488
+
2489
  //Update editor state when settings change
2490
  checkbox.click(function(){
2491
+ wsEditorData.hideAdvancedSettings = $(this).attr('checked'); //Using '$(this)' instead of 'checkbox' due to jQuery bugs
2492
+ var menuEditorNode = $('#ws_menu_editor');
2493
+ if ( wsEditorData.hideAdvancedSettings ){
2494
+ menuEditorNode.find('div.ws_advanced').hide();
2495
+ menuEditorNode.find('a.ws_toggle_advanced_fields').text(wsEditorData.captionShowAdvanced).show();
2496
  } else {
2497
+ menuEditorNode.find('div.ws_advanced').show();
2498
+ menuEditorNode.find('a.ws_toggle_advanced_fields').text(wsEditorData.captionHideAdvanced).hide();
2499
  }
2500
+
2501
  $.post(
2502
+ wsEditorData.adminAjaxUrl,
2503
  {
2504
  'action' : 'ws_ame_save_screen_options',
2505
+ 'hide_advanced_settings' : wsEditorData.hideAdvancedSettings ? 1 : 0,
2506
+ '_ajax_nonce' : wsEditorData.hideAdvancedSettingsNonce
2507
  }
2508
  );
2509
  });
2510
+
2511
  //Move our options into the screen meta panel
2512
  $('#adv-settings').empty().append(screenOptions.show());
2513
  });
menu-editor.php CHANGED
@@ -3,16 +3,20 @@
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.2.2
7
  Author: Janis Elsts
8
- Author URI: http://w-shadow.com/
9
  */
10
 
 
 
 
 
11
  //Are we running in the Dashboard?
12
  if ( is_admin() ) {
13
 
14
  //Load the plugin
15
- require dirname(__FILE__) . '/includes/menu-editor-core.php';
16
  $wp_menu_editor = new WPMenuEditor(__FILE__, 'ws_menu_editor');
17
 
18
- }//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.3
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
  //Are we running in the Dashboard?
16
  if ( is_admin() ) {
17
 
18
  //Load the plugin
19
+ require 'includes/menu-editor-core.php';
20
  $wp_menu_editor = new WPMenuEditor(__FILE__, 'ws_menu_editor');
21
 
22
+ }//is_admin()
readme.txt CHANGED
@@ -3,8 +3,8 @@ 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: 3.2
6
- Tested up to: 3.6
7
- Stable tag: 1.2.2
8
 
9
  Lets you edit the WordPress admin menu. You can re-order, hide or rename menus, add custom menus and more.
10
 
@@ -63,6 +63,17 @@ Plugins installed in the `mu-plugins` directory are treated as "always on", so y
63
 
64
  == Changelog ==
65
 
 
 
 
 
 
 
 
 
 
 
 
66
  = 1.2.2 =
67
  * Replaced a number of icons from the "Silk" set with GPL-compatible alternatives.
68
  * Tested with WP 3.6.
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: 3.2
6
+ Tested up to: 3.6.1
7
+ Stable tag: 1.3
8
 
9
  Lets you edit the WordPress admin menu. You can re-order, hide or rename menus, add custom menus and more.
10
 
63
 
64
  == Changelog ==
65
 
66
+ = 1.3 =
67
+ * Added a new settings page that lets you choose whether admin menu settings are per-site or network-wide, as well as specify who can access the plugin. To access this page, go to "Settings -> Menu Editor Pro" and click the small "Settings" link next to the page title.
68
+ * Added a way to show/hide advanced menu options through the settings page in addition to the "Screen Options" panel.
69
+ * Added a "Show menu access checks" option to make debugging menu permissions easier.
70
+ * Added partial WPML support. Now you can translate custom menu titles with WPML.
71
+ * The plugin will now display an error if you try to activate it when another version of it is already active.
72
+ * Added a "Target page" dropdown as an alternative to the "URL" field. To enter a custom URL, choose "Custom" from the dropdown.
73
+ * Fixed the "window title" setting only working for some menu items and not others.
74
+ * Fixed a number of bugs related to moving plugin menus around.
75
+ * Changed how the plugin stores menu settings. Note: The new format is not backwards-compatible with version 1.2.2.
76
+
77
  = 1.2.2 =
78
  * Replaced a number of icons from the "Silk" set with GPL-compatible alternatives.
79
  * Tested with WP 3.6.
screenshot-1.png CHANGED
Binary file
screenshot-3.png CHANGED
Binary file
uninstall.php CHANGED
@@ -2,7 +2,7 @@
2
 
3
  /**
4
  * @author W-Shadow
5
- * @copyright 2012
6
  *
7
  * The uninstallation script.
8
  */
@@ -14,7 +14,7 @@ if( defined( 'ABSPATH') && defined('WP_UNINSTALL_PLUGIN') ) {
14
  if ( function_exists('delete_site_option') ){
15
  delete_site_option('ws_menu_editor');
16
  }
17
-
18
  //Remove hint visibility flags
19
  if ( function_exists('delete_metadata') ) {
20
  delete_metadata('user', 0, 'ame_show_hints', '', true);
2
 
3
  /**
4
  * @author W-Shadow
5
+ * @copyright 2009
6
  *
7
  * The uninstallation script.
8
  */
14
  if ( function_exists('delete_site_option') ){
15
  delete_site_option('ws_menu_editor');
16
  }
17
+
18
  //Remove hint visibility flags
19
  if ( function_exists('delete_metadata') ) {
20
  delete_metadata('user', 0, 'ame_show_hints', '', true);