Admin Menu Editor - Version 1.0

Version Description

  • Added a "Feedback" link.
  • Added a dropdown list of all roles and capabilities to the menu editor.
  • Added toolbar buttons for sorting menu items alphabetically.
  • New "Add separator" button.
  • New separator graphics.
  • Minimum requirements upped to WP 3.0.
  • Compatibility with WP 3.0 MultiSite mode.
  • Plugin pages moved to different menus no longer stop working.
  • Fixed moved pages not having a window title.
  • Hide advanced menu fields by default (can be turned off in Screen Options).
  • Changed a lot of UI text to be a bit more intuitive.
  • In emergencies, administrators can now reset the custom menu by going to http:://example.com/wp-admin/?reset_admin_menu=1
  • Fixed the "Donate" link in readme.txt
  • Unbundle the JSON.php JSON parser/encoder and use the built-in class-json.php instead.
  • Use the native JSON decoding routine if it's available.
  • Replaced the cryptic "Cannot redeclare whatever" activation error message with a more useful one.
Download this release

Release Info

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

Code changes from version 0.2 to 1.0

JSON.php DELETED
@@ -1,805 +0,0 @@
1
- <?php
2
- /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
3
-
4
- /**
5
- * Converts to and from JSON format.
6
- *
7
- * JSON (JavaScript Object Notation) is a lightweight data-interchange
8
- * format. It is easy for humans to read and write. It is easy for machines
9
- * to parse and generate. It is based on a subset of the JavaScript
10
- * Programming Language, Standard ECMA-262 3rd Edition - December 1999.
11
- * This feature can also be found in Python. JSON is a text format that is
12
- * completely language independent but uses conventions that are familiar
13
- * to programmers of the C-family of languages, including C, C++, C#, Java,
14
- * JavaScript, Perl, TCL, and many others. These properties make JSON an
15
- * ideal data-interchange language.
16
- *
17
- * This package provides a simple encoder and decoder for JSON notation. It
18
- * is intended for use with client-side Javascript applications that make
19
- * use of HTTPRequest to perform server communication functions - data can
20
- * be encoded into JSON notation for use in a client-side javascript, or
21
- * decoded from incoming Javascript requests. JSON format is native to
22
- * Javascript, and can be directly eval()'ed with no further parsing
23
- * overhead
24
- *
25
- * All strings should be in ASCII or UTF-8 format!
26
- *
27
- * LICENSE: Redistribution and use in source and binary forms, with or
28
- * without modification, are permitted provided that the following
29
- * conditions are met: Redistributions of source code must retain the
30
- * above copyright notice, this list of conditions and the following
31
- * disclaimer. Redistributions in binary form must reproduce the above
32
- * copyright notice, this list of conditions and the following disclaimer
33
- * in the documentation and/or other materials provided with the
34
- * distribution.
35
- *
36
- * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
37
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
38
- * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
39
- * NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
40
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
41
- * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
42
- * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
43
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
44
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
45
- * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
46
- * DAMAGE.
47
- *
48
- * @category
49
- * @package Services_JSON
50
- * @author Michal Migurski <mike-json@teczno.com>
51
- * @author Matt Knapp <mdknapp[at]gmail[dot]com>
52
- * @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
53
- * @copyright 2005 Michal Migurski
54
- * @version CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $
55
- * @license http://www.opensource.org/licenses/bsd-license.php
56
- * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198
57
- */
58
-
59
- /**
60
- * Marker constant for Services_JSON::decode(), used to flag stack state
61
- */
62
- define('SERVICES_JSON_SLICE', 1);
63
-
64
- /**
65
- * Marker constant for Services_JSON::decode(), used to flag stack state
66
- */
67
- define('SERVICES_JSON_IN_STR', 2);
68
-
69
- /**
70
- * Marker constant for Services_JSON::decode(), used to flag stack state
71
- */
72
- define('SERVICES_JSON_IN_ARR', 3);
73
-
74
- /**
75
- * Marker constant for Services_JSON::decode(), used to flag stack state
76
- */
77
- define('SERVICES_JSON_IN_OBJ', 4);
78
-
79
- /**
80
- * Marker constant for Services_JSON::decode(), used to flag stack state
81
- */
82
- define('SERVICES_JSON_IN_CMT', 5);
83
-
84
- /**
85
- * Behavior switch for Services_JSON::decode()
86
- */
87
- define('SERVICES_JSON_LOOSE_TYPE', 16);
88
-
89
- /**
90
- * Behavior switch for Services_JSON::decode()
91
- */
92
- define('SERVICES_JSON_SUPPRESS_ERRORS', 32);
93
-
94
- /**
95
- * Converts to and from JSON format.
96
- *
97
- * Brief example of use:
98
- *
99
- * <code>
100
- * // create a new instance of Services_JSON
101
- * $json = new Services_JSON();
102
- *
103
- * // convert a complexe value to JSON notation, and send it to the browser
104
- * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4)));
105
- * $output = $json->encode($value);
106
- *
107
- * print($output);
108
- * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]]
109
- *
110
- * // accept incoming POST data, assumed to be in JSON notation
111
- * $input = file_get_contents('php://input', 1000000);
112
- * $value = $json->decode($input);
113
- * </code>
114
- */
115
- class Services_JSON
116
- {
117
- /**
118
- * constructs a new JSON instance
119
- *
120
- * @param int $use object behavior flags; combine with boolean-OR
121
- *
122
- * possible values:
123
- * - SERVICES_JSON_LOOSE_TYPE: loose typing.
124
- * "{...}" syntax creates associative arrays
125
- * instead of objects in decode().
126
- * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression.
127
- * Values which can't be encoded (e.g. resources)
128
- * appear as NULL instead of throwing errors.
129
- * By default, a deeply-nested resource will
130
- * bubble up with an error, so all return values
131
- * from encode() should be checked with isError()
132
- */
133
- function Services_JSON($use = 0)
134
- {
135
- $this->use = $use;
136
- }
137
-
138
- /**
139
- * convert a string from one UTF-16 char to one UTF-8 char
140
- *
141
- * Normally should be handled by mb_convert_encoding, but
142
- * provides a slower PHP-only method for installations
143
- * that lack the multibye string extension.
144
- *
145
- * @param string $utf16 UTF-16 character
146
- * @return string UTF-8 character
147
- * @access private
148
- */
149
- function utf162utf8($utf16)
150
- {
151
- // oh please oh please oh please oh please oh please
152
- if(function_exists('mb_convert_encoding')) {
153
- return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16');
154
- }
155
-
156
- $bytes = (ord($utf16{0}) << 8) | ord($utf16{1});
157
-
158
- switch(true) {
159
- case ((0x7F & $bytes) == $bytes):
160
- // this case should never be reached, because we are in ASCII range
161
- // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
162
- return chr(0x7F & $bytes);
163
-
164
- case (0x07FF & $bytes) == $bytes:
165
- // return a 2-byte UTF-8 character
166
- // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
167
- return chr(0xC0 | (($bytes >> 6) & 0x1F))
168
- . chr(0x80 | ($bytes & 0x3F));
169
-
170
- case (0xFFFF & $bytes) == $bytes:
171
- // return a 3-byte UTF-8 character
172
- // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
173
- return chr(0xE0 | (($bytes >> 12) & 0x0F))
174
- . chr(0x80 | (($bytes >> 6) & 0x3F))
175
- . chr(0x80 | ($bytes & 0x3F));
176
- }
177
-
178
- // ignoring UTF-32 for now, sorry
179
- return '';
180
- }
181
-
182
- /**
183
- * convert a string from one UTF-8 char to one UTF-16 char
184
- *
185
- * Normally should be handled by mb_convert_encoding, but
186
- * provides a slower PHP-only method for installations
187
- * that lack the multibye string extension.
188
- *
189
- * @param string $utf8 UTF-8 character
190
- * @return string UTF-16 character
191
- * @access private
192
- */
193
- function utf82utf16($utf8)
194
- {
195
- // oh please oh please oh please oh please oh please
196
- if(function_exists('mb_convert_encoding')) {
197
- return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
198
- }
199
-
200
- switch(strlen($utf8)) {
201
- case 1:
202
- // this case should never be reached, because we are in ASCII range
203
- // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
204
- return $utf8;
205
-
206
- case 2:
207
- // return a UTF-16 character from a 2-byte UTF-8 char
208
- // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
209
- return chr(0x07 & (ord($utf8{0}) >> 2))
210
- . chr((0xC0 & (ord($utf8{0}) << 6))
211
- | (0x3F & ord($utf8{1})));
212
-
213
- case 3:
214
- // return a UTF-16 character from a 3-byte UTF-8 char
215
- // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
216
- return chr((0xF0 & (ord($utf8{0}) << 4))
217
- | (0x0F & (ord($utf8{1}) >> 2)))
218
- . chr((0xC0 & (ord($utf8{1}) << 6))
219
- | (0x7F & ord($utf8{2})));
220
- }
221
-
222
- // ignoring UTF-32 for now, sorry
223
- return '';
224
- }
225
-
226
- /**
227
- * encodes an arbitrary variable into JSON format
228
- *
229
- * @param mixed $var any number, boolean, string, array, or object to be encoded.
230
- * see argument 1 to Services_JSON() above for array-parsing behavior.
231
- * if var is a strng, note that encode() always expects it
232
- * to be in ASCII or UTF-8 format!
233
- *
234
- * @return mixed JSON string representation of input var or an error if a problem occurs
235
- * @access public
236
- */
237
- function encode($var)
238
- {
239
- switch (gettype($var)) {
240
- case 'boolean':
241
- return $var ? 'true' : 'false';
242
-
243
- case 'NULL':
244
- return 'null';
245
-
246
- case 'integer':
247
- return (int) $var;
248
-
249
- case 'double':
250
- case 'float':
251
- return (float) $var;
252
-
253
- case 'string':
254
- // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT
255
- $ascii = '';
256
- $strlen_var = strlen($var);
257
-
258
- /*
259
- * Iterate over every character in the string,
260
- * escaping with a slash or encoding to UTF-8 where necessary
261
- */
262
- for ($c = 0; $c < $strlen_var; ++$c) {
263
-
264
- $ord_var_c = ord($var{$c});
265
-
266
- switch (true) {
267
- case $ord_var_c == 0x08:
268
- $ascii .= '\b';
269
- break;
270
- case $ord_var_c == 0x09:
271
- $ascii .= '\t';
272
- break;
273
- case $ord_var_c == 0x0A:
274
- $ascii .= '\n';
275
- break;
276
- case $ord_var_c == 0x0C:
277
- $ascii .= '\f';
278
- break;
279
- case $ord_var_c == 0x0D:
280
- $ascii .= '\r';
281
- break;
282
-
283
- case $ord_var_c == 0x22:
284
- case $ord_var_c == 0x2F:
285
- case $ord_var_c == 0x5C:
286
- // double quote, slash, slosh
287
- $ascii .= '\\'.$var{$c};
288
- break;
289
-
290
- case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)):
291
- // characters U-00000000 - U-0000007F (same as ASCII)
292
- $ascii .= $var{$c};
293
- break;
294
-
295
- case (($ord_var_c & 0xE0) == 0xC0):
296
- // characters U-00000080 - U-000007FF, mask 110XXXXX
297
- // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
298
- $char = pack('C*', $ord_var_c, ord($var{$c + 1}));
299
- $c += 1;
300
- $utf16 = $this->utf82utf16($char);
301
- $ascii .= sprintf('\u%04s', bin2hex($utf16));
302
- break;
303
-
304
- case (($ord_var_c & 0xF0) == 0xE0):
305
- // characters U-00000800 - U-0000FFFF, mask 1110XXXX
306
- // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
307
- $char = pack('C*', $ord_var_c,
308
- ord($var{$c + 1}),
309
- ord($var{$c + 2}));
310
- $c += 2;
311
- $utf16 = $this->utf82utf16($char);
312
- $ascii .= sprintf('\u%04s', bin2hex($utf16));
313
- break;
314
-
315
- case (($ord_var_c & 0xF8) == 0xF0):
316
- // characters U-00010000 - U-001FFFFF, mask 11110XXX
317
- // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
318
- $char = pack('C*', $ord_var_c,
319
- ord($var{$c + 1}),
320
- ord($var{$c + 2}),
321
- ord($var{$c + 3}));
322
- $c += 3;
323
- $utf16 = $this->utf82utf16($char);
324
- $ascii .= sprintf('\u%04s', bin2hex($utf16));
325
- break;
326
-
327
- case (($ord_var_c & 0xFC) == 0xF8):
328
- // characters U-00200000 - U-03FFFFFF, mask 111110XX
329
- // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
330
- $char = pack('C*', $ord_var_c,
331
- ord($var{$c + 1}),
332
- ord($var{$c + 2}),
333
- ord($var{$c + 3}),
334
- ord($var{$c + 4}));
335
- $c += 4;
336
- $utf16 = $this->utf82utf16($char);
337
- $ascii .= sprintf('\u%04s', bin2hex($utf16));
338
- break;
339
-
340
- case (($ord_var_c & 0xFE) == 0xFC):
341
- // characters U-04000000 - U-7FFFFFFF, mask 1111110X
342
- // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
343
- $char = pack('C*', $ord_var_c,
344
- ord($var{$c + 1}),
345
- ord($var{$c + 2}),
346
- ord($var{$c + 3}),
347
- ord($var{$c + 4}),
348
- ord($var{$c + 5}));
349
- $c += 5;
350
- $utf16 = $this->utf82utf16($char);
351
- $ascii .= sprintf('\u%04s', bin2hex($utf16));
352
- break;
353
- }
354
- }
355
-
356
- return '"'.$ascii.'"';
357
-
358
- case 'array':
359
- /*
360
- * As per JSON spec if any array key is not an integer
361
- * we must treat the the whole array as an object. We
362
- * also try to catch a sparsely populated associative
363
- * array with numeric keys here because some JS engines
364
- * will create an array with empty indexes up to
365
- * max_index which can cause memory issues and because
366
- * the keys, which may be relevant, will be remapped
367
- * otherwise.
368
- *
369
- * As per the ECMA and JSON specification an object may
370
- * have any string as a property. Unfortunately due to
371
- * a hole in the ECMA specification if the key is a
372
- * ECMA reserved word or starts with a digit the
373
- * parameter is only accessible using ECMAScript's
374
- * bracket notation.
375
- */
376
-
377
- // treat as a JSON object
378
- if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) {
379
- $properties = array_map(array($this, 'name_value'),
380
- array_keys($var),
381
- array_values($var));
382
-
383
- foreach($properties as $property) {
384
- if(Services_JSON::isError($property)) {
385
- return $property;
386
- }
387
- }
388
-
389
- return '{' . join(',', $properties) . '}';
390
- }
391
-
392
- // treat it like a regular array
393
- $elements = array_map(array($this, 'encode'), $var);
394
-
395
- foreach($elements as $element) {
396
- if(Services_JSON::isError($element)) {
397
- return $element;
398
- }
399
- }
400
-
401
- return '[' . join(',', $elements) . ']';
402
-
403
- case 'object':
404
- $vars = get_object_vars($var);
405
-
406
- $properties = array_map(array($this, 'name_value'),
407
- array_keys($vars),
408
- array_values($vars));
409
-
410
- foreach($properties as $property) {
411
- if(Services_JSON::isError($property)) {
412
- return $property;
413
- }
414
- }
415
-
416
- return '{' . join(',', $properties) . '}';
417
-
418
- default:
419
- return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS)
420
- ? 'null'
421
- : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string");
422
- }
423
- }
424
-
425
- /**
426
- * array-walking function for use in generating JSON-formatted name-value pairs
427
- *
428
- * @param string $name name of key to use
429
- * @param mixed $value reference to an array element to be encoded
430
- *
431
- * @return string JSON-formatted name-value pair, like '"name":value'
432
- * @access private
433
- */
434
- function name_value($name, $value)
435
- {
436
- $encoded_value = $this->encode($value);
437
-
438
- if(Services_JSON::isError($encoded_value)) {
439
- return $encoded_value;
440
- }
441
-
442
- return $this->encode(strval($name)) . ':' . $encoded_value;
443
- }
444
-
445
- /**
446
- * reduce a string by removing leading and trailing comments and whitespace
447
- *
448
- * @param $str string string value to strip of comments and whitespace
449
- *
450
- * @return string string value stripped of comments and whitespace
451
- * @access private
452
- */
453
- function reduce_string($str)
454
- {
455
- $str = preg_replace(array(
456
-
457
- // eliminate single line comments in '// ...' form
458
- '#^\s*//(.+)$#m',
459
-
460
- // eliminate multi-line comments in '/* ... */' form, at start of string
461
- '#^\s*/\*(.+)\*/#Us',
462
-
463
- // eliminate multi-line comments in '/* ... */' form, at end of string
464
- '#/\*(.+)\*/\s*$#Us'
465
-
466
- ), '', $str);
467
-
468
- // eliminate extraneous space
469
- return trim($str);
470
- }
471
-
472
- /**
473
- * decodes a JSON string into appropriate variable
474
- *
475
- * @param string $str JSON-formatted string
476
- *
477
- * @return mixed number, boolean, string, array, or object
478
- * corresponding to given JSON input string.
479
- * See argument 1 to Services_JSON() above for object-output behavior.
480
- * Note that decode() always returns strings
481
- * in ASCII or UTF-8 format!
482
- * @access public
483
- */
484
- function decode($str)
485
- {
486
- $str = $this->reduce_string($str);
487
-
488
- switch (strtolower($str)) {
489
- case 'true':
490
- return true;
491
-
492
- case 'false':
493
- return false;
494
-
495
- case 'null':
496
- return null;
497
-
498
- default:
499
- $m = array();
500
-
501
- if (is_numeric($str)) {
502
- // Lookie-loo, it's a number
503
-
504
- // This would work on its own, but I'm trying to be
505
- // good about returning integers where appropriate:
506
- // return (float)$str;
507
-
508
- // Return float or int, as appropriate
509
- return ((float)$str == (integer)$str)
510
- ? (integer)$str
511
- : (float)$str;
512
-
513
- } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) {
514
- // STRINGS RETURNED IN UTF-8 FORMAT
515
- $delim = substr($str, 0, 1);
516
- $chrs = substr($str, 1, -1);
517
- $utf8 = '';
518
- $strlen_chrs = strlen($chrs);
519
-
520
- for ($c = 0; $c < $strlen_chrs; ++$c) {
521
-
522
- $substr_chrs_c_2 = substr($chrs, $c, 2);
523
- $ord_chrs_c = ord($chrs{$c});
524
-
525
- switch (true) {
526
- case $substr_chrs_c_2 == '\b':
527
- $utf8 .= chr(0x08);
528
- ++$c;
529
- break;
530
- case $substr_chrs_c_2 == '\t':
531
- $utf8 .= chr(0x09);
532
- ++$c;
533
- break;
534
- case $substr_chrs_c_2 == '\n':
535
- $utf8 .= chr(0x0A);
536
- ++$c;
537
- break;
538
- case $substr_chrs_c_2 == '\f':
539
- $utf8 .= chr(0x0C);
540
- ++$c;
541
- break;
542
- case $substr_chrs_c_2 == '\r':
543
- $utf8 .= chr(0x0D);
544
- ++$c;
545
- break;
546
-
547
- case $substr_chrs_c_2 == '\\"':
548
- case $substr_chrs_c_2 == '\\\'':
549
- case $substr_chrs_c_2 == '\\\\':
550
- case $substr_chrs_c_2 == '\\/':
551
- if (($delim == '"' && $substr_chrs_c_2 != '\\\'') ||
552
- ($delim == "'" && $substr_chrs_c_2 != '\\"')) {
553
- $utf8 .= $chrs{++$c};
554
- }
555
- break;
556
-
557
- case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)):
558
- // single, escaped unicode character
559
- $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2)))
560
- . chr(hexdec(substr($chrs, ($c + 4), 2)));
561
- $utf8 .= $this->utf162utf8($utf16);
562
- $c += 5;
563
- break;
564
-
565
- case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F):
566
- $utf8 .= $chrs{$c};
567
- break;
568
-
569
- case ($ord_chrs_c & 0xE0) == 0xC0:
570
- // characters U-00000080 - U-000007FF, mask 110XXXXX
571
- //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
572
- $utf8 .= substr($chrs, $c, 2);
573
- ++$c;
574
- break;
575
-
576
- case ($ord_chrs_c & 0xF0) == 0xE0:
577
- // characters U-00000800 - U-0000FFFF, mask 1110XXXX
578
- // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
579
- $utf8 .= substr($chrs, $c, 3);
580
- $c += 2;
581
- break;
582
-
583
- case ($ord_chrs_c & 0xF8) == 0xF0:
584
- // characters U-00010000 - U-001FFFFF, mask 11110XXX
585
- // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
586
- $utf8 .= substr($chrs, $c, 4);
587
- $c += 3;
588
- break;
589
-
590
- case ($ord_chrs_c & 0xFC) == 0xF8:
591
- // characters U-00200000 - U-03FFFFFF, mask 111110XX
592
- // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
593
- $utf8 .= substr($chrs, $c, 5);
594
- $c += 4;
595
- break;
596
-
597
- case ($ord_chrs_c & 0xFE) == 0xFC:
598
- // characters U-04000000 - U-7FFFFFFF, mask 1111110X
599
- // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
600
- $utf8 .= substr($chrs, $c, 6);
601
- $c += 5;
602
- break;
603
-
604
- }
605
-
606
- }
607
-
608
- return $utf8;
609
-
610
- } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) {
611
- // array, or object notation
612
-
613
- if ($str{0} == '[') {
614
- $stk = array(SERVICES_JSON_IN_ARR);
615
- $arr = array();
616
- } else {
617
- if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
618
- $stk = array(SERVICES_JSON_IN_OBJ);
619
- $obj = array();
620
- } else {
621
- $stk = array(SERVICES_JSON_IN_OBJ);
622
- $obj = new stdClass();
623
- }
624
- }
625
-
626
- array_push($stk, array('what' => SERVICES_JSON_SLICE,
627
- 'where' => 0,
628
- 'delim' => false));
629
-
630
- $chrs = substr($str, 1, -1);
631
- $chrs = $this->reduce_string($chrs);
632
-
633
- if ($chrs == '') {
634
- if (reset($stk) == SERVICES_JSON_IN_ARR) {
635
- return $arr;
636
-
637
- } else {
638
- return $obj;
639
-
640
- }
641
- }
642
-
643
- //print("\nparsing {$chrs}\n");
644
-
645
- $strlen_chrs = strlen($chrs);
646
-
647
- for ($c = 0; $c <= $strlen_chrs; ++$c) {
648
-
649
- $top = end($stk);
650
- $substr_chrs_c_2 = substr($chrs, $c, 2);
651
-
652
- if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) {
653
- // found a comma that is not inside a string, array, etc.,
654
- // OR we've reached the end of the character list
655
- $slice = substr($chrs, $top['where'], ($c - $top['where']));
656
- array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false));
657
- //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
658
-
659
- if (reset($stk) == SERVICES_JSON_IN_ARR) {
660
- // we are in an array, so just push an element onto the stack
661
- array_push($arr, $this->decode($slice));
662
-
663
- } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
664
- // we are in an object, so figure
665
- // out the property name and set an
666
- // element in an associative array,
667
- // for now
668
- $parts = array();
669
-
670
- if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
671
- // "name":value pair
672
- $key = $this->decode($parts[1]);
673
- $val = $this->decode($parts[2]);
674
-
675
- if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
676
- $obj[$key] = $val;
677
- } else {
678
- $obj->$key = $val;
679
- }
680
- } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
681
- // name:value pair, where name is unquoted
682
- $key = $parts[1];
683
- $val = $this->decode($parts[2]);
684
-
685
- if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
686
- $obj[$key] = $val;
687
- } else {
688
- $obj->$key = $val;
689
- }
690
- }
691
-
692
- }
693
-
694
- } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) {
695
- // found a quote, and we are not inside a string
696
- array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c}));
697
- //print("Found start of string at {$c}\n");
698
-
699
- } elseif (($chrs{$c} == $top['delim']) &&
700
- ($top['what'] == SERVICES_JSON_IN_STR) &&
701
- ((strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1)) {
702
- // found a quote, we're in a string, and it's not escaped
703
- // we know that it's not escaped becase there is _not_ an
704
- // odd number of backslashes at the end of the string so far
705
- array_pop($stk);
706
- //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n");
707
-
708
- } elseif (($chrs{$c} == '[') &&
709
- in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
710
- // found a left-bracket, and we are in an array, object, or slice
711
- array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false));
712
- //print("Found start of array at {$c}\n");
713
-
714
- } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) {
715
- // found a right-bracket, and we're in an array
716
- array_pop($stk);
717
- //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
718
-
719
- } elseif (($chrs{$c} == '{') &&
720
- in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
721
- // found a left-brace, and we are in an array, object, or slice
722
- array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false));
723
- //print("Found start of object at {$c}\n");
724
-
725
- } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) {
726
- // found a right-brace, and we're in an object
727
- array_pop($stk);
728
- //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
729
-
730
- } elseif (($substr_chrs_c_2 == '/*') &&
731
- in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
732
- // found a comment start, and we are in an array, object, or slice
733
- array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false));
734
- $c++;
735
- //print("Found start of comment at {$c}\n");
736
-
737
- } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) {
738
- // found a comment end, and we're in one now
739
- array_pop($stk);
740
- $c++;
741
-
742
- for ($i = $top['where']; $i <= $c; ++$i)
743
- $chrs = substr_replace($chrs, ' ', $i, 1);
744
-
745
- //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
746
-
747
- }
748
-
749
- }
750
-
751
- if (reset($stk) == SERVICES_JSON_IN_ARR) {
752
- return $arr;
753
-
754
- } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
755
- return $obj;
756
-
757
- }
758
-
759
- }
760
- }
761
- }
762
-
763
- /**
764
- * @todo Ultimately, this should just call PEAR::isError()
765
- */
766
- function isError($data, $code = null)
767
- {
768
- if (class_exists('pear')) {
769
- return PEAR::isError($data, $code);
770
- } elseif (is_object($data) && (get_class($data) == 'services_json_error' ||
771
- is_subclass_of($data, 'services_json_error'))) {
772
- return true;
773
- }
774
-
775
- return false;
776
- }
777
- }
778
-
779
- if (class_exists('PEAR_Error')) {
780
-
781
- class Services_JSON_Error extends PEAR_Error
782
- {
783
- function Services_JSON_Error($message = 'unknown error', $code = null,
784
- $mode = null, $options = null, $userinfo = null)
785
- {
786
- parent::PEAR_Error($message, $code, $mode, $options, $userinfo);
787
- }
788
- }
789
-
790
- } else {
791
-
792
- /**
793
- * @todo Ultimately, this class shall be descended from PEAR_Error
794
- */
795
- class Services_JSON_Error
796
- {
797
- function Services_JSON_Error($message = 'unknown error', $code = null,
798
- $mode = null, $options = null, $userinfo = null)
799
- {
800
-
801
- }
802
- }
803
-
804
- }
805
- ?>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
css/menu-editor.css ADDED
@@ -0,0 +1,580 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Admin Menu Editor CSS file */
2
+
3
+ .ws_main_container {
4
+ margin: 2px;
5
+ width: 310px;
6
+ float: left;
7
+ display:block;
8
+ border: 1px solid #cdd5d5;
9
+
10
+ border-radius: 3px;
11
+ -moz-border-radius: 3px;
12
+ -webkit-border-radius: 3px;
13
+ }
14
+
15
+ .ws_box {
16
+ min-height: 30px;
17
+ width: 100%;
18
+ margin: 0;
19
+ padding-top: 2px;
20
+ padding-bottom: 4px;
21
+ }
22
+
23
+ #ws_menu_box {
24
+ }
25
+
26
+ #ws_submenu_box {
27
+ }
28
+
29
+ /*
30
+ * The sidebar
31
+ */
32
+
33
+ #ws_editor_sidebar {
34
+ width: 138px;
35
+ padding: 2px;
36
+ }
37
+
38
+ .ws_main_button {
39
+ clear: both;
40
+ display: block;
41
+ margin: 4px;
42
+ margin-left: auto;
43
+ margin-right: auto;
44
+ width: 120px;
45
+ padding: 4px !important;
46
+ }
47
+
48
+ #ws_save_menu {
49
+ margin-bottom: 20px;
50
+ }
51
+
52
+ #ws_export_menu {
53
+ margin-top: 12px;
54
+ }
55
+
56
+ /*
57
+ * Menu components and widgets
58
+ */
59
+
60
+ .ws_container {
61
+ display: block;
62
+ width: 290px;
63
+
64
+ padding : 3px;
65
+ margin: 2px;
66
+ margin-left: auto;
67
+ margin-right: auto;
68
+
69
+ border: 1px solid #a9badb;
70
+ background-color: #bdd3ff;
71
+ }
72
+
73
+ .ws_active {
74
+ background-color : #8eb0f1 !important; /* make sure this overrides ws_menu_separator */
75
+ }
76
+
77
+ .ws_menu { }
78
+ .ws_item { }
79
+
80
+ .ws_menu_separator {
81
+ background-image: url("../images/menu-arrows.png");
82
+ background-repeat: no-repeat;
83
+ background-position : 4px 8px;
84
+ background-color: #F9F9F9;
85
+ border-color: #d9d9d9;
86
+ }
87
+
88
+ .ws_submenu {
89
+ min-height: 2em;
90
+ }
91
+
92
+
93
+ .ws_item_head {
94
+ padding: 0;
95
+ }
96
+
97
+ .ws_item_title {
98
+ display: block;
99
+ padding: 2px;
100
+ cursor: default;
101
+ }
102
+
103
+ .ws_edit_link {
104
+ float: right;
105
+ margin-right: 0px;
106
+ cursor: pointer;
107
+ display:block;
108
+ width: 40px;
109
+ height: 22px;
110
+
111
+ background-image: url('../images/bullet_arrow_down2.png');
112
+ background-repeat: no-repeat;
113
+ background-position: center;
114
+
115
+ border-top-right-radius: 3px;
116
+ border-top-left-radius: 3px;
117
+
118
+ -moz-border-radius-topright: 3px;
119
+ -moz-border-radius-topleft: 3px;
120
+
121
+ -webkit-border-top-right-radius: 3px;
122
+ -webkit-border-top-left-radius: 3px;
123
+ }
124
+
125
+ a.ws_edit_link:hover {
126
+ background-color: #ffffd0;
127
+ background-image: url('../images/bullet_arrow_down2.png');
128
+ }
129
+
130
+ .ws_edit_link_expanded {
131
+ background-color: #ffffd0;
132
+ border-bottom: none;
133
+ border-color: #ffffd0;
134
+
135
+ background-image: url('../images/bullet_arrow_down2.png');
136
+ padding-bottom: 1px;
137
+ background-position: center 3px;
138
+ }
139
+
140
+ /****************************************
141
+ Per-menu settings fields & panels
142
+ *****************************************/
143
+
144
+ .ws_editbox {
145
+ display: block;
146
+ background-color: #ffffd0;
147
+ padding: 4px;
148
+
149
+ border-radius: 2px;
150
+ border-top-right-radius: 0px;
151
+
152
+ -moz-border-radius: 2px;
153
+ -moz-border-radius-topright: 0px;
154
+
155
+ -webkit-border-radius: 2px;
156
+ -webkit-border-top-right-radius: 0px;
157
+ }
158
+
159
+ .ws_edit_panel {
160
+ margin: 0;
161
+ padding: 0;
162
+ border: none;
163
+ }
164
+
165
+ .ws_edit_field {
166
+ margin-bottom: 8px;
167
+ height: 42.2px;
168
+ }
169
+
170
+ .ws_edit_field-custom {
171
+ margin-top: 10px;
172
+ }
173
+
174
+ /* The reset-to-default button */
175
+ .ws_reset_button {
176
+ display: block;
177
+ float: right;
178
+
179
+ margin-left: 4px;
180
+ margin-top: 2px;
181
+ margin-right: 6px;
182
+ cursor: pointer;
183
+
184
+ width: 16px;
185
+ height: 16px;
186
+ vertical-align: top;
187
+
188
+ background-image: url("../images/pencil_delete_gray.png");
189
+ background-repeat: no-repeat;
190
+ background-position: center;
191
+ }
192
+
193
+ .ws_reset_button:hover {
194
+ background-image: url("../images/pencil_delete.png");
195
+ }
196
+
197
+ .ws_input_default input, .ws_input_default select {
198
+ color: gray;
199
+ }
200
+
201
+ /* No reset button for fields set to the default value */
202
+ .ws_input_default .ws_reset_button {
203
+ visibility: hidden;
204
+ }
205
+
206
+ /* The input box in each field editor */
207
+ #ws_menu_editor .ws_editbox input[type="text"],
208
+ #ws_menu_editor .ws_editbox select {
209
+ display: block;
210
+ float: left;
211
+ width: 254px;
212
+
213
+ font-size: 12px;
214
+ padding: 3px;
215
+ }
216
+
217
+ #ws_menu_editor .ws_edit_field label {
218
+ display: block;
219
+ float: left;
220
+ }
221
+
222
+ #ws_menu_editor .ws_edit_field-custom input[type="checkbox"] {
223
+ margin-top: 0px;
224
+ }
225
+
226
+ /* Dropdown button for the capability field */
227
+ #ws_menu_editor .ws_dropdown_button {
228
+ display : block;
229
+ float: left;
230
+
231
+ width: 20px;
232
+ height: 20px;
233
+
234
+ margin: 1px 1px 1px 0;
235
+ padding: 0;
236
+
237
+ text-align: center;
238
+ vertical-align: middle;
239
+ font-size: 9px !important;
240
+
241
+ border-color: #dfdfdf;
242
+
243
+ border-top-right-radius: 3px;
244
+ border-bottom-right-radius: 3px;
245
+ border-top-left-radius: 0px;
246
+ border-bottom-left-radius: 0px;
247
+
248
+ -moz-border-radius-topright: 3px;
249
+ -moz-border-radius-bottomright: 3px;
250
+ -moz-border-radius-topleft: 0px;
251
+ -moz-border-radius-bottomleft: 0px;
252
+
253
+ -webkit-border-top-right-radius: 3px;
254
+ -webkit-border-bottom-right-radius: 3px;
255
+ -webkit-border-top-left-radius: 0px;
256
+ -webkit-border-bottom-left-radius: 0px;
257
+ }
258
+
259
+ /*
260
+ The capability field's appearance and size need to be changed
261
+ to accomodate the dropdown button.
262
+ */
263
+ #ws_menu_editor .ws_edit_field-access_level input.ws_field_value {
264
+ width: 230px;
265
+ margin-right: 0;
266
+ border-right: 0;
267
+
268
+ border-top-right-radius: 0px;
269
+ border-bottom-right-radius: 0px;
270
+
271
+ -moz-border-radius-topright: 0px;
272
+ -moz-border-radius-bottomright: 0px;
273
+
274
+ -webkit-border-top-right-radius: 0px;
275
+ -webkit-border-bottom-right-radius: 0px;
276
+ }
277
+
278
+ /* Unlike others, this field is just a single checkbox, so it has a smaller height */
279
+ #ws_menu_editor .ws_edit_field-custom {
280
+ height: 16px;
281
+ }
282
+
283
+ /*
284
+ * "Show/hide advanced fields"
285
+ */
286
+ .ws_toggle_container {
287
+ text-align: right;
288
+ margin-right: 27px;
289
+ }
290
+
291
+ .ws_toggle_advanced_fields {
292
+ color: #6087CB;
293
+ text-decoration: none;
294
+ font-size: 0.85em;
295
+ }
296
+
297
+ .ws_toggle_advanced_fields:visited, .ws_toggle_advanced_fields:active {
298
+ color: #6087CB;
299
+ }
300
+
301
+ .ws_toggle_advanced_fields:hover {
302
+ color: #d54e21;
303
+ text-decoration: underline;
304
+ }
305
+
306
+ /************************************
307
+ Menu flags
308
+ *************************************/
309
+
310
+ .ws_flag_container {
311
+ float: right;
312
+ margin-right: 4px;
313
+ padding-top: 2px;
314
+ }
315
+
316
+ .ws_flag {
317
+ display: block;
318
+ float: right;
319
+ width: 16px;
320
+ height: 16px;
321
+ margin-left: 4px;
322
+ background-repeat: no-repeat;
323
+ }
324
+
325
+ /* user-created items */
326
+ .ws_custom_item_flag {
327
+ background-image: url('../images/page_white_add.png');
328
+ }
329
+
330
+ /* items not present in the default menu */
331
+ .ws_missing_flag {
332
+ background-image: url('../images/plugin_error.png');
333
+ }
334
+
335
+ /* unused items - those that are in the default menu but not in the custom one */
336
+ .ws_unused_flag {
337
+ background-image: url('../images/plugin_add.png');
338
+ }
339
+
340
+ /* hidden items */
341
+ .ws_hidden_flag {
342
+ background-image: url('../images/plugin_disabled.png');
343
+ }
344
+
345
+ /* These classes could be used to apply different styles to items depending on their flags */
346
+ .ws_missing { }
347
+ .ws_custom_item { }
348
+ .ws_hidden { }
349
+ .ws_unused { }
350
+
351
+
352
+ /************************************
353
+ Toolbars
354
+ *************************************/
355
+
356
+ .ws_toolbar {
357
+ display: block;
358
+ width: 100%;
359
+ height: 34px;
360
+ }
361
+
362
+ .ws_button_container {
363
+ padding-left: 6px;
364
+ padding-top: 6px;
365
+ }
366
+
367
+ .ws_button {
368
+ display: block;
369
+ margin-right: 3px;
370
+ padding: 4px;
371
+ border: 1px solid #c0c0e0;
372
+ float: left;
373
+
374
+ border-radius: 3px;
375
+ -moz-border-radius: 3px;
376
+ -webkit-border-radius: 3px;
377
+ }
378
+
379
+ a.ws_button:hover {
380
+ background-color: #d0e0ff;
381
+ border-color: #9090c0;
382
+ }
383
+
384
+ .ws_separator {
385
+ float: left;
386
+ width: 5px;
387
+ }
388
+
389
+ /************************************
390
+ Capability selector
391
+ *************************************/
392
+
393
+ select#ws_cap_selector {
394
+ width: 252px;
395
+ height: 20em;
396
+
397
+ z-index: 1002;
398
+ position: absolute;
399
+ display: none;
400
+
401
+ font-family : "Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif;
402
+ font-size: 12px;
403
+ }
404
+
405
+ select#ws_cap_selector option {
406
+ font-family : "Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif;
407
+ font-size: 12px;
408
+ padding: 3px;
409
+ }
410
+
411
+ select#ws_cap_selector optgroup option {
412
+ padding-left: 10px;
413
+ }
414
+
415
+ /************************************
416
+ Export and import
417
+ *************************************/
418
+
419
+ #export_dialog, #import_dialog {
420
+ display: none;
421
+ }
422
+
423
+ .ui-widget-overlay {
424
+ background-color: black;
425
+ position: absolute;
426
+ left: 0px;
427
+ top: 0px;
428
+ opacity: 0.70;
429
+ -moz-opacity: 0.70;
430
+ filter: alpha(opacity=70);
431
+ }
432
+
433
+ .ui-dialog {
434
+ background: white;
435
+ border: 1px solid #c0c0c0;
436
+
437
+ padding: 2px;
438
+
439
+ border-radius: 5px;
440
+ -moz-border-radius: 5px;
441
+ -webkit-border-radius: 5px;
442
+ }
443
+
444
+ .ui-dialog-titlebar {
445
+ display: block;
446
+ height: 22px;
447
+ margin: 0;
448
+ padding: 4px 4px 4px 8px;
449
+
450
+ background-color: #86A7E3;
451
+ font-size: 1.0em;
452
+ line-height: 22px;
453
+
454
+ border-radius: 5px;
455
+ -moz-border-radius: 5px;
456
+ -webkit-border-radius: 5px;
457
+ }
458
+
459
+ .ui-dialog-title {
460
+ color: white;
461
+ font-weight: bold;
462
+ }
463
+
464
+ .ui-dialog-titlebar-close {
465
+ background-image: url(../images/x.png);
466
+ background-repeat: no-repeat;
467
+ background-position: center;
468
+ background-color: #86A7E3;
469
+
470
+ width: 22px;
471
+ height: 22px;
472
+ display: block;
473
+ float: right;
474
+ color: white;
475
+
476
+ border-radius: 3px;
477
+ -moz-border-radius: 3px;
478
+ -webkit-border-radius: 3px;
479
+ }
480
+
481
+ .ui-dialog-titlebar-close:hover {
482
+ /*background-image: url(../images/x-light.png);*/
483
+ background-color: #a6c2f5;
484
+ }
485
+
486
+ .ui-icon-closethick {
487
+
488
+ }
489
+
490
+ .ui-dialog-content {
491
+ padding: 6px;
492
+ font-size: 1.1em;
493
+ }
494
+
495
+ .ws_dialog_panel {
496
+ height: 84px;
497
+ }
498
+
499
+ #export_dialog .ws_dialog_panel {
500
+ height: 70px;
501
+ }
502
+
503
+ .ws_dialog_buttons {
504
+ height: 23px;
505
+ text-align: right;
506
+ }
507
+
508
+ .ws_dialog_buttons .button-primary {
509
+ display: block;
510
+ float: left;
511
+ margin-top: 0px;
512
+ }
513
+
514
+ .ws_dialog_buttons .button {
515
+ margin-top: 0px;
516
+ }
517
+
518
+ #import_file_selector {
519
+ display: block;
520
+ width: 286px;
521
+
522
+ margin-top: 6px;
523
+ margin-bottom: 12px;
524
+ margin-left: auto;
525
+ margin-right: auto;
526
+ }
527
+
528
+ #ws_start_import {
529
+ min-width: 100px;
530
+ }
531
+
532
+ #import_complete_notice {
533
+ text-align: center;
534
+ font-size: large;
535
+ padding-top: 25px;
536
+ }
537
+
538
+ /************************************
539
+ Screen meta buttons
540
+ *************************************/
541
+
542
+ /* All buttons */
543
+ #ws-ame-feedback-widget-wrap,
544
+ #ws-pro-version-notice {
545
+ float: right;
546
+ height: 22px;
547
+ padding: 0;
548
+ margin: 0 6px 0 0;
549
+ font-family: "Lucida Grande", Verdana, Arial, "Bitstream Vera Sans", sans-serif;
550
+ background: #e3e3e3; /* original gray */
551
+
552
+ border-bottom-left-radius: 3px;
553
+ border-bottom-right-radius: 3px;
554
+ -moz-border-radius-bottomleft: 3px;
555
+ -moz-border-radius-bottomright: 3px;
556
+ -webkit-border-bottom-left-radius: 3px;
557
+ -webkit-border-bottom-right-radius: 3px;
558
+ }
559
+
560
+ #ws-ame-feedback-widget-wrap a.show-settings,
561
+ #ws-pro-version-notice a.show-settings {
562
+ background-image: none;
563
+ padding:0 6px 0 6px;
564
+ }
565
+
566
+ /* "Upgrade to Pro" */
567
+ #ws-pro-version-notice {
568
+ background-color: #00C31F;
569
+ }
570
+
571
+
572
+ #ws-pro-version-notice a.show-settings {
573
+ font-weight: bold;
574
+ color: #DEFFD8;
575
+ text-shadow: none;
576
+ }
577
+
578
+ #ws-pro-version-notice a.show-settings:hover {
579
+ color: white;
580
+ }
images/menu-arrows.png ADDED
Binary file
images/pencil_delete.png ADDED
Binary file
images/pencil_delete_gray.png ADDED
Binary file
images/separator_add.png ADDED
Binary file
images/sort_ascending.png ADDED
Binary file
images/sort_descending.png ADDED
Binary file
images/spinner.gif ADDED
Binary file
images/transparent16.png ADDED
Binary file
images/x-light.png ADDED
Binary file
images/x.png ADDED
Binary file
admin-menu-editor-mu.php → includes/admin-menu-editor-mu.php RENAMED
@@ -18,9 +18,12 @@ mu-plugins/
18
  **/
19
 
20
  //Load the plugin
21
- $ws_menu_editor_filename = dirname(__FILE__) . '/admin-menu-editor/menu-editor.php';
 
22
  if ( file_exists($ws_menu_editor_filename) ) {
23
  require $ws_menu_editor_filename;
 
 
24
  } else {
25
  add_action('admin_notices', 'ws_ame_installation_error');
26
  }
@@ -34,7 +37,7 @@ function ws_ame_installation_error(){
34
  <p>
35
  Please copy the entire <code>admin-menu-directory</code> directory to your <code>mu-plugins</code>
36
  directory, then move only the admin-menu-editor-mu.php file from
37
- <code>admin-menu-editor</code> to <code>mu-plugins</code>.
38
  </p>
39
  </div>
40
  <?php
18
  **/
19
 
20
  //Load the plugin
21
+ $ws_menu_editor_filename = dirname(__FILE__) . '/admin-menu-editor/menu-editor.php';
22
+ $ws_menu_editor_pro_filename = dirname(__FILE__) . '/admin-menu-editor-pro/menu-editor.php';
23
  if ( file_exists($ws_menu_editor_filename) ) {
24
  require $ws_menu_editor_filename;
25
+ } elseif ( file_exists($ws_menu_editor_pro_filename) ) {
26
+ require $ws_menu_editor_pro_filename;
27
  } else {
28
  add_action('admin_notices', 'ws_ame_installation_error');
29
  }
37
  <p>
38
  Please copy the entire <code>admin-menu-directory</code> directory to your <code>mu-plugins</code>
39
  directory, then move only the admin-menu-editor-mu.php file from
40
+ <code>admin-menu-editor/includes</code> to <code>mu-plugins</code>.
41
  </p>
42
  </div>
43
  <?php
includes/menu-editor-core.php ADDED
@@ -0,0 +1,1133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ //Can't have two different versions of the plugin active at the same time. It would be incredibly buggy.
4
+ if (class_exists('WPMenuEditor')){
5
+ trigger_error(
6
+ 'Another version of Admin Menu Editor is already active. Please deactivate it before activating this one.',
7
+ E_USER_ERROR
8
+ );
9
+ }
10
+
11
+ //Load the "framework"
12
+ require 'shadow_plugin_framework.php';
13
+
14
+ if ( !class_exists('WPMenuEditor') ) :
15
+
16
+ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
17
+
18
+ protected $default_wp_menu = null; //Holds the default WP menu for later use in the editor
19
+ protected $default_wp_submenu = null; //Holds the default WP menu for later use
20
+ protected $title_lookups = array(); //A list of page titles indexed by $item['file']. Used to
21
+ //fix the titles of moved plugin pages.
22
+ private $custom_menu = null; //The current custom menu with defaults merged in
23
+ public $menu_format_version = 4;
24
+
25
+ private $templates = null; //Template arrays for various menu structures. See the constructor for details.
26
+
27
+ function init(){
28
+ //Determine if the plugin is active network-wide (i.e. either installed in
29
+ //the /mu-plugins/ directory or activated "network wide" by the super admin.
30
+ if ( $this->is_super_plugin() ){
31
+ $this->sitewide_options = true;
32
+ }
33
+
34
+ //Set some plugin-specific options
35
+ if ( empty($this->option_name) ){
36
+ $this->option_name = 'ws_menu_editor';
37
+ }
38
+ $this->defaults = array(
39
+ 'hide_advanced_settings' => true,
40
+ 'menu_format_version' => 0,
41
+ );
42
+ $this->serialize_with_json = false; //(Don't) store the options in JSON format
43
+
44
+ $this->settings_link = 'options-general.php?page=menu_editor';
45
+
46
+ $this->magic_hooks = true;
47
+ $this->magic_hook_priority = 99999;
48
+
49
+ //Build some template arrays
50
+ $this->templates['basic_defaults'] = array(
51
+ 'page_title' => '',
52
+ 'menu_title' => '',
53
+ 'access_level' => 'read',
54
+ 'file' => '',
55
+ 'css_class' => '',
56
+ 'hookname' => '',
57
+ 'icon_url' => '',
58
+ 'position' => 0,
59
+ 'separator' => false,
60
+ 'custom' => false,
61
+ 'open_in' => 'same_window', //'new_window', 'iframe' or 'same_window' (the default)
62
+ );
63
+
64
+ //Template for a basic top-level menu
65
+ $this->templates['blank_menu'] = array(
66
+ 'page_title' => null,
67
+ 'menu_title' => null,
68
+ 'access_level' => null,
69
+ 'file' => null,
70
+ 'css_class' => null,
71
+ 'hookname' => null,
72
+ 'icon_url' => null,
73
+ 'position' => null,
74
+ 'separator' => null,
75
+ 'custom' => null,
76
+ 'open_in' => null,
77
+ 'defaults' => $this->templates['basic_defaults'],
78
+ 'items' => array(),
79
+ );
80
+ //Template for menu items
81
+ $this->templates['blank_item'] = array(
82
+ 'menu_title' => null,
83
+ 'access_level' => null,
84
+ 'file' => null,
85
+ 'page_title' => null,
86
+ 'position' => null,
87
+ 'custom' => null,
88
+ 'open_in' => null,
89
+ 'defaults' => $this->templates['basic_defaults'],
90
+ );
91
+
92
+ //AJAXify screen options
93
+ add_action( 'wp_ajax_ws_ame_save_screen_options', array(&$this,'ajax_save_screen_options') );
94
+ }
95
+
96
+ /**
97
+ * Activation hook
98
+ *
99
+ * @return void
100
+ */
101
+ function activate(){
102
+ //If we have no stored settings for this version of the plugin, try importing them
103
+ //from other versions (i.e. the free or the Pro version).
104
+ if ( !$this->load_options() ){
105
+ $this->import_settings();
106
+ }
107
+
108
+ parent::activate();
109
+ }
110
+
111
+ /**
112
+ * Import settings from a different version of the plugin.
113
+ *
114
+ * @return bool True if settings were imported successfully, False otherwise
115
+ */
116
+ function import_settings(){
117
+ $possible_names = array('ws_menu_editor', 'ws_menu_editor_pro');
118
+ foreach($possible_names as $option_name){
119
+ if ( $this->load_options($option_name) ){
120
+ return true;
121
+ }
122
+ }
123
+
124
+ return false;
125
+ }
126
+
127
+ /**
128
+ * Add the JS required by the editor to the page header
129
+ *
130
+ * @return void
131
+ */
132
+ function enqueue_scripts(){
133
+ wp_enqueue_script('jquery');
134
+ wp_enqueue_script('jquery-ui-sortable');
135
+ wp_enqueue_script('jquery-ui-dialog');
136
+ wp_enqueue_script('jquery-form');
137
+
138
+ //jQuery JSON plugin
139
+ wp_enqueue_script('jquery-json', $this->plugin_dir_url.'/js/jquery.json-1.3.js', array('jquery'), '1.3');
140
+ //jQuery sort plugin
141
+ wp_enqueue_script('jquery-sort', $this->plugin_dir_url.'/js/jquery.sort.js', array('jquery'));
142
+
143
+ //Editor's scipts
144
+ wp_enqueue_script('menu-editor', $this->plugin_dir_url.'/js/menu-editor.js', array('jquery'), '1.0');
145
+ }
146
+
147
+ /**
148
+ * Add the editor's CSS file to the page header
149
+ *
150
+ * @return void
151
+ */
152
+ function enqueue_styles(){
153
+ wp_enqueue_style('menu-editor-style', $this->plugin_dir_url . '/css/menu-editor.css', array(), '1.0');
154
+ }
155
+
156
+ /**
157
+ * Create a configuration page and load the custom menu
158
+ *
159
+ * @return void
160
+ */
161
+ function hook_admin_menu(){
162
+ global $menu, $submenu;
163
+
164
+ //Menu reset (for emergencies). Executed by accessing http://example.com/wp-admin/?reset_admin_menu=1
165
+ $reset_requested = isset($_GET['reset_admin_menu']) && $_GET['reset_admin_menu'];
166
+ if ( $reset_requested && $this->current_user_can_edit_menu() ){
167
+ $this->options['custom_menu'] = null;
168
+ $this->save_options();
169
+ }
170
+
171
+ //The menu editor is only visible to users with the manage_options privilege.
172
+ //Or, if the plugin is installed in mu-plugins, only to the site administrator(s).
173
+ if ( $this->current_user_can_edit_menu() ){
174
+ $page = add_options_page(
175
+ apply_filters('admin_menu_editor-self_page_title', 'Menu Editor'),
176
+ apply_filters('admin_menu_editor-self_menu_title', 'Menu Editor'),
177
+ 'manage_options',
178
+ 'menu_editor',
179
+ array(&$this, 'page_menu_editor')
180
+ );
181
+ //Output our JS & CSS on that page only
182
+ add_action("admin_print_scripts-$page", array(&$this, 'enqueue_scripts'));
183
+ add_action("admin_print_styles-$page", array(&$this, 'enqueue_styles'));
184
+
185
+ //Make a placeholder for our screen options (hacky)
186
+ add_meta_box("ws-ame-screen-options", "You should never see this", array(&$this, 'noop'), $page);
187
+ }
188
+
189
+ //WP 3.0 in multisite mode has two separators with the same filename. This plugin
190
+ //expects all top-level menus to have unique filenames/URLs.
191
+ $first_separator1 = -1;
192
+ $last_separator1 = -1;
193
+ foreach($menu as $index => $item){
194
+ if ( $item[2] == 'separator1' ){
195
+ $last_separator1 = $index;
196
+ if ( $first_separator1 == -1 ){
197
+ $first_separator1 = $index;
198
+ }
199
+ }
200
+ }
201
+ if ( $first_separator1 != $last_separator1 ){
202
+ $menu[$first_separator1][2] = 'separator0';
203
+ }
204
+
205
+ //Store the "original" menus for later use in the editor
206
+ $this->default_wp_menu = $menu;
207
+ $this->default_wp_submenu = $submenu;
208
+
209
+ //Is there a custom menu to use?
210
+ if ( !empty($this->options['custom_menu']) ){
211
+ //Check if we need to upgrade the menu structure
212
+ if ( empty($this->options['menu_format_version']) || ($this->options['menu_format_version'] < $this->menu_format_version) ){
213
+ $this->options['custom_menu'] = $this->upgrade_menu_structure($this->options['custom_menu']);
214
+ $this->options['menu_format_version'] = $this->menu_format_version;
215
+ $this->save_options();
216
+ }
217
+ //Merge in data from the default menu
218
+ $tree = $this->menu_merge($this->options['custom_menu'], $menu, $submenu);
219
+ //Apply the custom menu
220
+ list($menu, $submenu, $this->title_lookups) = $this->tree2wp($tree);
221
+ //Save for later - the editor page will need it
222
+ $this->custom_menu = $tree;
223
+ //Re-filter the menu (silly WP should do that itself, oh well)
224
+ $this->filter_menu();
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Determine if the current user may use the menu editor.
230
+ *
231
+ * @return bool
232
+ */
233
+ function current_user_can_edit_menu(){
234
+ if ( $this->is_super_plugin() ){
235
+ return is_super_admin();
236
+ } else {
237
+ return current_user_can('manage_options');
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Intercept a handy action to fix the page title for moved plugin pages.
243
+ *
244
+ * @return void
245
+ */
246
+ function hook_admin_xml_ns(){
247
+ global $title;
248
+ global $pagenow;
249
+ global $plugin_page;
250
+
251
+ if ( empty($title) && !empty($plugin_page) && !empty($pagenow) ){
252
+ $file = sprintf('%s?page=%s', $pagenow, $plugin_page);
253
+ if ( isset($this->title_lookups[$file]) ){
254
+ $title = esc_html( strip_tags( $this->title_lookups[$file] ) );
255
+ }
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Loop over the Dashboard submenus and remove pages for which the current user does not have privs.
261
+ *
262
+ * @return void
263
+ */
264
+ function filter_menu(){
265
+ global $menu, $submenu, $_wp_submenu_nopriv, $_wp_menu_nopriv;
266
+
267
+ foreach ( array( 'submenu' ) as $sub_loop ) {
268
+ foreach ($$sub_loop as $parent => $sub) {
269
+ foreach ($sub as $index => $data) {
270
+ if ( ! current_user_can($data[1]) ) {
271
+ unset(${$sub_loop}[$parent][$index]);
272
+ $_wp_submenu_nopriv[$parent][$data[2]] = true;
273
+ }
274
+ }
275
+
276
+ if ( empty(${$sub_loop}[$parent]) )
277
+ unset(${$sub_loop}[$parent]);
278
+ }
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Encode a menu tree as JSON
284
+ *
285
+ * @param array $tree
286
+ * @return string
287
+ */
288
+ function getMenuAsJS($tree){
289
+ return $this->json_encode($tree);
290
+ }
291
+
292
+ /**
293
+ * Convert a WP menu structure to an associative array
294
+ *
295
+ * @param array $item An element of the $menu array
296
+ * @param integer $pos The position (index) of the menu item
297
+ * @return array
298
+ */
299
+ function menu2assoc($item, $pos=0){
300
+ $item = array(
301
+ 'menu_title' => $item[0],
302
+ 'access_level' => $item[1],
303
+ 'file' => $item[2],
304
+ 'page_title' => $item[3],
305
+ 'css_class' => $item[4],
306
+ 'hookname' => (isset($item[5])?$item[5]:''), //ID
307
+ 'icon_url' => (isset($item[6])?$item[6]:''),
308
+ 'position' => $pos,
309
+ );
310
+ $item['separator'] = strpos($item['css_class'], 'wp-menu-separator') !== false;
311
+ //Flag plugin pages
312
+ $item['is_plugin_page'] = (get_plugin_page_hook($item['file'], '') != null);
313
+
314
+ return array_merge($this->templates['basic_defaults'], $item);
315
+ }
316
+
317
+ /**
318
+ * Convert a WP submenu structure to an associative array
319
+ *
320
+ * @param array $item An element of the $submenu array
321
+ * @param integer $pos The position (index) of that element
322
+ * @param string $parent Parent file that this menu item belongs to.
323
+ * @return array
324
+ */
325
+ function submenu2assoc($item, $pos = 0, $parent = ''){
326
+ $item = array(
327
+ 'menu_title' => $item[0],
328
+ 'access_level' => $item[1],
329
+ 'file' => $item[2],
330
+ 'page_title' => (isset($item[3])?$item[3]:''),
331
+ 'position' => $pos,
332
+ );
333
+ //Save the default parent menu
334
+ $item['parent'] = $parent;
335
+ //Flag plugin pages
336
+ $item['is_plugin_page'] = (get_plugin_page_hook($item['file'], $parent) != null);
337
+
338
+ return array_merge($this->templates['basic_defaults'], $item);
339
+ }
340
+
341
+ /**
342
+ * Populate lookup arrays with default values from $menu and $submenu. Used later to merge
343
+ * a custom menu with the native WordPress menu structure somewhat gracefully.
344
+ *
345
+ * @param array $menu
346
+ * @param array $submenu
347
+ * @return array An array with two elements containing menu and submenu defaults.
348
+ */
349
+ function build_lookups($menu, $submenu){
350
+ //Process the top menu
351
+ $menu_defaults = array();
352
+ foreach($menu as $pos => $item){
353
+ $item = $this->menu2assoc($item, $pos);
354
+ $menu_defaults[$item['file']] = $item; //index by filename
355
+ }
356
+
357
+ //Process the submenu
358
+ $submenu_defaults = array();
359
+ foreach($submenu as $parent => $items){
360
+ foreach($items as $pos => $item){
361
+ $item = $this->submenu2assoc($item, $pos, $parent);
362
+ //File itself is not guaranteed to be unique, so we use a surrogate ID to identify submenus.
363
+ $uid = $this->unique_submenu_id($item['file'], $parent);
364
+ $submenu_defaults[$uid] = $item;
365
+ }
366
+ }
367
+
368
+ return array($menu_defaults, $submenu_defaults);
369
+ }
370
+
371
+ /**
372
+ * Merge $menu and $submenu into the $tree. Adds/replaces defaults, inserts new items
373
+ * and marks missing items as such.
374
+ *
375
+ * @param array $tree A menu in plugin's internal form
376
+ * @param array $menu WordPress menu structure
377
+ * @param array $submenu WordPress submenu structure
378
+ * @return array Updated menu tree
379
+ */
380
+ function menu_merge($tree, $menu, $submenu){
381
+ list($menu_defaults, $submenu_defaults) = $this->build_lookups($menu, $submenu);
382
+
383
+ //Iterate over all menus and submenus and look up default values
384
+ foreach ($tree as &$topmenu){
385
+ $topfile = $this->get_menu_field($topmenu, 'file');
386
+ //Is this menu present in the default WP menu?
387
+ if (isset($menu_defaults[$topfile])){
388
+ //Yes, load defaults from that item
389
+ $topmenu['defaults'] = $menu_defaults[$topfile];
390
+ //Note that the original item was used
391
+ $menu_defaults[$topfile]['used'] = true;
392
+ } else {
393
+ //Record the menu as missing, unless it's a menu separator
394
+ if ( empty($topmenu['separator']) ){
395
+ $topmenu['missing'] = true;
396
+ //[Nasty] Fill the 'defaults' array for menu's that don't have it.
397
+ //This should never be required - saving a custom menu should set the defaults
398
+ //for all menus it contains automatically.
399
+ if ( empty($topmenu['defaults']) ){
400
+ $tmp = $topmenu;
401
+ $topmenu['defaults'] = $tmp;
402
+ }
403
+ }
404
+ }
405
+
406
+ if (is_array($topmenu['items'])) {
407
+ //Iterate over submenu items
408
+ foreach ($topmenu['items'] as $file => &$item){
409
+ $uid = $this->unique_submenu_id($item, $topfile);
410
+
411
+ //Is this item present in the default WP menu?
412
+ if (isset($submenu_defaults[$uid])){
413
+ //Yes, load defaults from that item
414
+ $item['defaults'] = $submenu_defaults[$uid];
415
+ $submenu_defaults[$uid]['used'] = true;
416
+ } else {
417
+ //Record as missing
418
+ $item['missing'] = true;
419
+ if ( empty($item['defaults']) ){
420
+ $tmp = $item;
421
+ $item['defaults'] = $tmp;
422
+ }
423
+ }
424
+ }
425
+ }
426
+ }
427
+
428
+ //If we don't unset these they will fuck up the next two loops where the same names are used.
429
+ unset($topmenu);
430
+ unset($item);
431
+
432
+ //Note : Now we have some items marked as missing, and some items in lookup arrays
433
+ //that are not marked as used. The missing items are handled elsewhere (e.g. tree2wp()),
434
+ //but lets merge in the unused items now.
435
+
436
+ //Find and merge unused toplevel menus
437
+ foreach ($menu_defaults as $topfile => $topmenu){
438
+ //Skip used menus and separators
439
+ if ( !empty($topmenu['used']) || !empty($topmenu['separator'])) {
440
+ continue;
441
+ };
442
+
443
+ //Found an unused item. Build the tree entry.
444
+ $entry = $this->templates['blank_menu'];
445
+ $entry['defaults'] = $topmenu;
446
+ $entry['items'] = array(); //prepare a place for menu items, if any.
447
+ //Note that this item is unused
448
+ $entry['unused'] = true;
449
+ //Add the new entry to the menu tree
450
+ $tree[$topfile] = $entry;
451
+ }
452
+ unset($topmenu);
453
+
454
+ //Find and merge submenu items
455
+ foreach($submenu_defaults as $uid => $item){
456
+ if ( !empty($item['used']) ) continue;
457
+ //Found an unused item. Build an entry and attach it under the default toplevel menu.
458
+ $entry = $this->templates['blank_item'];
459
+ $entry['defaults'] = $item;
460
+ //Note that this item is unused
461
+ $entry['unused'] = true;
462
+
463
+ //Check if the toplevel menu exists
464
+ if (isset($tree[$item['parent']])) {
465
+ //Okay, insert the item.
466
+ $tree[$item['parent']]['items'][$item['file']] = $entry;
467
+ } else {
468
+ //Ooops? This should never happen. Some kind of inconsistency?
469
+ }
470
+ }
471
+
472
+ //Resort the tree to ensure the found items are in the right spots
473
+ $tree = $this->sort_menu_tree($tree);
474
+
475
+ return $tree;
476
+ }
477
+
478
+ /**
479
+ * Generate an ID that uniquely identifies a given submenu item.
480
+ *
481
+ * @param string|array $file Menu item in question
482
+ * @param string $parent Parent menu. Optional. If $file is an array, the function will try to get the parent value from $file['defaults'] instead.
483
+ * @return string Unique ID
484
+ */
485
+ function unique_submenu_id($file, $parent = ''){
486
+ if ( is_array($file) ){
487
+ if ( isset($file['defaults']) && isset($file['defaults']['parent']) ){
488
+ $parent = $file['defaults']['parent'];
489
+ }
490
+ $file = $this->get_menu_field($file, 'file');
491
+ }
492
+
493
+ if ( !empty($parent) ){
494
+ return $parent . '::' . $file;
495
+ } else {
496
+ return $file;
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Convert the WP menu structure to the internal representation. All properties set as defaults.
502
+ *
503
+ * @param array $menu
504
+ * @param array $submenu
505
+ * @return array Menu in the internal tree format.
506
+ */
507
+ function wp2tree($menu, $submenu){
508
+ $tree = array();
509
+ $separator_count = 0;
510
+ foreach ($menu as $pos => $item){
511
+
512
+ $tree_item = $this->templates['blank_menu'];
513
+ $tree_item['defaults'] = $this->menu2assoc($item, $pos);
514
+ $tree_item['separator'] = empty($item[2]) || empty($item[0]) || (strpos($item[4], 'wp-menu-separator') !== false);
515
+
516
+ if ( empty($tree_item['defaults']['file']) ){
517
+ $tree_item['defaults']['file'] = 'separator_'.$separator_count;
518
+ $separator_count++;
519
+ }
520
+
521
+ //Attach submenu items
522
+ $parent = $tree_item['defaults']['file'];
523
+ if ( isset($submenu[$parent]) ){
524
+ foreach($submenu[$parent] as $pos => $subitem){
525
+ $tree_item['items'][$subitem[2]] = array_merge(
526
+ $this->templates['blank_item'],
527
+ array('defaults' => $this->submenu2assoc($subitem, $pos, $parent))
528
+ );
529
+ }
530
+ }
531
+
532
+ $tree[$parent] = $tree_item;
533
+ }
534
+
535
+ $tree = $this->sort_menu_tree($tree);
536
+
537
+ return $tree;
538
+ }
539
+
540
+ /**
541
+ * Set all undefined menu fields to the default value
542
+ *
543
+ * @param array $item Menu item in the plugin's internal form
544
+ * @return array
545
+ */
546
+ function apply_defaults($item){
547
+ foreach($item as $key => $value){
548
+ //Is the field set?
549
+ if ($value === null){
550
+ //Use default, if available
551
+ if (isset($item['defaults']) && isset($item['defaults'][$key])){
552
+ $item[$key] = $item['defaults'][$key];
553
+ }
554
+ }
555
+ }
556
+ return $item;
557
+ }
558
+
559
+
560
+ /**
561
+ * Apply custom menu filters to an item of the custom menu.
562
+ *
563
+ * Calls two types of filters :
564
+ * 'custom_admin_$item_type' with the entire $item passed as the argument.
565
+ * 'custom_admin_$item_type-$field' with the value of a single field of $item as the argument.
566
+ *
567
+ * Used when converting the current custom menu to a WP-format menu.
568
+ *
569
+ * @param array $item Associative array representing one menu item (either top-level or submenu).
570
+ * @param string $item_type 'menu' or 'submenu'
571
+ * @param mixed $extra Optional extra data to pass to hooks.
572
+ * @return array Filtered menu item.
573
+ */
574
+ function apply_menu_filters($item, $item_type = '', $extra = null){
575
+ if ( empty($item_type) ){
576
+ //Only top-level menus have an icon
577
+ $item_type = isset($item['icon_url'])?'menu':'submenu';
578
+ }
579
+
580
+ $item = apply_filters("custom_admin_{$item_type}", $item, $extra);
581
+ foreach($item as $field => $value){
582
+ $item[$field] = apply_filters("custom_admin_{$item_type}-$field", $value, $extra);
583
+ }
584
+
585
+ return $item;
586
+ }
587
+
588
+ /**
589
+ * Get the value of a menu/submenu field.
590
+ * Will return the corresponding value from the 'defaults' entry of $item if the
591
+ * specified field is not set in the item itself.
592
+ *
593
+ * @param array $item
594
+ * @param string $field_name
595
+ * @param mixed $default Returned if the requested field is not set and is not listed in $item['defaults']. Defaults to null.
596
+ * @return mixed Field value.
597
+ */
598
+ function get_menu_field($item, $field_name, $default = null){
599
+ if ( isset($item[$field_name]) && ($item[$field_name] !== null) ){
600
+ return $item[$field_name];
601
+ } else {
602
+ if ( isset($item['defaults']) && isset($item['defaults'][$field_name]) ){
603
+ return $item['defaults'][$field_name];
604
+ } else {
605
+ return $default;
606
+ }
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Custom comparison function that compares menu items based on their position in the menu.
612
+ *
613
+ * @param array $a
614
+ * @param array $b
615
+ * @return int
616
+ */
617
+ function compare_position($a, $b){
618
+ if ($a['position']!==null) {
619
+ $p1 = $a['position'];
620
+ } else {
621
+ if ( isset($a['defaults']['position']) ){
622
+ $p1 = $a['defaults']['position'];
623
+ } else {
624
+ $p1 = 0;
625
+ }
626
+ }
627
+
628
+ if ($b['position']!==null) {
629
+ $p2 = $b['position'];
630
+ } else {
631
+ if ( isset($b['defaults']['position']) ){
632
+ $p2 = $b['defaults']['position'];
633
+ } else {
634
+ $p2 = 0;
635
+ }
636
+ }
637
+
638
+ return $p1 - $p2;
639
+ }
640
+
641
+ /**
642
+ * Sort the menus and menu items of a given menu according to their positions
643
+ *
644
+ * @param array $tree A menu structure in the internal format
645
+ * @return array Sorted menu in the internal format
646
+ */
647
+ function sort_menu_tree($tree){
648
+ //Resort the tree to ensure the found items are in the right spots
649
+ uasort($tree, array(&$this, 'compare_position'));
650
+ //Resort all submenus as well
651
+ foreach ($tree as &$topmenu){
652
+ if (!empty($topmenu['items'])){
653
+ uasort($topmenu['items'], array(&$this, 'compare_position'));
654
+ }
655
+ }
656
+
657
+ return $tree;
658
+ }
659
+
660
+ /**
661
+ * Convert internal menu representation to the form used by WP.
662
+ *
663
+ * Note : While this function doesn't cause any side effects of its own,
664
+ * it executes several filters that may modify global state. Specifically,
665
+ * IFrame-handling callbacks in 'extras.php' may insert items into the
666
+ * global $menu and $submenu arrays.
667
+ *
668
+ * @param array $tree
669
+ * @return array $menu and $submenu
670
+ */
671
+ function tree2wp($tree){
672
+ $menu = array();
673
+ $submenu = array();
674
+ $title_lookup = array();
675
+
676
+ //Sort the menu by position
677
+ uasort($tree, array(&$this, 'compare_position'));
678
+
679
+ //Prepare the top menu
680
+ $first_nonseparator_found = false;
681
+ foreach ($tree as $topmenu){
682
+
683
+ //Skip missing menus, unless they're user-created and thus might point to a non-standard file
684
+ $custom = $this->get_menu_field($topmenu, 'custom', false);
685
+ if ( !empty($topmenu['missing']) && !$custom ) {
686
+ continue;
687
+ };
688
+
689
+ //Skip hidden entries
690
+ if (!empty($topmenu['hidden'])) continue;
691
+
692
+ //Skip leading menu separators. Fixes a superfluous separator showing up
693
+ //in WP 3.0 (multisite mode) when there's a custom menu and the current user
694
+ //can't access its first item ("Super Admin").
695
+ if ( !empty($topmenu['separator']) && !$first_nonseparator_found ) continue;
696
+
697
+ $first_nonseparator_found = true;
698
+
699
+ //Build the WP item structure, using defaults where necessary
700
+ $topmenu = $this->apply_defaults($topmenu);
701
+ $topmenu = $this->apply_menu_filters($topmenu, 'menu');
702
+ $menu[] = array(
703
+ $topmenu['menu_title'],
704
+ $topmenu['access_level'],
705
+ $topmenu['file'],
706
+ $topmenu['page_title'],
707
+ $topmenu['css_class'],
708
+ $topmenu['hookname'], //ID
709
+ $topmenu['icon_url']
710
+ );
711
+
712
+ //Prepare the submenu of this menu
713
+ if( !empty($topmenu['items']) ){
714
+ $items = $topmenu['items'];
715
+ //Sort by position
716
+ uasort($items, array(&$this, 'compare_position'));
717
+
718
+ foreach ($items as $item) {
719
+
720
+ //Skip missing items, unless they're user-created
721
+ $custom = $this->get_menu_field($item, 'custom', false);
722
+ if ( !empty($item['missing']) && !$custom ) continue;
723
+ //Skip hidden items
724
+ if (!empty($item['hidden'])) {
725
+ continue;
726
+ }
727
+
728
+ //Special case : plugin pages that have been moved to a different menu.
729
+ //If the file field hasn't already been modified, we'll need to adjust it
730
+ //to point to the old parent. This is required because WP identifies
731
+ //plugin pages using *both* the plugin file and the parent file.
732
+ if ( $this->get_menu_field($item, 'is_plugin_page', false) && ($item['file'] === null) ){
733
+ $default_parent = '';
734
+ if ( isset($item['defaults']) && isset($item['defaults']['parent'])){
735
+ $default_parent = $item['defaults']['parent'];
736
+ }
737
+ if ( $topmenu['file'] != $default_parent ){
738
+ $item['file'] = $default_parent . '?page=' . $item['defaults']['file'];
739
+ }
740
+ }
741
+
742
+ $item = $this->apply_defaults($item);
743
+ $item = $this->apply_menu_filters($item, 'submenu', $topmenu['file']);
744
+ $submenu[$topmenu['file']][] = array(
745
+ $item['menu_title'],
746
+ $item['access_level'],
747
+ $item['file'],
748
+ $item['page_title'],
749
+ );
750
+
751
+ //Make a note of the page's correct title so we can fix it later
752
+ //if necessary.
753
+ $title_lookup[$item['file']] = $item['menu_title'];
754
+ }
755
+ }
756
+ }
757
+
758
+ return array($menu, $submenu, $title_lookup);
759
+ }
760
+
761
+ /**
762
+ * Upgrade a menu tree to the currently used structure
763
+ * Does nothing if the menu is already up to date.
764
+ *
765
+ * @param array $tree
766
+ * @return array
767
+ */
768
+ function upgrade_menu_structure($tree){
769
+
770
+ //Append new fields, if any
771
+ foreach($tree as &$menu){
772
+ $menu = array_merge($this->templates['blank_menu'], $menu);
773
+ $menu['defaults'] = array_merge($this->templates['basic_defaults'], $menu['defaults']);
774
+
775
+ foreach($menu['items'] as $item_file => $item){
776
+ $item = array_merge($this->templates['blank_item'], $item);
777
+ $item['defaults'] = array_merge($this->templates['basic_defaults'], $item['defaults']);
778
+ $menu['items'][$item_file] = $item;
779
+ }
780
+ }
781
+
782
+ return $tree;
783
+ }
784
+
785
+ /**
786
+ * Output the menu editor page
787
+ *
788
+ * @return void
789
+ */
790
+ function page_menu_editor(){
791
+ global $menu, $submenu;
792
+ global $wp_roles;
793
+
794
+ if ( !$this->current_user_can_edit_menu() ){
795
+ die("Access denied");
796
+ }
797
+
798
+ $action = isset($_POST['action'])?$_POST['action']:(isset($_GET['action'])?$_GET['action']:'');
799
+ do_action('admin_menu_editor_header', $action);
800
+
801
+ //Handle form submissions
802
+ if (isset($_POST['data'])){
803
+ check_admin_referer('menu-editor-form');
804
+
805
+ //Try to decode a menu tree encoded as JSON
806
+ $data = $this->json_decode($_POST['data'], true);
807
+ if (!$data || count(($data) < 2) ){
808
+ $fixed = stripslashes($_POST['data']);
809
+ $data = $this->json_decode( $fixed, true );
810
+ }
811
+
812
+ $url = remove_query_arg('noheader');
813
+
814
+ if ($data){
815
+ //Save the custom menu
816
+ $this->options['custom_menu'] = $data;
817
+ $this->save_options();
818
+ //Redirect back to the editor and display the success message
819
+ wp_redirect( add_query_arg('message', 1, $url) );
820
+ } else {
821
+ //Or redirect & display the error message
822
+ wp_redirect( add_query_arg('message', 2, $url) );
823
+ }
824
+ die();
825
+ }
826
+
827
+ //Attach a "Feedback" link to the screen meta panel.
828
+ $this->print_uservoice_widget();
829
+ //Kindly remind the user to give me money
830
+ if ( !apply_filters('admin_menu_editor_is_pro', false) ){
831
+ $this->print_upgrade_notice();
832
+ }
833
+ ?>
834
+ <div class="wrap">
835
+ <h2>
836
+ <?php echo apply_filters('admin_menu_editor-self_page_title', 'Menu Editor'); ?>
837
+ </h2>
838
+
839
+ <?php
840
+
841
+ if ( !empty($_GET['message']) ){
842
+ if ( intval($_GET['message']) == 1 ){
843
+ echo '<div id="message" class="updated fade"><p><strong>Settings saved.</strong></p></div>';
844
+ } elseif ( intval($_GET['message']) == 2 ) {
845
+ echo '<div id="message" class="error"><p><strong>Failed to decode input! The menu wasn\'t modified.</strong></p></div>';
846
+ }
847
+ }
848
+
849
+ //Build a tree struct. for the default menu
850
+ $default_menu = $this->wp2tree($this->default_wp_menu, $this->default_wp_submenu);
851
+
852
+ //Is there a custom menu?
853
+ if (!empty($this->custom_menu)){
854
+ $custom_menu = $this->custom_menu;
855
+ } else {
856
+ //Start out with the default menu if there is no user-created one
857
+ $custom_menu = $default_menu;
858
+ }
859
+
860
+ //Encode both menus as JSON
861
+ $default_menu_js = $this->getMenuAsJS($default_menu);
862
+ $custom_menu_js = $this->getMenuAsJS($custom_menu);
863
+
864
+ $plugin_url = $this->plugin_dir_url;
865
+ $images_url = $this->plugin_dir_url . '/images';
866
+
867
+ //Create a list of all known capabilities and roles. Used for the dropdown list on the access field.
868
+ $all_capabilities = $this->get_all_capabilities();
869
+ $all_capabilities = array_keys($all_capabilities);
870
+ natcasesort($all_capabilities);
871
+
872
+ $all_roles = $this->get_all_roles();
873
+ asort($all_roles);
874
+ ?>
875
+ <div id='ws_menu_editor'>
876
+ <div class='ws_main_container'>
877
+ <div class='ws_toolbar'>
878
+ <div class="ws_button_container">
879
+ <a id='ws_cut_menu' class='ws_button' href='javascript:void(0)' title='Cut'><img src='<?php echo $images_url; ?>/cut.png' /></a>
880
+ <a id='ws_copy_menu' class='ws_button' href='javascript:void(0)' title='Copy'><img src='<?php echo $images_url; ?>/page_white_copy.png' /></a>
881
+ <a id='ws_paste_menu' class='ws_button' href='javascript:void(0)' title='Paste'><img src='<?php echo $images_url; ?>/page_white_paste.png' /></a>
882
+
883
+ <div class="ws_separator">&nbsp;</div>
884
+
885
+ <a id='ws_new_menu' class='ws_button' href='javascript:void(0)' title='New menu'><img src='<?php echo $images_url; ?>/page_white_add.png' /></a>
886
+ <a id='ws_hide_menu' class='ws_button' href='javascript:void(0)' title='Show/Hide'><img src='<?php echo $images_url; ?>/plugin_disabled.png' /></a>
887
+ <a id='ws_delete_menu' class='ws_button' href='javascript:void(0)' title='Delete menu'><img src='<?php echo $images_url; ?>/page_white_delete.png' /></a>
888
+
889
+ <div class="ws_separator">&nbsp;</div>
890
+
891
+ <a id='ws_new_separator' class='ws_button' href='javascript:void(0)' title='New separator'><img src='<?php echo $images_url; ?>/separator_add.png' /></a>
892
+ </div>
893
+ </div>
894
+
895
+ <div id='ws_menu_box' class="ws_box">
896
+ </div>
897
+ </div>
898
+ <div class='ws_main_container'>
899
+ <div class='ws_toolbar'>
900
+ <div class="ws_button_container">
901
+ <a id='ws_cut_item' class='ws_button' href='javascript:void(0)' title='Cut'><img src='<?php echo $images_url; ?>/cut.png' /></a>
902
+ <a id='ws_copy_item' class='ws_button' href='javascript:void(0)' title='Copy'><img src='<?php echo $images_url; ?>/page_white_copy.png' /></a>
903
+ <a id='ws_paste_item' class='ws_button' href='javascript:void(0)' title='Paste'><img src='<?php echo $images_url; ?>/page_white_paste.png' /></a>
904
+
905
+ <div class="ws_separator">&nbsp;</div>
906
+
907
+ <a id='ws_new_item' class='ws_button' href='javascript:void(0)' title='New menu item'><img src='<?php echo $images_url; ?>/page_white_add.png' /></a>
908
+ <a id='ws_hide_item' class='ws_button' href='javascript:void(0)' title='Show/Hide'><img src='<?php echo $images_url; ?>/plugin_disabled.png' /></a>
909
+ <a id='ws_delete_item' class='ws_button' href='javascript:void(0)' title='Delete menu item'><img src='<?php echo $images_url; ?>/page_white_delete.png' /></a>
910
+
911
+ <div class="ws_separator">&nbsp;</div>
912
+
913
+ <a id='ws_sort_ascending' class='ws_button' href='javascript:void(0)' title='Sort ascending'>
914
+ <img src='<?php echo $images_url; ?>/sort_ascending.png' />
915
+ </a>
916
+ <a id='ws_sort_descending' class='ws_button' href='javascript:void(0)' title='Sort descending'>
917
+ <img src='<?php echo $images_url; ?>/sort_descending.png' />
918
+ </a>
919
+ </div>
920
+ </div>
921
+
922
+ <div id='ws_submenu_box' class="ws_box">
923
+ </div>
924
+ </div>
925
+ </div>
926
+
927
+ <div class="ws_main_container" id="ws_editor_sidebar">
928
+ <form method="post" action="<?php echo admin_url('options-general.php?page=menu_editor&noheader=1'); ?>" id='ws_main_form' name='ws_main_form'>
929
+ <?php wp_nonce_field('menu-editor-form'); ?>
930
+ <input type="hidden" name="data" id="ws_data" value="">
931
+ <input type="button" id='ws_save_menu' class="button-primary ws_main_button" value="Save Changes" />
932
+ </form>
933
+
934
+ <input type="button" id='ws_reset_menu' value="Undo changes" class="button ws_main_button" />
935
+ <input type="button" id='ws_load_menu' value="Load default menu" class="button ws_main_button" />
936
+
937
+ <?php
938
+ do_action('admin_menu_editor_sidebar');
939
+ ?>
940
+ </div>
941
+
942
+ </div>
943
+
944
+ <?php
945
+ //Createa a pop-up capability selector
946
+ $capSelector = array('<select id="ws_cap_selector" size="10">');
947
+
948
+ $capSelector[] = '<optgroup label="Roles">';
949
+ foreach($all_roles as $role_id => $role_name){
950
+ $capSelector[] = sprintf(
951
+ '<option value="%s">%s</option>',
952
+ esc_attr($role_id),
953
+ $role_name
954
+ );
955
+ }
956
+ $capSelector[] = '</optgroup>';
957
+
958
+ $capSelector[] = '<optgroup label="Capabilities">';
959
+ foreach($all_capabilities as $cap){
960
+ $capSelector[] = sprintf(
961
+ '<option value="%s">%s</option>',
962
+ esc_attr($cap),
963
+ $cap
964
+ );
965
+ }
966
+ $capSelector[] = '</optgroup>';
967
+ $capSelector[] = '</select>';
968
+
969
+ echo implode("\n", $capSelector);
970
+ ?>
971
+
972
+ <span id="ws-ame-screen-meta-contents" style="display:none;">
973
+ <label for="ws-hide-advanced-settings">
974
+ <input type="checkbox" id="ws-hide-advanced-settings"<?php
975
+ if ( $this->options['hide_advanced_settings'] ){
976
+ echo ' checked="checked"';
977
+ }
978
+ ?> /> Hide advanced options
979
+ </label>
980
+ </span>
981
+
982
+ <script type='text/javascript'>
983
+
984
+ var defaultMenu = <?php echo $default_menu_js; ?>;
985
+ var customMenu = <?php echo $custom_menu_js; ?>;
986
+
987
+ var imagesUrl = "<?php echo esc_js($images_url); ?>";
988
+
989
+ var adminAjaxUrl = "<?php echo esc_js(admin_url('admin-ajax.php')); ?>";
990
+
991
+ var hideAdvancedSettings = <?php echo $this->options['hide_advanced_settings']?'true':'false'; ?>;
992
+ var hideAdvancedSettingsNonce = '<?php echo esc_js(wp_create_nonce('ws_ame_save_screen_options')); ?>';
993
+
994
+ var captionShowAdvanced = 'Show advanced options';
995
+ var captionHideAdvanced = 'Hide advanced options';
996
+
997
+ window.wsMenuEditorPro = false; //Will be overwritten if extras are loaded
998
+
999
+ </script>
1000
+
1001
+ <?php
1002
+
1003
+ //Let the Pro version script output it's extra HTML & scripts.
1004
+ do_action('admin_menu_editor_footer');
1005
+ }
1006
+
1007
+ /**
1008
+ * Retrieve a list of all known capabilities of all roles
1009
+ *
1010
+ * @return array Associative array with capability names as keys
1011
+ */
1012
+ function get_all_capabilities(){
1013
+ global $wp_roles;
1014
+
1015
+ $capabilities = array();
1016
+
1017
+ if ( !isset($wp_roles) || !isset($wp_roles->roles) ){
1018
+ return $capabilities;
1019
+ }
1020
+
1021
+ //Iterate over all known roles and collect their capabilities
1022
+ foreach($wp_roles->roles as $role_id => $role){
1023
+ if ( !empty($role['capabilities']) && is_array($role['capabilities']) ){ //Being defensive here
1024
+ $capabilities = array_merge($capabilities, $role['capabilities']);
1025
+ }
1026
+ }
1027
+
1028
+ //Add multisite-specific capabilities (not listed in any roles in WP 3.0)
1029
+ $multisite_caps = array(
1030
+ 'manage_sites' => 1,
1031
+ 'manage_network' => 1,
1032
+ 'manage_network_users' => 1,
1033
+ 'manage_network_themes' => 1,
1034
+ 'manage_network_options' => 1,
1035
+ 'manage_network_plugins' => 1,
1036
+ );
1037
+ $capabilities = array_merge($capabilities, $multisite_caps);
1038
+
1039
+ return $capabilities;
1040
+ }
1041
+
1042
+ /**
1043
+ * Retrieve a list of all known roles
1044
+ *
1045
+ * @return array Associative array with role IDs as keys and role display names as values
1046
+ */
1047
+ function get_all_roles(){
1048
+ global $wp_roles;
1049
+ $roles = array();
1050
+
1051
+ if ( !isset($wp_roles) || !isset($wp_roles->roles) ){
1052
+ return $roles;
1053
+ }
1054
+
1055
+ foreach($wp_roles->roles as $role_id => $role){
1056
+ $roles[$role_id] = $role['name'];
1057
+ }
1058
+
1059
+ return $roles;
1060
+ }
1061
+
1062
+ /**
1063
+ * Output the JavaScript that adds the "Feedback" widget to screen meta.
1064
+ *
1065
+ * @return void
1066
+ */
1067
+ function print_uservoice_widget(){
1068
+ ?>
1069
+ <script type="text/javascript">
1070
+ (function($){
1071
+ $('#screen-meta-links').append(
1072
+ '<div id="ws-ame-feedback-widget-wrap" class="hide-if-no-js screen-meta-toggle">' +
1073
+ '<a href="http://feedback.w-shadow.com/forums/58572-admin-menu-editor" id="ws-ame-feedback-widget" class="show-settings" target="_blank" title="Open the user feedback forum">Feedback</a>' +
1074
+ '</div>'
1075
+ );
1076
+ })(jQuery);
1077
+ </script>
1078
+ <?php
1079
+ }
1080
+
1081
+ /**
1082
+ * Output the "Upgrade to Pro" message
1083
+ *
1084
+ * @return void
1085
+ */
1086
+ function print_upgrade_notice(){
1087
+ ?>
1088
+ <script type="text/javascript">
1089
+ (function($){
1090
+ $('#screen-meta-links').append(
1091
+ '<div id="ws-pro-version-notice" class="hide-if-no-js screen-meta-toggle">' +
1092
+ '<a href="http://wpplugins.com/plugin/146/admin-menu-editor-pro" id="ws-pro-version-notice-link" class="show-settings" target="_blank" title="View Pro version details">Upgrade to Pro</a>' +
1093
+ '</div>'
1094
+ );
1095
+ })(jQuery);
1096
+ </script>
1097
+ <?php
1098
+ }
1099
+
1100
+ /**
1101
+ * AJAX callback for saving screen options (whether to show or to hide advanced menu options).
1102
+ *
1103
+ * Handles the 'ws_ame_save_screen_options' action. The new option value
1104
+ * is read from $_POST['hide_advanced_settings'].
1105
+ *
1106
+ * @return void
1107
+ */
1108
+ function ajax_save_screen_options(){
1109
+ if (!current_user_can('manage_options') || !check_ajax_referer('ws_ame_save_screen_options', false, false)){
1110
+ die( $this->json_encode( array(
1111
+ 'error' => "You're not allowed to do that!"
1112
+ )));
1113
+ }
1114
+
1115
+ $this->options['hide_advanced_settings'] = !empty($_POST['hide_advanced_settings']);
1116
+ $this->save_options();
1117
+ die('1');
1118
+ }
1119
+
1120
+ /**
1121
+ * A callback for the stub meta box added to the plugin's page. Does nothing.
1122
+ *
1123
+ * @return void
1124
+ */
1125
+ function noop(){
1126
+ //nihil
1127
+ }
1128
+
1129
+ } //class
1130
+
1131
+ endif;
1132
+
1133
+ ?>
shadow_plugin_framework.php → includes/shadow_plugin_framework.php RENAMED
@@ -14,9 +14,15 @@ if ( ! defined( 'WP_PLUGIN_URL' ) )
14
  define( 'WP_PLUGIN_URL', WP_CONTENT_URL. '/plugins' );
15
  if ( ! defined( 'WP_PLUGIN_DIR' ) )
16
  define( 'WP_PLUGIN_DIR', WP_CONTENT_DIR . '/plugins' );
 
 
 
 
 
 
17
 
18
  class MenuEd_ShadowPluginFramework {
19
- public static $framework_version = '0.2';
20
 
21
  public $is_mu_plugin = null; //True if installed in the mu-plugins directory, false otherwise
22
 
@@ -24,6 +30,7 @@ class MenuEd_ShadowPluginFramework {
24
  public $option_name = ''; //should be set or overriden by the plugin
25
  protected $defaults = array(); //should be set or overriden by the plugin
26
  protected $sitewide_options = false; //WPMU only : save the setting in a site-wide option
 
27
 
28
  public $plugin_file = ''; //Filename of the plugin.
29
  public $plugin_basename = ''; //Basename of the plugin, as returned by plugin_basename().
@@ -35,17 +42,18 @@ class MenuEd_ShadowPluginFramework {
35
  protected $settings_link = ''; //If set, this will be automatically added after "Deactivate"/"Edit".
36
 
37
  /**
38
- * ShadowPluginFramework::__construct()
39
- * Initializes the plugin and loads settings from the database.
40
  *
41
  * @param string $plugin_file Plugin's filename. Usuallly you can just use __FILE__.
42
  * @return void
43
  */
44
- protected function __construct( $plugin_file = ''){
45
  if ($plugin_file == ''){
46
  //Try to guess the name of the file that included this file.
47
  //Not implemented yet.
48
  }
 
49
 
50
  if ( is_null($this->is_mu_plugin) )
51
  $this->is_mu_plugin = $this->is_in_wpmu_plugin_dir($plugin_file);
@@ -59,6 +67,32 @@ class MenuEd_ShadowPluginFramework {
59
  $this->plugin_dir_url = WP_PLUGIN_URL . '/' . dirname($this->plugin_basename);
60
  }
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  /************************************
63
  Load settings
64
  ************************************/
@@ -73,12 +107,7 @@ class MenuEd_ShadowPluginFramework {
73
  $this->load_options();
74
  }
75
 
76
- /************************************
77
- Add the default hooks
78
- ************************************/
79
- add_action('activate_'.$this->plugin_basename, array(&$this,'activate'));
80
- add_action('deactivate_'.$this->plugin_basename, array(&$this,'deactivate'));
81
-
82
  if ($this->settings_link)
83
  add_filter('plugin_action_links', array(&$this, 'plugin_action_links'), 10, 2);
84
 
@@ -87,16 +116,25 @@ class MenuEd_ShadowPluginFramework {
87
  }
88
 
89
  /**
90
- * ShadowPluginFramework::load_options()
91
- * Loads the plugin's configuration : loads an option specified by $this->option_name into $this->options.
92
  *
 
93
  * @return boolean TRUE if options were loaded okay and FALSE otherwise.
94
  */
95
- function load_options(){
 
 
 
 
96
  if ( $this->sitewide_options ) {
97
- $this->options = get_site_option($this->option_name);
98
  } else {
99
- $this->options = get_option($this->option_name);
 
 
 
 
100
  }
101
 
102
  if(!is_array($this->options)){
@@ -108,6 +146,57 @@ class MenuEd_ShadowPluginFramework {
108
  }
109
  }
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  /**
112
  * ShadowPluginFramework::set_magic_hooks()
113
  * Automagically sets up hooks for all methods named "hook_[tag]". Uses the Reflection API.
@@ -132,22 +221,7 @@ class MenuEd_ShadowPluginFramework {
132
  unset($class);
133
  }
134
 
135
- /**
136
- * ShadowPluginFramework::save_options()
137
- * Saves the $options array to the database.
138
- *
139
- * @return void
140
- */
141
- function save_options(){
142
- if ($this->option_name) {
143
- if ( $this->sitewide_options ) {
144
- update_site_option($this->option_name, $this->options);
145
- } else {
146
- update_option($this->option_name, $this->options);
147
- }
148
- }
149
- }
150
-
151
  /**
152
  * ShadowPluginFramework::activate()
153
  * Stub function for the activation hook. Simply stores the default configuration.
@@ -194,7 +268,6 @@ class MenuEd_ShadowPluginFramework {
194
  }
195
 
196
  /**
197
- * MenuEd_ShadowPluginFramework::is_in_wpmu_plugin_dir()
198
  * Checks if the specified file is inside the mu-plugins directory.
199
  *
200
  * @param string $filename The filename to check. Leave blank to use the current plugin's filename.
@@ -210,6 +283,52 @@ class MenuEd_ShadowPluginFramework {
210
  return (strpos( realpath($filename), realpath(WPMU_PLUGIN_DIR) ) !== false);
211
  }
212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  }
214
 
215
  ?>
14
  define( 'WP_PLUGIN_URL', WP_CONTENT_URL. '/plugins' );
15
  if ( ! defined( 'WP_PLUGIN_DIR' ) )
16
  define( 'WP_PLUGIN_DIR', WP_CONTENT_DIR . '/plugins' );
17
+
18
+
19
+ //Load JSON functions for PHP < 5.2
20
+ if (!class_exists('Services_JSON')){
21
+ require ABSPATH . WPINC . '/class-json.php';
22
+ }
23
 
24
  class MenuEd_ShadowPluginFramework {
25
+ public static $framework_version = '0.4';
26
 
27
  public $is_mu_plugin = null; //True if installed in the mu-plugins directory, false otherwise
28
 
30
  public $option_name = ''; //should be set or overriden by the plugin
31
  protected $defaults = array(); //should be set or overriden by the plugin
32
  protected $sitewide_options = false; //WPMU only : save the setting in a site-wide option
33
+ protected $serialize_with_json = false; //Use the JSON format for option storage
34
 
35
  public $plugin_file = ''; //Filename of the plugin.
36
  public $plugin_basename = ''; //Basename of the plugin, as returned by plugin_basename().
42
  protected $settings_link = ''; //If set, this will be automatically added after "Deactivate"/"Edit".
43
 
44
  /**
45
+ * Class constructor. Populates some internal fields, then calls the plugin's own
46
+ * intializer (if any).
47
  *
48
  * @param string $plugin_file Plugin's filename. Usuallly you can just use __FILE__.
49
  * @return void
50
  */
51
+ function __construct( $plugin_file = '', $option_name = null ){
52
  if ($plugin_file == ''){
53
  //Try to guess the name of the file that included this file.
54
  //Not implemented yet.
55
  }
56
+ $this->option_name = $option_name;
57
 
58
  if ( is_null($this->is_mu_plugin) )
59
  $this->is_mu_plugin = $this->is_in_wpmu_plugin_dir($plugin_file);
67
  $this->plugin_dir_url = WP_PLUGIN_URL . '/' . dirname($this->plugin_basename);
68
  }
69
 
70
+ /************************************
71
+ Add the default hooks
72
+ ************************************/
73
+ add_action('activate_'.$this->plugin_basename, array(&$this,'activate'));
74
+ add_action('deactivate_'.$this->plugin_basename, array(&$this,'deactivate'));
75
+
76
+ $this->init(); //Call the plugin's init() function
77
+ $this->init_finish(); //Complete initialization by loading settings, etc
78
+ }
79
+
80
+ /**
81
+ * Init the plugin. Should be overridden in a sub-class.
82
+ * Called by the class constructor.
83
+ *
84
+ * @return void
85
+ */
86
+ function init(){
87
+ //Do nothing.
88
+ }
89
+
90
+ /**
91
+ * Initialize settings and set up magic hooks.
92
+ *
93
+ * @return void
94
+ */
95
+ function init_finish(){
96
  /************************************
97
  Load settings
98
  ************************************/
107
  $this->load_options();
108
  }
109
 
110
+ //Add a "Settings" action link
 
 
 
 
 
111
  if ($this->settings_link)
112
  add_filter('plugin_action_links', array(&$this, 'plugin_action_links'), 10, 2);
113
 
116
  }
117
 
118
  /**
119
+ * Load the plugin's configuration.
120
+ * Loads the specified option into $this->options, substituting defaults where necessary.
121
  *
122
+ * @param string $option_name Optional. The slug of the option to load. If not set, the value of $this->option_name will be used instead.
123
  * @return boolean TRUE if options were loaded okay and FALSE otherwise.
124
  */
125
+ function load_options($option_name = null){
126
+ if ( empty($option_name) ){
127
+ $option_name = $this->option_name;
128
+ }
129
+
130
  if ( $this->sitewide_options ) {
131
+ $this->options = get_site_option($option_name);
132
  } else {
133
+ $this->options = get_option($option_name);
134
+ }
135
+
136
+ if ( $this->serialize_with_json || is_string($this->options) ){
137
+ $this->options = $this->json_decode($this->options, true);
138
  }
139
 
140
  if(!is_array($this->options)){
146
  }
147
  }
148
 
149
+ /**
150
+ * ShadowPluginFramework::save_options()
151
+ * Saves the $options array to the database.
152
+ *
153
+ * @return void
154
+ */
155
+ function save_options(){
156
+ if ($this->option_name) {
157
+ $stored_options = $this->options;
158
+ if ( $this->serialize_with_json ){
159
+ $stored_options = $this->json_encode($stored_options);
160
+ }
161
+
162
+ if ( $this->sitewide_options ) {
163
+ update_site_option($this->option_name, $stored_options);
164
+ } else {
165
+ update_option($this->option_name, $stored_options);
166
+ }
167
+ }
168
+ }
169
+
170
+
171
+ /**
172
+ * Backwards fompatible json_decode.
173
+ *
174
+ * @param string $data
175
+ * @param bool $assoc Decode objects as associative arrays.
176
+ * @return string
177
+ */
178
+ function json_decode($data, $assoc=false){
179
+ if ( function_exists('json_decode') ){
180
+ return json_decode($data, $assoc);
181
+ }
182
+ $flag = $assoc?SERVICES_JSON_LOOSE_TYPE:0;
183
+ $json = new Services_JSON($flag);
184
+ return( $json->decode($data) );
185
+ }
186
+
187
+ /**
188
+ * Backwards fompatible json_encode.
189
+ *
190
+ * @param mixed $data
191
+ * @return string
192
+ */
193
+ function json_encode($data) {
194
+ $json = new Services_JSON();
195
+ return( $json->encodeUnsafe($data) );
196
+ }
197
+
198
+
199
+
200
  /**
201
  * ShadowPluginFramework::set_magic_hooks()
202
  * Automagically sets up hooks for all methods named "hook_[tag]". Uses the Reflection API.
221
  unset($class);
222
  }
223
 
224
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  /**
226
  * ShadowPluginFramework::activate()
227
  * Stub function for the activation hook. Simply stores the default configuration.
268
  }
269
 
270
  /**
 
271
  * Checks if the specified file is inside the mu-plugins directory.
272
  *
273
  * @param string $filename The filename to check. Leave blank to use the current plugin's filename.
283
  return (strpos( realpath($filename), realpath(WPMU_PLUGIN_DIR) ) !== false);
284
  }
285
 
286
+ /**
287
+ * Check if the plugin is active for the entire network.
288
+ * Will return true when the plugin is installed in /mu-plugins/ (WPMU, pre-3.0)
289
+ * or has been activated via "Network Activate" (WP 3.0+).
290
+ *
291
+ * Blame the ridiculous blog/site/network confusion perpetrated by
292
+ * the WP API for the silly name.
293
+ *
294
+ * @return bool
295
+ */
296
+ function is_super_plugin(){
297
+ if ( is_null($this->is_mu_plugin) ){
298
+ $this->is_mu_plugin = $this->is_in_wpmu_plugin_dir($this->plugin_file);
299
+ }
300
+
301
+ if ( $this->is_mu_plugin ){
302
+ return true;
303
+ } else {
304
+ return $this->is_plugin_active_for_network($this->plugin_basename);
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Check whether the plugin is active for the entire network.
310
+ *
311
+ * Silly WP doesn't load the file that contains this native function until *after*
312
+ * all plugins are loaded, so until then we use a copy-pasted version of the same.
313
+ *
314
+ * @param string $plugin
315
+ * @return bool
316
+ */
317
+ function is_plugin_active_for_network( $plugin ) {
318
+ if ( function_exists('is_plugin_active_for_network') ){
319
+ return is_plugin_active_for_network($plugin);
320
+ }
321
+
322
+ if ( !is_multisite() )
323
+ return false;
324
+
325
+ $plugins = get_site_option( 'active_sitewide_plugins');
326
+ if ( isset($plugins[$plugin]) )
327
+ return true;
328
+
329
+ return false;
330
+ }
331
+
332
  }
333
 
334
  ?>
js/jquery.form.js ADDED
@@ -0,0 +1,675 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ /**
373
+ * ajaxForm() provides a mechanism for fully automating form submission.
374
+ *
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
384
+ * passes the options argument along after properly binding events for submit elements and
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
+ /**
427
+ * formToArray() gathers form element data into an array of objects that can
428
+ * be passed to any of the following ajax functions: $.get, $.post, or load.
429
+ * Each object in the array has both a 'name' and 'value' property. An example of
430
+ * an array for a simple login form might be:
431
+ *
432
+ * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
433
+ *
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
+ /**
479
+ * Serializes form data into a 'submittable' string. This method will return a string
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
+ /**
488
+ * Serializes all field elements in the jQuery object into a query string.
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
536
+ * v == ['C1']
537
+ *
538
+ * The successful argument controls whether or not the field element must be 'successful'
539
+ * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
540
+ * The default value of the successful argument is true. If this value is false the value(s)
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
+ /**
592
+ * Clears the form data. Takes the following actions on the form's input fields:
593
+ * - input text fields will have their 'value' property set to the empty string
594
+ * - select elements will have their 'selectedIndex' property set to -1
595
+ * - checkbox and radio inputs will have their 'checked' property set to false
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
+ /**
643
+ * Checks/unchecks any matching checkboxes or radio buttons and
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);
jquery.json-1.3.js → js/jquery.json-1.3.js RENAMED
File without changes
js/jquery.sort.js ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * jQuery.fn.sort
3
+ * --------------
4
+ * @param Function comparator:
5
+ * Exactly the same behaviour as [1,2,3].sort(comparator)
6
+ *
7
+ * @param Function getSortable
8
+ * A function that should return the element that is
9
+ * to be sorted. The comparator will run on the
10
+ * current collection, but you may want the actual
11
+ * resulting sort to occur on a parent or another
12
+ * associated element.
13
+ *
14
+ * E.g. $('td').sort(comparator, function(){
15
+ * return this.parentNode;
16
+ * })
17
+ *
18
+ * The <td>'s parent (<tr>) will be sorted instead
19
+ * of the <td> itself.
20
+ */
21
+ jQuery.fn.sort = (function(){
22
+
23
+ var sort = [].sort;
24
+
25
+ return function(comparator, getSortable) {
26
+
27
+ getSortable = getSortable || function(){return this;};
28
+
29
+ var placements = this.map(function(){
30
+
31
+ var sortElement = getSortable.call(this),
32
+ parentNode = sortElement.parentNode,
33
+
34
+ // Since the element itself will change position, we have
35
+ // to have some way of storing its original position in
36
+ // the DOM. The easiest way is to have a 'flag' node:
37
+ nextSibling = parentNode.insertBefore(
38
+ document.createTextNode(''),
39
+ sortElement.nextSibling
40
+ );
41
+
42
+ return function() {
43
+
44
+ if (parentNode === this) {
45
+ throw new Error(
46
+ "You can't sort elements if any one is a descendant of another."
47
+ );
48
+ }
49
+
50
+ // Insert before flag:
51
+ parentNode.insertBefore(this, nextSibling);
52
+ // Remove flag:
53
+ parentNode.removeChild(nextSibling);
54
+
55
+ };
56
+
57
+ });
58
+
59
+ return sort.call(this, comparator).each(function(i){
60
+ placements[i].call(getSortable.call(this));
61
+ });
62
+
63
+ };
64
+
65
+ })();
js/menu-editor.js ADDED
@@ -0,0 +1,1395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //(c) W-Shadow
2
+
3
+ var wsIdCounter = 0;
4
+
5
+ (function ($){
6
+
7
+ /*
8
+ * Utility function for generating pseudo-random alphanumeric menu IDs.
9
+ * Rationale: Simpler than atomically auto-incrementing or globally unique IDs.
10
+ */
11
+ function randomMenuId(size){
12
+ if ( typeof size == 'undefined' ){
13
+ size = 5;
14
+ }
15
+
16
+ var text = "";
17
+ var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
18
+
19
+ for( var i=0; i < size; i++ )
20
+ text += possible.charAt(Math.floor(Math.random() * possible.length));
21
+
22
+ return text;
23
+ }
24
+
25
+ function outputWpMenu(menu){
26
+ //Remove the current menu data
27
+ $('#ws_menu_box').empty();
28
+ $('#ws_submenu_box').empty();
29
+ //Kill autocomplete boxes
30
+ $('.ac_results').remove();
31
+
32
+ //Display the new menu
33
+ var i = 0;
34
+ for (var filename in menu){
35
+ outputTopMenu(menu[filename]);
36
+ i++;
37
+ }
38
+ }
39
+
40
+ /*
41
+ * Create edit widgets for a top-level menu and its submenus and append them all to the DOM.
42
+ *
43
+ * Inputs :
44
+ * menu - an object containing menu data
45
+ * afterNode - if specified, the new menu widget will be inserted after this node. Otherwise,
46
+ * it will be added to the end of the list.
47
+ * Outputs :
48
+ * Object with two fields - 'menu' and 'submenu' - containing the DOM nodes of the created widgets.
49
+ */
50
+ function outputTopMenu(menu, afterNode){
51
+ //Create a container for menu items, even if there are none
52
+ var submenu = buildSubmenu(menu.items);
53
+
54
+ //Create the menu widget
55
+ var menu_obj = buildTopMenu(menu);
56
+ menu_obj.data('submenu_id', submenu.attr('id'));
57
+
58
+ //Display
59
+ submenu.appendTo('#ws_submenu_box');
60
+ if ( typeof afterNode != 'undefined' ){
61
+ $(afterNode).after(menu_obj);
62
+ } else {
63
+ menu_obj.appendTo('#ws_menu_box');
64
+ }
65
+
66
+ return {
67
+ 'menu' : menu_obj,
68
+ 'submenu' : submenu
69
+ };
70
+ }
71
+
72
+ /*
73
+ * Create an edit widget for a top-level menu.
74
+ */
75
+ function buildTopMenu(menu){
76
+ var subclass = '';
77
+ if ( menu.separator ) {
78
+ subclass = subclass + ' ws_menu_separator';
79
+ }
80
+
81
+ //Create the menu HTML
82
+ var menu_obj = $('<div></div>')
83
+ .attr('class', "ws_container ws_menu "+subclass)
84
+ .attr('id', 'ws-topmenu-'+(wsIdCounter++))
85
+ .data('defaults', menu.defaults)
86
+ .data('initial_values', getCurrentFieldValues(menu))
87
+ .data('field_editors_created', false);
88
+
89
+ //Add a header and a container for property editors (to improve performance
90
+ //the editors themselves are created later, when the user tries to access them
91
+ //for the first time).
92
+ var contents = [];
93
+ contents.push(
94
+ '<div class="ws_item_head">',
95
+ '<a class="ws_edit_link"> </a><div class="ws_flag_container"> </div>',
96
+ '<span class="ws_item_title">',
97
+ ((menu.menu_title!=null)?menu.menu_title:menu.defaults.menu_title),
98
+ '&nbsp;</span>',
99
+ '</div>',
100
+ '<div class="ws_editbox" style="display: none;"></div>'
101
+ );
102
+ menu_obj.append(contents.join(''));
103
+
104
+ //Apply flags based on the item's state
105
+ if (menu.missing && !getFieldValue(menu, 'custom', false)) {
106
+ addMenuFlag(menu_obj, 'missing');
107
+ }
108
+ if (menu.hidden) {
109
+ addMenuFlag(menu_obj, 'hidden');
110
+ }
111
+ if (menu.unused) {
112
+ addMenuFlag(menu_obj, 'unused');
113
+ }
114
+ if (getFieldValue(menu, 'custom', false)) {
115
+ addMenuFlag(menu_obj, 'custom_item');
116
+ }
117
+
118
+ return menu_obj;
119
+ }
120
+
121
+ /*
122
+ * Create and populate a submenu container.
123
+ */
124
+ function buildSubmenu(items){
125
+ //Create a container for menu items, even if there are none
126
+ var submenu = $('<div class="ws_submenu"style="display:none;"></div>');
127
+ submenu.attr('id', 'ws-submenu-'+(wsIdCounter++));
128
+
129
+ //Only show menus that have items.
130
+ //Skip arrays (with a length) because filled menus are encoded as custom objects.
131
+ var entry = null
132
+ if (items && (typeof items != 'Array')){
133
+ for (var item_file in items){
134
+ entry = buildMenuItem(items[item_file]);
135
+ if ( entry ){
136
+ submenu.append(entry);
137
+ }
138
+ }
139
+ }
140
+
141
+ //Make the submenu sortable
142
+ makeBoxSortable(submenu);
143
+
144
+ return submenu;
145
+ }
146
+
147
+ /*
148
+ * Create an edit widget for a menu entry and return it.
149
+ */
150
+ function buildMenuItem(entry){
151
+ if (!entry.defaults) {
152
+ return null
153
+ };
154
+
155
+ var item = $('<div class="ws_container ws_item">')
156
+ .data('defaults', entry.defaults)
157
+ .data('initial_values', getCurrentFieldValues(entry))
158
+ .data('field_editors_created', false);
159
+
160
+ //Add a header and a container for property editors (to improve performance
161
+ //the editors themselves are created later, when the user tries to access them
162
+ //for the first time).
163
+ var contents = [];
164
+ contents.push(
165
+ '<div class="ws_item_head">',
166
+ '<a class="ws_edit_link"> </a><div class="ws_flag_container"> </div>',
167
+ '<span class="ws_item_title">',
168
+ ((entry.menu_title!=null)?entry.menu_title:entry.defaults.menu_title),
169
+ '&nbsp;</span>',
170
+ '</div>',
171
+ '<div class="ws_editbox" style="display: none;"></div>'
172
+ );
173
+ item.append(contents.join(''));
174
+
175
+ //Apply flags based on the item's state
176
+ if (entry.missing && !getFieldValue(entry, 'custom', false)) {
177
+ addMenuFlag(item, 'missing');
178
+ }
179
+ if (entry.hidden) {
180
+ addMenuFlag(item, 'hidden');
181
+ }
182
+ if (entry.unused) {
183
+ addMenuFlag(item, 'unused');
184
+ }
185
+ if (getFieldValue(entry, 'custom', false)) {
186
+ addMenuFlag(item, 'custom_item');
187
+ }
188
+
189
+ return item;
190
+ }
191
+
192
+ /*
193
+ * List of all menu fields that have an associated editor
194
+ */
195
+ var knownMenuFields = {
196
+ 'menu_title' : {
197
+ caption : 'Menu title',
198
+ standardCaption : true,
199
+ advanced : false,
200
+ type : 'text',
201
+ defaultValue: '',
202
+ visible: true
203
+ },
204
+ 'access_level' : {
205
+ caption: 'Required capability',
206
+ standardCaption : true,
207
+ advanced : false,
208
+ type : 'text',
209
+ defaultValue: 'read',
210
+ addDropdown : true,
211
+ visible: true
212
+ },
213
+ 'file' : {
214
+ caption: 'URL',
215
+ advanced : false,
216
+ standardCaption : true,
217
+ type : 'text',
218
+ defaultValue: '',
219
+ visible: true
220
+ },
221
+ 'page_title' : {
222
+ caption: "Window title",
223
+ standardCaption : true,
224
+ advanced : true,
225
+ type : 'text',
226
+ defaultValue: '',
227
+ visible: true
228
+ },
229
+ 'open_in' : {
230
+ caption: 'Open in',
231
+ standardCaption : true,
232
+ advanced : true,
233
+ type : 'select',
234
+ options : {
235
+ 'Same window or tab' : 'same_window',
236
+ 'New window' : 'new_window',
237
+ 'Frame' : 'iframe'
238
+ },
239
+ defaultValue: 'same_window',
240
+ visible: false
241
+ },
242
+ 'css_class' : {
243
+ caption: 'CSS classes',
244
+ standardCaption : true,
245
+ advanced : true,
246
+ type : 'text',
247
+ defaultValue: '',
248
+ visible: true
249
+ },
250
+ 'hookname' : {
251
+ caption: 'Hook name',
252
+ standardCaption : true,
253
+ advanced : true,
254
+ type : 'text',
255
+ defaultValue: '',
256
+ visible: true
257
+ },
258
+ 'icon_url' : {
259
+ caption: 'Icon URL',
260
+ standardCaption : true,
261
+ advanced : true,
262
+ type : 'text',
263
+ defaultValue: 'div',
264
+ visible: true
265
+ },
266
+ 'custom' : {
267
+ caption : 'Custom',
268
+ standardCaption : false,
269
+ advanced: true,
270
+ type: 'checkbox',
271
+ defaultValue: false,
272
+ visible: true
273
+ }
274
+ };
275
+
276
+ /*
277
+ * Create editors for the visible fields of a menu entry and append them to the specified node.
278
+ */
279
+ function buildEditboxFields(containerNode, entry){
280
+ var fields = knownMenuFields;
281
+
282
+ var basicFields = $('<div class="ws_edit_panel ws_basic"></div>').appendTo(containerNode);
283
+ var advancedFields = $('<div class="ws_edit_panel ws_advanced"></div>').appendTo(containerNode);
284
+
285
+ if ( hideAdvancedSettings ){
286
+ advancedFields.css('display', 'none');
287
+ }
288
+
289
+ for (var field_name in fields){
290
+ var field = buildEditboxField(entry, field_name, fields[field_name]);
291
+ if (field){
292
+ if (fields[field_name].advanced){
293
+ advancedFields.append(field);
294
+ } else {
295
+ basicFields.append(field);
296
+ }
297
+ }
298
+ }
299
+
300
+ //Add a link that shows/hides advanced fields
301
+ containerNode.append(
302
+ '<div class="ws_toggle_container"><a href="#" class="ws_toggle_advanced_fields"'+
303
+ (hideAdvancedSettings?'':' style="display:none;"')+'>'+
304
+ (hideAdvancedSettings?captionShowAdvanced:captionHideAdvanced)
305
+ +'</a></div>'
306
+ );
307
+ }
308
+
309
+ /*
310
+ * Create an editor for a specified field.
311
+ */
312
+ function buildEditboxField(entry, field_name, field_settings){
313
+ if (typeof entry[field_name] === 'undefined') {
314
+ return null; //skip fields this entry doesn't have
315
+ }
316
+
317
+ var default_value = (typeof entry.defaults[field_name] != 'undefined')?entry.defaults[field_name]:field_settings.defaultValue;
318
+ var value = (entry[field_name]!=null)?entry[field_name]:default_value;
319
+
320
+ //Build a form field of the appropriate type
321
+ var inputBox = null;
322
+ switch(field_settings.type){
323
+ case 'select':
324
+ inputBox = $('<select class="ws_field_value">');
325
+ var option = null;
326
+ for( var optionTitle in field_settings.options ){
327
+ option = $('<option>')
328
+ .val(field_settings.options[optionTitle])
329
+ .text(optionTitle);
330
+ if ( field_settings.options[optionTitle] == value ){
331
+ option.attr('selected', 'selected');
332
+ }
333
+ option.appendTo(inputBox);
334
+ }
335
+ break;
336
+
337
+ case 'checkbox':
338
+ inputBox = $('<label><input type="checkbox"'+(value?' checked="checked"':'')+ ' class="ws_field_value"> '+
339
+ field_settings.caption+'</label>'
340
+ );
341
+ break;
342
+
343
+ case 'text':
344
+ default:
345
+ inputBox = $('<input type="text" class="ws_field_value">').val(value);
346
+ }
347
+
348
+
349
+ var className = "ws_edit_field ws_edit_field-"+field_name;
350
+ if(entry[field_name]==null){
351
+ className += ' ws_input_default';
352
+ }
353
+
354
+ var editField = $('<div>' + (field_settings.standardCaption?(field_settings.caption+'<br>'):'') + '</div>')
355
+ .attr('class', className)
356
+ .append(inputBox);
357
+
358
+ if ( (typeof(field_settings['addDropdown']) != 'undefined') && field_settings.addDropdown ){
359
+ //Add a dropdown button
360
+ editField.append('<input type="button" class="button ws_dropdown_button" value="&#9660;" tabindex="-1">');
361
+ }
362
+
363
+ editField
364
+ .append('<img src="'+imagesUrl+'/transparent16.png" class="ws_reset_button" title="Reset to default value">&nbsp;</img>')
365
+ .data('field_name', field_name)
366
+ .data('default_value', default_value);
367
+
368
+ if ( !field_settings.visible ){
369
+ editField.css('display', 'none');
370
+ }
371
+
372
+ return editField;
373
+ }
374
+
375
+ /*
376
+ * Get the current values of all menu fields (ignores defaults).
377
+ * Returns an object containing each field as a separate property.
378
+ */
379
+ function getCurrentFieldValues(entry){
380
+ var values = {};
381
+
382
+ for (var field_name in knownMenuFields){
383
+ if (typeof entry[field_name] === 'undefined') {
384
+ continue; //skip fields this entry doesn't have
385
+ }
386
+ values[field_name] = entry[field_name];
387
+ }
388
+
389
+ values.defaults = entry.defaults;
390
+
391
+ return values;
392
+ }
393
+
394
+ /*
395
+ * Get the current value of a single menu field.
396
+ *
397
+ * If the specified field is not set, this function will attempt to retrieve it
398
+ * from the "defaults" property of the menu object. If *that* fails, it will return
399
+ * the value of the optional third argument defaultValue.
400
+ */
401
+ function getFieldValue(entry, fieldName, defaultValue){
402
+ if ( (typeof entry[fieldName] === 'undefined') || (entry[fieldName] === null) ) {
403
+ if ( (typeof entry['defaults'] === 'undefined') || (typeof entry['defaults'][fieldName] === 'undefined') ){
404
+ return defaultValue;
405
+ } else {
406
+ return entry.defaults[fieldName];
407
+ }
408
+ } else {
409
+ return entry[fieldName];
410
+ }
411
+ }
412
+
413
+ /*
414
+ * Make a menu container sortable
415
+ */
416
+ function makeBoxSortable(menuBox){
417
+ //Make the submenu sortable
418
+ menuBox.sortable({
419
+ items: '> .ws_container',
420
+ cursor: 'move',
421
+ dropOnEmpty: true,
422
+ cancel : '.ws_editbox, .ws_edit_link'
423
+ });
424
+ }
425
+
426
+ /***************************************************************************
427
+ Parsing & encoding menu inputs
428
+ ***************************************************************************/
429
+
430
+ /*
431
+ * Encode the current menu structure as JSON
432
+ *
433
+ * Returns :
434
+ * A JSON-encoded string representing the current menu tree loaded in the editor.
435
+ */
436
+ function encodeMenuAsJSON(){
437
+ var tree = readMenuTreeState();
438
+ return $.toJSON(tree);
439
+ }
440
+
441
+ function readMenuTreeState(){
442
+ var tree = {};
443
+ var menu_position = 0;
444
+
445
+ //Gather all menus and their items
446
+ $('#ws_menu_box .ws_menu').each(function(i) {
447
+ var menu = readMenuState(this, menu_position++);
448
+
449
+ //Attach the current menu to the main struct
450
+ var filename = (menu.file !== null)?menu.file:menu.defaults.file;
451
+ tree[filename] = menu;
452
+ });
453
+
454
+ return tree;
455
+ }
456
+
457
+ /*
458
+ * Extract the settings of a top-level menu from its editor widget(s).
459
+ *
460
+ * Inputs :
461
+ * menu_div - DOM node, typically one with the .ws_menu class.
462
+ * position - The current menu position (int).
463
+ *
464
+ * Output :
465
+ * A menu object in the tree format.
466
+ *
467
+ */
468
+ function readMenuState(menu_div, position){
469
+ menu_div = $(menu_div);
470
+ var menu = readAllFields(menu_div);
471
+
472
+ menu.defaults = menu_div.data('defaults');
473
+
474
+ menu.position = position;
475
+ menu.defaults.position = position; //the real default value will later overwrite this
476
+
477
+ menu.separator = menu_div.hasClass('ws_menu_separator');
478
+ menu.hidden = menu_div.hasClass('ws_hidden');
479
+
480
+ //Gather the menu's items, if any
481
+ menu.items = {};
482
+ var item_position = 0;
483
+ $('#'+menu_div.data('submenu_id')).find('.ws_item').each(function (i) {
484
+ var item = readItemState(this, item_position++);
485
+ menu.items[ (item.file?item.file:item.defaults.file) ] = item;
486
+ });
487
+
488
+ return menu;
489
+ }
490
+
491
+ /*
492
+ * Extract the current menu item settings from its editor widget.
493
+ *
494
+ * Inputs :
495
+ * item_div - DOM node containing the editor widget, usually with the .ws_item class.
496
+ * position - Menu item position among its sibling menu items.
497
+ *
498
+ */
499
+ function readItemState(item_div, position){
500
+ var item_div = $(item_div);
501
+ var item = readAllFields(item_div);
502
+
503
+ item.defaults = item_div.data('defaults');
504
+
505
+ //Save the position data
506
+ if ( typeof position == 'undefined' ){
507
+ position = 0;
508
+ }
509
+ item.position = position;
510
+ item.defaults.position = position;
511
+
512
+ //Check if the item is hidden
513
+ item.hidden = item_div.hasClass('ws_hidden');
514
+
515
+ return item;
516
+ }
517
+
518
+ /*
519
+ * Extract the values of all menu/item fields present in a container node
520
+ *
521
+ * Inputs:
522
+ * container - a jQuery collection representing the node to read.
523
+ */
524
+ function readAllFields(container){
525
+ if ( !container.hasClass('ws_container') ){
526
+ container = container.parents('ws_container').first();
527
+ }
528
+
529
+ if ( !container.data('field_editors_created') ){
530
+ return container.data('initial_values');
531
+ }
532
+
533
+ var state = {};
534
+
535
+ //Iterate over all fields of the item
536
+ container.find('.ws_edit_field').each(function() {
537
+ var field = $(this);
538
+
539
+ //Get the name of this field
540
+ field_name = field.data('field_name');
541
+ //Skip if unnamed
542
+ if (!field_name) return true;
543
+
544
+ //Find the field (usually an input or select element).
545
+ input_box = field.find('.ws_field_value');
546
+
547
+ //Save null if default used, custom value otherwise
548
+ if (field.hasClass('ws_input_default')){
549
+ state[field_name] = null;
550
+ } else {
551
+ if ( input_box.attr('type') == 'checkbox' ){
552
+ state[field_name] = input_box.is(':checked');
553
+ } else {
554
+ state[field_name] = input_box.val();
555
+ }
556
+ }
557
+ });
558
+
559
+ return state;
560
+ }
561
+
562
+
563
+ /***************************************************************************
564
+ Flag manipulation
565
+ ***************************************************************************/
566
+
567
+ var item_flags = {
568
+ 'custom_item' : 'This is a custom menu item',
569
+ 'unused' : 'This item was automatically (re)inserted into your custom menu because it is present in the default WordPress menu',
570
+ 'missing' : 'This item is not present in the default WordPress menu. Tick the &quot;Custom&quot; checkbox if you want it to be visible anyway.',
571
+ 'hidden' : 'This item is hidden'
572
+ }
573
+
574
+ function addMenuFlag(item, flag){
575
+ item = $(item);
576
+
577
+ var item_class = 'ws_' + flag;
578
+ var img_class = 'ws_' + flag + '_flag';
579
+
580
+ item.addClass(item_class);
581
+ //Add the flag image
582
+ var flag_container = item.find('.ws_flag_container');
583
+ if ( flag_container.find('.' + img_class).length == 0 ){
584
+ flag_container.append('<div class="ws_flag '+img_class+'" title="'+item_flags[flag]+'"></div>');
585
+ }
586
+ }
587
+
588
+ function removeMenuFlag(item, flag){
589
+ item = $(item);
590
+ var item_class = 'ws_' + flag;
591
+ var img_class = 'ws_' + flag + '_flag';
592
+
593
+ item.removeClass('ws_' + flag);
594
+ item.find('.' + img_class).remove();
595
+ }
596
+
597
+ function toggleMenuFlag(item, flag){
598
+ if (menuHasFlag(item, flag)){
599
+ removeMenuFlag(item, flag);
600
+ } else {
601
+ addMenuFlag(item, flag);
602
+ }
603
+ }
604
+
605
+ function menuHasFlag(item, flag){
606
+ return $(item).hasClass('ws_'+flag);
607
+ }
608
+
609
+ function clearMenuFlags(item){
610
+ item = $(item);
611
+ item.find('.ws_flag').remove();
612
+ for(var flag in item_flags){
613
+ item.removeClass('ws_'+flag);
614
+ }
615
+ }
616
+
617
+ //Cut & paste stuff
618
+ var menu_in_clipboard = null;
619
+ var submenu_in_clipboard = null;
620
+ var item_in_clipboard = null;
621
+ var ws_paste_count = 0;
622
+
623
+ $(document).ready(function(){
624
+ if (window.wsMenuEditorPro) {
625
+ knownMenuFields['open_in'].visible = true;
626
+ };
627
+
628
+ //Show the menu
629
+ outputWpMenu(customMenu);
630
+
631
+ //Make the top menu box sortable (we only need to do this once)
632
+ var mainMenuBox = $('#ws_menu_box');
633
+ makeBoxSortable(mainMenuBox);
634
+
635
+ /***************************************************************************
636
+ Event handlers for editor widgets
637
+ ***************************************************************************/
638
+
639
+ //Highlight the clicked menu item and show it's submenu
640
+ var currentVisibleSubmenu = null;
641
+ $('#ws_menu_editor .ws_container').live('click', (function () {
642
+ var container = $(this);
643
+ if ( container.hasClass('ws_active') ){
644
+ return;
645
+ }
646
+
647
+ //Highlight the active item and un-highlight the previous one
648
+ container.addClass('ws_active')
649
+ container.siblings('.ws_active').removeClass('ws_active');
650
+ if ( container.hasClass('ws_menu') ){
651
+ //Show/hide the appropriate submenu
652
+ if ( currentVisibleSubmenu ){
653
+ currentVisibleSubmenu.hide();
654
+ }
655
+ currentVisibleSubmenu = $('#'+container.data('submenu_id')).show();
656
+ }
657
+ }));
658
+
659
+ //Show/hide a menu's properties
660
+ $('#ws_menu_editor .ws_edit_link').live('click', (function () {
661
+ var container = $(this).parents('.ws_container').first();
662
+ var box = container.find('.ws_editbox');
663
+
664
+ //For performance, the property editors for each menu are only created
665
+ //when the user tries to access access them for the first time.
666
+ if ( !container.data('field_editors_created') ){
667
+ buildEditboxFields(box, container.data('initial_values'));
668
+ container.data('field_editors_created', true);
669
+ }
670
+
671
+ $(this).toggleClass('ws_edit_link_expanded');
672
+ //show/hide the editbox
673
+ if ($(this).hasClass('ws_edit_link_expanded')){
674
+ box.show();
675
+ } else {
676
+ //Make sure changes are applied before the menu is collapsed
677
+ box.find('input').change();
678
+ box.hide();
679
+ }
680
+ }));
681
+
682
+ //The "Default" button : Reset to default value when clicked
683
+ $('#ws_menu_editor .ws_reset_button').live('click', (function () {
684
+ //Find the field div (it holds the default value)
685
+ var field = $(this).parent();
686
+ //Find the related input field
687
+ var input = field.find('.ws_field_value');
688
+ if ( (input.length > 0) && (field.length > 0) ) {
689
+ //Set the value to the default
690
+ if (input.attr('type') == 'checkbox'){
691
+ if ( field.data('default_value') ){
692
+ input.attr('checked', 'checked');
693
+ } else {
694
+ input.removeAttr('checked');
695
+ }
696
+ } else {
697
+ input.val(field.data('default_value'));
698
+ }
699
+ field.addClass('ws_input_default');
700
+ //Trigger the change event to ensure consistency
701
+ input.change();
702
+ }
703
+ }));
704
+
705
+ //When a field is edited, change it's appearance if it's contents don't match the default value.
706
+ function fieldValueChange(){
707
+ var input = $(this);
708
+ var field = input.parents('.ws_edit_field').first();
709
+
710
+ if ( input.attr('type') == 'checkbox' ){
711
+ var value = input.is(':checked');
712
+ } else {
713
+ var value = input.val();
714
+ }
715
+
716
+ if ( field.data('default_value') != value ) {
717
+ field.removeClass('ws_input_default');
718
+ }
719
+
720
+ var fieldName = field.data('field_name');
721
+ if ( fieldName == 'menu_title' ){
722
+ //If the changed field is the menu title, update the header
723
+ field.parents('.ws_container').first().find('.ws_item_title').html(input.val()+'&nbsp;');
724
+ } else if ( fieldName == 'custom' ){
725
+ //Show/hide the custom flag
726
+ var myContainer = field.parents('.ws_container').first();
727
+ if ( value ){
728
+ addMenuFlag(myContainer, 'custom_item');
729
+ } else {
730
+ removeMenuFlag(myContainer, 'custom_item');
731
+ }
732
+ } else if (fieldName == 'file' ){
733
+ //A menu must always have a non-empty URL. If the user deletes the current value,
734
+ //reset back to the default.
735
+ if ( value == '' ){
736
+ field.find('.ws_reset_button').click();
737
+ }
738
+ }
739
+ }
740
+ $('#ws_menu_editor .ws_field_value').live('click', fieldValueChange);
741
+ //jQuery 1.3.x can't catch 'change' events with live(),
742
+ //so we handle that by using event delegation instead.
743
+ $('#ws_menu_editor').change(function(event){
744
+ var target = $(event.target);
745
+ if ( target.is('.ws_field_value') ){
746
+ fieldValueChange.call(target, event);
747
+ }
748
+ });
749
+
750
+ //Show/hide advanced fields
751
+ $('#ws_menu_editor .ws_toggle_advanced_fields').live('click', function(){
752
+ var self = $(this);
753
+ var advancedFields = self.parents('.ws_container').first().find('.ws_advanced');
754
+
755
+ if ( advancedFields.is(':visible') ){
756
+ advancedFields.hide();
757
+ self.text(captionShowAdvanced);
758
+ } else {
759
+ advancedFields.show();
760
+ self.text(captionHideAdvanced);
761
+ }
762
+
763
+ return false;
764
+ });
765
+
766
+
767
+ //Event handlers for the capability dropdown
768
+ var capList = $('#ws_cap_selector');
769
+ var currentCapListOwner = null;
770
+ var timeoutForgetOwner = 0;
771
+
772
+ //Show/hide the capability dropdown list when the button is clicked
773
+ $('#ws_menu_editor input.ws_dropdown_button').live('click',function(event){
774
+ var list = capList;
775
+
776
+ var button = $(this);
777
+ var inputBox = button.parent().find('input.ws_field_value');
778
+
779
+ clearTimeout(timeoutForgetOwner);
780
+ timeoutForgetOwner = 0;
781
+
782
+ //If we already own the list, hide it and rescind ownership.
783
+ if ( currentCapListOwner == this ){
784
+ list.hide();
785
+
786
+ currentCapListOwner = null;
787
+ inputBox.focus();
788
+
789
+ return;
790
+ }
791
+ currentCapListOwner = this; //Got ye now!
792
+
793
+ //Move the dropdown near to the button
794
+ var inputPos = inputBox.offset();
795
+ list.css({
796
+ position: 'absolute',
797
+ left: inputPos.left,
798
+ top: inputPos.top + inputBox.outerHeight()
799
+ });
800
+
801
+ //Pre-select the current capability (will clear selection if there's no match)
802
+ list.val(inputBox.val());
803
+
804
+ list.show();
805
+ list.focus();
806
+ });
807
+
808
+ //Also show it when the user presses the down arrow in the input field
809
+ $('#ws_menu_editor .ws_edit_field-access_level input.ws_field_value').live('keyup', function(event){
810
+ if ( event.which == 40 ){
811
+ $(this).parent().find('input.ws_dropdown_button').click();
812
+ }
813
+ });
814
+
815
+ //Hide capability dropdown when it loses focus
816
+ capList.blur(function(event){
817
+ capList.hide();
818
+ /*
819
+ * Hackiness : make sure the list doesn't disappear & immediately reappear
820
+ * when the event that caused it to lose focus was the user clicking on the
821
+ * dropdown button.
822
+ */
823
+ timeoutForgetOwner = setTimeout(
824
+ (function(){
825
+ currentCapListOwner = null;
826
+ }),
827
+ 200
828
+ );
829
+ });
830
+
831
+ capList.keydown(function(event){
832
+ //Also hide it when the user presses Esc
833
+ if ( event.which == 27 ){
834
+ var inputBox = $(currentCapListOwner).parent().find('input.ws_field_value');
835
+
836
+ capList.hide();
837
+ if ( currentCapListOwner ){
838
+ $(currentCapListOwner).parent().find('input.ws_field_value').focus();
839
+ }
840
+ currentCapListOwner = null;
841
+
842
+ //Select an item & hide the list when the user presses Enter or Tab
843
+ } else if ( (event.which == 13) || (event.which == 9) ){
844
+ capList.hide();
845
+
846
+ var inputBox = $(currentCapListOwner).parent().find('input.ws_field_value');
847
+ if ( capList.val() ){
848
+ inputBox.val(capList.val());
849
+ inputBox.change();
850
+ }
851
+
852
+ inputBox.focus();
853
+ currentCapListOwner = null;
854
+
855
+ event.preventDefault();
856
+ }
857
+ });
858
+
859
+ //Eat Tab keys to prevent focus theft. Required to make the "select item on Tab" thing work.
860
+ capList.keyup(function(event){
861
+ if ( event.which == 9 ){
862
+ event.preventDefault();
863
+ }
864
+ })
865
+
866
+
867
+ //Update the input & hide the list when an option is clicked
868
+ capList.click(function(){
869
+ if ( !currentCapListOwner || !capList.val() ){
870
+ return;
871
+ }
872
+ capList.hide();
873
+
874
+ var inputBox = $(currentCapListOwner).parent().find('input.ws_field_value');
875
+ inputBox.val(capList.val()).change().focus();
876
+ currentCapListOwner = null;
877
+ });
878
+
879
+ //Highlight an option when the user mouses over it (doesn't work in IE)
880
+ capList.mousemove(function(event){
881
+ if ( !event.target ){
882
+ return;
883
+ }
884
+
885
+ var option = $(event.target);
886
+ if ( !option.attr('selected') && option.attr('value')){
887
+ option.attr('selected', 'selected');
888
+ }
889
+ });
890
+
891
+
892
+ /*************************************************************************
893
+ Menu toolbar buttons
894
+ *************************************************************************/
895
+ //Show/Hide menu
896
+ $('#ws_hide_menu').click(function () {
897
+ //Get the selected menu
898
+ var selection = $('#ws_menu_box .ws_active');
899
+ if (!selection.length) return;
900
+
901
+ //Mark the menu as hidden/visible
902
+ //selection.toggleClass('ws_hidden');
903
+ toggleMenuFlag(selection, 'hidden');
904
+
905
+ //Also mark all of it's submenus as hidden/visible
906
+ if ( menuHasFlag(selection,'hidden') ){
907
+ $('#' + selection.data('submenu_id') + ' .ws_item').each(function(){
908
+ addMenuFlag(this, 'hidden');
909
+ });
910
+ } else {
911
+ $('#' + selection.data('submenu_id') + ' .ws_item').each(function(){
912
+ removeMenuFlag(this, 'hidden');
913
+ });
914
+ }
915
+ });
916
+
917
+ //Delete menu
918
+ $('#ws_delete_menu').click(function () {
919
+ //Get the selected menu
920
+ var selection = $('#ws_menu_box .ws_active');
921
+ if (!selection.length) return;
922
+
923
+ if (confirm('Delete this menu?')){
924
+ //Delete the submenu first
925
+ $('#' + selection.data('submenu_id')).remove();
926
+ //Delete the menu
927
+ selection.remove();
928
+ }
929
+ });
930
+
931
+ //Copy menu
932
+ $('#ws_copy_menu').click(function () {
933
+ //Get the selected menu
934
+ var selection = $('#ws_menu_box .ws_active');
935
+ if (!selection.length) return;
936
+
937
+ //Store a copy of the current menu state in clipboard
938
+ menu_in_clipboard = readMenuState(selection);
939
+ });
940
+
941
+ //Cut menu
942
+ $('#ws_cut_menu').click(function () {
943
+ //Get the selected menu
944
+ var selection = $('#ws_menu_box .ws_active');
945
+ if (!selection.length) return;
946
+
947
+ //Store a copy of the current menu state in clipboard
948
+ menu_in_clipboard = readMenuState(selection);
949
+
950
+ //Remove the original menu and submenu
951
+ selection.remove();
952
+ $('#'+selection.data('submenu_id')).remove;
953
+ });
954
+
955
+ //Paste menu
956
+ $('#ws_paste_menu').click(function () {
957
+ //Check if anything has been copied/cut
958
+ if (!menu_in_clipboard) return;
959
+ //Get the selected menu
960
+ var selection = $('#ws_menu_box .ws_active');
961
+
962
+ if (selection.length > 0) {
963
+ //If a menu is selected add the pasted item after it
964
+ outputTopMenu(menu_in_clipboard, selection);
965
+ } else {
966
+ //Otherwise add the pasted item at the end
967
+ outputTopMenu(menu_in_clipboard);
968
+ };
969
+ });
970
+
971
+ //New menu
972
+ $('#ws_new_menu').click(function () {
973
+ ws_paste_count++;
974
+
975
+ //The new menu starts out rather bare
976
+ var randomId = 'custom_menu_'+randomMenuId();
977
+ var menu = {
978
+ menu_title : null,
979
+ page_title : null,
980
+ access_level : null,
981
+ file : null,
982
+ css_class : null,
983
+ icon_url : null,
984
+ hookname : null,
985
+ position : null,
986
+ custom : null,
987
+ open_in: null,
988
+ items : {}, //No items
989
+ defaults : {
990
+ menu_title : 'Custom Menu '+ws_paste_count,
991
+ page_title : '',
992
+ access_level : 'read',
993
+ file : randomId,
994
+ css_class : 'menu-top',
995
+ icon_url : 'images/generic.png',
996
+ hookname : randomId,
997
+ open_in : 'same_window',
998
+ custom: true, //Important : flag the new menu as custom, or it won't show up after saving.
999
+ position : 0,
1000
+ separator: false
1001
+ }
1002
+ };
1003
+
1004
+ //Insert the new menu
1005
+ result = outputTopMenu(menu);
1006
+
1007
+ //The menus's editbox is always open
1008
+ result.menu.find('.ws_edit_link').click();
1009
+ });
1010
+
1011
+ //New separator
1012
+ $('#ws_new_separator').click(function () {
1013
+ ws_paste_count++;
1014
+
1015
+ //The new menu starts out rather bare
1016
+ var randomId = 'separator_'+randomMenuId();
1017
+ var menu = {
1018
+ menu_title : null,
1019
+ page_title : null,
1020
+ access_level : null,
1021
+ file : null,
1022
+ css_class : null,
1023
+ icon_url : null,
1024
+ hookname : null,
1025
+ position : null,
1026
+ separator : true, //Flag as a separator
1027
+ custom : null,
1028
+ open_in : null,
1029
+ items : {}, //No items
1030
+ defaults : {
1031
+ menu_title : '',
1032
+ page_title : '',
1033
+ access_level : 'read',
1034
+ file : randomId,
1035
+ css_class : 'wp-menu-separator',
1036
+ icon_url : '',
1037
+ hookname : '',
1038
+ position : 0,
1039
+ custom: false, //Separators don't need to flagged as custom to be retained.
1040
+ open_in: 'same_window',
1041
+ separator: true
1042
+ }
1043
+ };
1044
+
1045
+ //Insert the new menu
1046
+ result = outputTopMenu(menu);
1047
+ });
1048
+
1049
+ /*************************************************************************
1050
+ Item toolbar buttons
1051
+ *************************************************************************/
1052
+ //Show/Hide item
1053
+ $('#ws_hide_item').click(function () {
1054
+ //Get the selected item
1055
+ var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
1056
+ if (!selection.length) return;
1057
+
1058
+ //Mark the item as hidden/visible
1059
+ toggleMenuFlag(selection, 'hidden');
1060
+ });
1061
+
1062
+ //Delete menu
1063
+ $('#ws_delete_item').click(function () {
1064
+ //Get the selected menu
1065
+ var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
1066
+ if (!selection.length) return;
1067
+
1068
+ if (confirm('Delete this menu item?')){
1069
+ //Delete the item
1070
+ selection.remove();
1071
+ }
1072
+ });
1073
+
1074
+ //Copy item
1075
+ $('#ws_copy_item').click(function () {
1076
+ //Get the selected item
1077
+ var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
1078
+ if (!selection.length) return;
1079
+
1080
+ //Store a copy of item state in the clipboard
1081
+ item_in_clipboard = readItemState(selection);
1082
+ });
1083
+
1084
+ //Cut item
1085
+ $('#ws_cut_item').click(function () {
1086
+ //Get the selected item
1087
+ var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
1088
+ if (!selection.length) return;
1089
+
1090
+ //Store a copy of item state in the clipboard
1091
+ item_in_clipboard = readItemState(selection);
1092
+
1093
+ //Remove the original item
1094
+ selection.remove();
1095
+ });
1096
+
1097
+ //Paste item
1098
+ $('#ws_paste_item').click(function () {
1099
+ //Check if anything has been copied/cut
1100
+ if (!item_in_clipboard) return;
1101
+ //Create a new editor widget for the copied item
1102
+ var new_item = buildMenuItem(item_in_clipboard);
1103
+
1104
+ //Get the selected menu
1105
+ var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
1106
+ if (selection.length > 0) {
1107
+ //If an item is selected add the pasted item after it
1108
+ selection.after(new_item);
1109
+ } else {
1110
+ //Otherwise add the pasted item at the end
1111
+ $('#ws_submenu_box .ws_submenu:visible').append(new_item);
1112
+ };
1113
+
1114
+ new_item.show();
1115
+ });
1116
+
1117
+ //New item
1118
+ $('#ws_new_item').click(function () {
1119
+ if ($('.ws_submenu:visible').length < 1) {
1120
+ return; //Abort if no submenu visible
1121
+ }
1122
+
1123
+ ws_paste_count++;
1124
+ var entry = {
1125
+ menu_title : null,
1126
+ page_title : null,
1127
+ access_level : null,
1128
+ file : null,
1129
+ custom : null,
1130
+ open_in : null,
1131
+ defaults : {
1132
+ menu_title : 'Custom Item ' + ws_paste_count,
1133
+ page_title : '',
1134
+ access_level : 'read',
1135
+ file : 'custom_item_'+randomMenuId(),
1136
+ is_plugin_page : false,
1137
+ custom: true,
1138
+ open_in : 'same_window'
1139
+ }
1140
+ };
1141
+
1142
+ var menu = buildMenuItem(entry);
1143
+
1144
+ //Insert the item into the box
1145
+ $('#ws_submenu_box .ws_submenu:visible').append(menu);
1146
+
1147
+ //The items's editbox is always open
1148
+ menu.find('.ws_edit_link').click();
1149
+ });
1150
+
1151
+ function compareMenus(a, b){
1152
+ function jsTrim(str){
1153
+ return str.replace(/^\s+|\s+$/g, "");
1154
+ }
1155
+
1156
+ var aTitle = jsTrim( $(a).find('.ws_item_title').text() );
1157
+ var bTitle = jsTrim( $(b).find('.ws_item_title').text() );
1158
+
1159
+ aTitle = aTitle.toLowerCase();
1160
+ bTitle = bTitle.toLowerCase();
1161
+
1162
+ return aTitle > bTitle ? 1 : -1;
1163
+ }
1164
+
1165
+ //Sort items in ascending order
1166
+ $('#ws_sort_ascending').click(function () {
1167
+ var submenu = $('#ws_submenu_box .ws_submenu:visible');
1168
+ if (submenu.length < 1) {
1169
+ return; //Abort if no submenu visible
1170
+ }
1171
+
1172
+ submenu.find('.ws_container').sort(compareMenus);
1173
+ });
1174
+
1175
+ //Sort items in descending order
1176
+ $('#ws_sort_descending').click(function () {
1177
+ var submenu = $('#ws_submenu_box .ws_submenu:visible');
1178
+ if (submenu.length < 1) {
1179
+ return; //Abort if no submenu visible
1180
+ }
1181
+
1182
+ submenu.find('.ws_container').sort((function(a, b){
1183
+ return -compareMenus(a, b);
1184
+ }));
1185
+ });
1186
+
1187
+ //==============================================
1188
+ // Main buttons
1189
+ //==============================================
1190
+
1191
+ //Save Changes - encode the current menu as JSON and save
1192
+ $('#ws_save_menu').click(function () {
1193
+ var data = encodeMenuAsJSON();
1194
+ $('#ws_data').val(data);
1195
+ $('#ws_main_form').submit();
1196
+ });
1197
+
1198
+ //Load default menu - load the default WordPress menu
1199
+ $('#ws_load_menu').click(function () {
1200
+ if (confirm('Are you sure you want to load the default WordPress menu?')){
1201
+ outputWpMenu(defaultMenu);
1202
+ }
1203
+ });
1204
+
1205
+ //Reset menu - re-load the custom menu. Discards any changes made by user.
1206
+ $('#ws_reset_menu').click(function () {
1207
+ if (confirm('Undo all changes made in the current editing session?')){
1208
+ outputWpMenu(customMenu);
1209
+ }
1210
+ });
1211
+
1212
+ //Export menu - download the current menu as a file
1213
+ $('#export_dialog').dialog({
1214
+ autoOpen: false,
1215
+ closeText: ' ',
1216
+ modal: true,
1217
+ minHeight: 100
1218
+ });
1219
+
1220
+ $('#ws_export_menu').click(function(){
1221
+ var button = $(this);
1222
+ button.attr('disabled', 'disabled');
1223
+ button.val('Exporting...');
1224
+
1225
+ $('#export_complete_notice, #download_menu_button').hide();
1226
+ $('#export_progress_notice').show();
1227
+ $('#export_dialog').dialog('open');
1228
+
1229
+ //Encode and store the menu for download
1230
+ var menu = readMenuTreeState();
1231
+ var exportData = {
1232
+ 'format' : exportFormatString,
1233
+ 'menu' : menu
1234
+ };
1235
+ exportData = $.toJSON(exportData);
1236
+
1237
+ $.post(
1238
+ adminAjaxUrl,
1239
+ {
1240
+ 'data' : exportData,
1241
+ 'action' : 'export_custom_menu',
1242
+ '_ajax_nonce' : exportMenuNonce
1243
+ },
1244
+ function(data, textStatus){
1245
+ button.val('Export');
1246
+ button.removeAttr('disabled');
1247
+
1248
+ if ( typeof data['error'] != 'undefined' ){
1249
+ $('#export_dialog').dialog('close');
1250
+ alert(data.error);
1251
+ }
1252
+
1253
+ if ( (typeof data['download_url'] != 'undefined') && data.download_url ){
1254
+ //window.location = data.download_url;
1255
+ $('#download_menu_button').attr('href', data.download_url);
1256
+ $('#export_progress_notice').hide();
1257
+ $('#export_complete_notice, #download_menu_button').show();
1258
+ }
1259
+ },
1260
+ 'json'
1261
+ );
1262
+ });
1263
+
1264
+ $('#ws_cancel_export').click(function(){
1265
+ $('#export_dialog').dialog('close');
1266
+ });
1267
+
1268
+ $('#download_menu_button').click(function(){
1269
+ $('#export_dialog').dialog('close');
1270
+ });
1271
+
1272
+ //Import menu - upload an exported menu and show it in the editor
1273
+ $('#import_dialog').dialog({
1274
+ autoOpen: false,
1275
+ closeText: ' ',
1276
+ modal: true
1277
+ });
1278
+
1279
+ $('#ws_cancel_import').click(function(){
1280
+ $('#import_dialog').dialog('close');
1281
+ });
1282
+
1283
+ $('#ws_import_menu').click(function(){
1284
+ $('#import_progress_notice, #import_progress_notice2, #import_complete_notice').hide();
1285
+ $('#import_menu_form').resetForm();
1286
+ //The "Upload" button is disabled until the user selects a file
1287
+ $('#ws_start_import').attr('disabled', 'disabled');
1288
+
1289
+ $('#import_dialog .hide-when-uploading').show();
1290
+
1291
+ $('#import_dialog').dialog('open');
1292
+ })
1293
+
1294
+ $('#import_file_selector').change(function(){
1295
+ if ( $(this).val() ){
1296
+ $('#ws_start_import').removeAttr('disabled');
1297
+ } else {
1298
+ $('#ws_start_import').attr('disabled', 'disabled');
1299
+ };
1300
+ });
1301
+
1302
+ //AJAXify the upload form
1303
+ $('#import_menu_form').ajaxForm({
1304
+ dataType : 'json',
1305
+ beforeSubmit: function(formData, $form, options) {
1306
+
1307
+ //Check if the user has selected a file
1308
+ for(var i = 0; i < formData.length; i++){
1309
+ if ( formData[i].name == 'menu' ){
1310
+ if ( (typeof formData[i]['value'] == 'undefined') || !formData[i]['value']){
1311
+ alert('Select a file first!');
1312
+ return false;
1313
+ }
1314
+ }
1315
+ }
1316
+
1317
+ $('#import_dialog .hide-when-uploading').hide();
1318
+ $('#import_progress_notice').show();
1319
+
1320
+ $('#ws_start_import').attr('disabled', 'disabled');
1321
+ },
1322
+ success: function(data){
1323
+ if ( !$('#import_dialog').dialog('isOpen') ){
1324
+ //Whoops, the user closed the dialog while the upload was in progress.
1325
+ //Discard the response silently.
1326
+ return;
1327
+ };
1328
+
1329
+ if ( typeof data['error'] != 'undefined' ){
1330
+ alert(data.error);
1331
+ //Let the user try again
1332
+ $('#import_menu_form').resetForm();
1333
+ $('#import_dialog .hide-when-uploading').show();
1334
+ }
1335
+ $('#import_progress_notice').hide();
1336
+
1337
+ if ( (typeof data['menu'] != 'undefined') && data.menu ){
1338
+ //Whee, we got back a (seemingly) valid menu. A veritable miracle!
1339
+ //Lets load it into the editor.
1340
+ $('#import_progress_notice2').show();
1341
+ outputWpMenu(data.menu);
1342
+ $('#import_progress_notice2').hide();
1343
+ //Display a success notice, then automatically close the window after a few moments
1344
+ $('#import_complete_notice').show();
1345
+ setTimeout((function(){
1346
+ //Close the import dialog
1347
+ $('#import_dialog').dialog('close');
1348
+ }), 500);
1349
+ }
1350
+
1351
+ }
1352
+ });
1353
+
1354
+ });
1355
+
1356
+ })(jQuery);
1357
+
1358
+ //==============================================
1359
+ // Screen options
1360
+ //==============================================
1361
+
1362
+ jQuery(function($){
1363
+ var screenOptions = $('#ws-ame-screen-meta-contents');
1364
+ var checkbox = screenOptions.find('#ws-hide-advanced-settings');
1365
+
1366
+ if ( hideAdvancedSettings ){
1367
+ checkbox.attr('checked', 'checked');
1368
+ } else {
1369
+ checkbox.removeAttr('checked');
1370
+ }
1371
+
1372
+ //Update editor state when settings change
1373
+ checkbox.click(function(){
1374
+ hideAdvancedSettings = $(this).attr('checked'); //Using '$(this)' instead of 'checkbox' due to jQuery bugs
1375
+ if ( hideAdvancedSettings ){
1376
+ $('#ws_menu_editor div.ws_advanced').hide();
1377
+ $('#ws_menu_editor a.ws_toggle_advanced_fields').text(captionShowAdvanced).show();
1378
+ } else {
1379
+ $('#ws_menu_editor div.ws_advanced').show();
1380
+ $('#ws_menu_editor a.ws_toggle_advanced_fields').text(captionHideAdvanced).hide();
1381
+ }
1382
+
1383
+ $.post(
1384
+ adminAjaxUrl,
1385
+ {
1386
+ 'action' : 'ws_ame_save_screen_options',
1387
+ 'hide_advanced_settings' : hideAdvancedSettings?1:0,
1388
+ '_ajax_nonce' : hideAdvancedSettingsNonce
1389
+ }
1390
+ );
1391
+ });
1392
+
1393
+ //Move our options into the screen meta panel
1394
+ $('#adv-settings').empty().append(screenOptions.show());
1395
+ });
menu-editor-core.php DELETED
@@ -1,653 +0,0 @@
1
- <?php
2
-
3
- //Load the "framework"
4
- require 'shadow_plugin_framework.php';
5
-
6
- //Load JSON functions for PHP < 5.2
7
- if (!class_exists('Services_JSON')){
8
- require 'JSON.php';
9
- }
10
-
11
- class WPMenuEditor extends MenuEd_ShadowPluginFramework {
12
-
13
- protected $default_wp_menu = null; //Holds the default WP menu for later use in the editor
14
- protected $default_wp_submenu = null; //Holds the default WP menu for later use
15
-
16
- private $blank_menu = null;
17
- private $blank_item = null;
18
-
19
- function __construct($plugin_file=''){
20
- if ( empty($plugin_file) ) $plugin_file = __FILE__;
21
-
22
- //Determine if the plugin is installed in the mu-plugins directory
23
- $this->is_mu_plugin = $this->is_in_wpmu_plugin_dir($plugin_file);
24
- //If so, we'll store the custom menu in a site-wide option
25
- if ( $this->is_mu_plugin ){
26
- $this->sitewide_options = true;
27
- }
28
-
29
- //Set some plugin-specific options
30
- $this->option_name = 'ws_menu_editor';
31
- $this->defaults = array();
32
-
33
- $this->settings_link = 'options-general.php?page=menu_editor';
34
-
35
- $this->magic_hooks = true;
36
- $this->magic_hook_priority = 99999;
37
-
38
- //Call the default constructor
39
- parent::__construct($plugin_file);
40
-
41
- //Build some template arrays
42
- $this->blank_menu = array(
43
- 'page_title' => null,
44
- 'menu_title' => null,
45
- 'access_level' => null,
46
- 'file' => null,
47
- 'css_class' => null,
48
- 'hookname' => null,
49
- 'icon_url' => null,
50
- 'position' => null,
51
- 'defaults' => null,
52
- 'separator' => null,
53
- 'custom' => null,
54
- );
55
-
56
- $this->blank_item = array(
57
- 'menu_title' => null,
58
- 'access_level' => null,
59
- 'file' => null,
60
- 'page_title' => null,
61
- 'position' => null,
62
- 'custom' => null,
63
- );
64
-
65
- }
66
-
67
- //Backwards fompatible json_decode.
68
- //We can't define this globally as that conflicts with the version created by Simple Tags.
69
- function json_decode($data, $assoc=false){
70
- $flag = $assoc?SERVICES_JSON_LOOSE_TYPE:0;
71
- $json = new Services_JSON($flag);
72
- return( $json->decode($data) );
73
- }
74
-
75
- //Backwards fompatible json_encode.
76
- //Can't define this globally as that conflicts with Simple Tags.
77
- function json_encode($data) {
78
- $json = new Services_JSON();
79
- return( $json->encode($data) );
80
- }
81
-
82
- /**
83
- * WPMenuEditor::enqueue_scripts()
84
- * Add the JS required by the editor to the page header
85
- *
86
- * @return void
87
- */
88
- function enqueue_scripts(){
89
- wp_enqueue_script('jquery');
90
- wp_enqueue_script('jquery-ui-sortable');
91
-
92
- //jQuery JSON plugin
93
- wp_enqueue_script('jquery-json', $this->plugin_dir_url.'/jquery.json-1.3.js', array('jquery'), '1.3');
94
- //Editor's scipts
95
- wp_enqueue_script('menu-editor', $this->plugin_dir_url.'/menu-editor.js', array('jquery'));
96
- }
97
-
98
- /**
99
- * WPMenuEditor::print_editor_css()
100
- * Add the editor's CSS file to the page header
101
- *
102
- * @return void
103
- */
104
- function print_editor_css(){
105
- echo '<link type="text/css" rel="stylesheet" href="', $this->plugin_dir_url, '/menu-editor.css" />',"\n";
106
- }
107
-
108
- /**
109
- * WPMenuEditor::hook_admin_menu()
110
- * Create a configuration page and load the custom menu
111
- *
112
- * @return void
113
- */
114
- function hook_admin_menu(){
115
- global $menu, $submenu;
116
-
117
- //The menu editor is only visible to users with the manage_options privilege.
118
- //Or, if the plugin is installed in mu-plugins, only to the site administrator(s).
119
- if ( !$this->is_mu_plugin || ( function_exists('is_site_admin') && is_site_admin() ) ){
120
- $page = add_options_page('Menu Editor', 'Menu Editor', 'manage_options', 'menu_editor', array(&$this, 'page_menu_editor'));
121
- //Output our JS & CSS on that page only
122
- add_action("admin_print_scripts-$page", array(&$this, 'enqueue_scripts'));
123
- add_action("admin_print_scripts-$page", array(&$this, 'print_editor_css'));
124
- }
125
-
126
- $this->default_wp_menu = $menu;
127
- $this->default_wp_submenu = $submenu;
128
-
129
- //Is there a custom menu to use?
130
- if ( !empty($this->options['custom_menu']) ){
131
- //Merge in data from the default menu
132
- $tree = $this->menu_merge($this->options['custom_menu'], $menu, $submenu);
133
- //Apply the custom menu
134
- list($menu, $submenu) = $this->tree2wp($tree);
135
- //Save for later - the editor page will need it
136
- $this->custom_menu = $tree;
137
- //Re-filter the menu (silly WP should do that itself, oh well)
138
- $this->filter_menu();
139
- }
140
- }
141
-
142
- /**
143
- * WPMenuEditor::filter_menu()
144
- * Loop over the Dashboard submenus and remove pages for which the current user does not have privs.
145
- *
146
- * @return void
147
- */
148
- function filter_menu(){
149
- global $menu, $submenu, $_wp_submenu_nopriv, $_wp_menu_nopriv;
150
-
151
- foreach ( array( 'submenu' ) as $sub_loop ) {
152
- foreach ($$sub_loop as $parent => $sub) {
153
- foreach ($sub as $index => $data) {
154
- if ( ! current_user_can($data[1]) ) {
155
- unset(${$sub_loop}[$parent][$index]);
156
- $_wp_submenu_nopriv[$parent][$data[2]] = true;
157
- }
158
- }
159
-
160
- if ( empty(${$sub_loop}[$parent]) )
161
- unset(${$sub_loop}[$parent]);
162
- }
163
- }
164
- }
165
-
166
- /**
167
- * WPMenuEditor::page_menu_editor()
168
- * Output the menu editor page
169
- *
170
- * @return void
171
- */
172
- function page_menu_editor(){
173
- global $menu, $submenu;
174
- if ( !current_user_can('manage_options') ){
175
- die("Access denied");
176
- }
177
-
178
- //Handle form submissions
179
- if (isset($_POST['data'])){
180
- check_admin_referer('menu-editor-form');
181
-
182
- //Try to decode a menu tree encoded as JSON
183
- $data = $this->json_decode($_POST['data'], true);
184
- if (!$data){
185
- $fixed = stripslashes($_POST['data']);
186
- $data = $this->json_decode( $fixed, true );
187
- }
188
-
189
- $url = remove_query_arg('noheader');
190
-
191
- if ($data){
192
- //Save the custom menu
193
- $this->options['custom_menu'] = $data;
194
- $this->save_options();
195
- //Redirect back to the editor and display the success message
196
- wp_redirect( add_query_arg('message', 1, $url) );
197
- } else {
198
- //Or redirect & display the error message
199
- wp_redirect( add_query_arg('message', 2, $url) );
200
- }
201
- die();
202
- }
203
-
204
- ?>
205
- <div class="wrap">
206
- <h2>Menu Editor</h2>
207
- <?php
208
-
209
- if ( !empty($_GET['message']) ){
210
- if ( intval($_GET['message']) == 1 ){
211
- echo '<div id="message" class="updated fade"><p><strong>Settings saved.</strong></p></div>';
212
- } elseif ( intval($_GET['message']) == 2 ) {
213
- echo '<div id="message" class="error"><p><strong>Failed to decode input! The menu wasn\'t modified.</strong></p></div>';
214
- }
215
- }
216
-
217
- //Build a tree struct. for the default menu
218
- $default_menu = $this->wp2tree($this->default_wp_menu, $this->default_wp_submenu);
219
-
220
- //Is there a custom menu?
221
- if (!empty($this->options['custom_menu'])){
222
- $custom_menu = $this->options['custom_menu'];
223
- //Merge in the current defaults
224
- $custom_menu = $this->menu_merge($custom_menu, $this->default_wp_menu, $this->default_wp_submenu);
225
- } else {
226
- //Start out with the default menu if there is no user-created one
227
- $custom_menu = $default_menu;
228
- }
229
-
230
- //Encode both menus as JSON
231
- $default_menu_js = $this->getMenuAsJS($default_menu);
232
- $custom_menu_js = $this->getMenuAsJS($custom_menu);
233
-
234
- $plugin_url = $this->plugin_dir_url;
235
- $images_url = $this->plugin_dir_url . '/images';
236
- ?>
237
- <div id='ws_menu_editor'>
238
- <div id='ws_menu_box' class='ws_main_container'>
239
- <div class='ws_toolbar'>
240
- <a id='ws_cut_menu' class='ws_button' href='javascript:void(0)' title='Cut'><img src='<?php echo $images_url; ?>/cut.png' /></a>
241
- <a id='ws_copy_menu' class='ws_button' href='javascript:void(0)' title='Copy'><img src='<?php echo $images_url; ?>/page_white_copy.png' /></a>
242
- <a id='ws_paste_menu' class='ws_button' href='javascript:void(0)' title='Paste'><img src='<?php echo $images_url; ?>/page_white_paste.png' /></a>
243
- <a id='ws_new_menu' class='ws_button' href='javascript:void(0)' title='New'><img src='<?php echo $images_url; ?>/page_white_add.png' /></a>
244
- <a id='ws_hide_menu' class='ws_button' href='javascript:void(0)' title='Show/Hide'><img src='<?php echo $images_url; ?>/plugin_disabled.png' /></a>
245
- <a id='ws_delete_menu' class='ws_button' href='javascript:void(0)' title='Delete'><img src='<?php echo $images_url; ?>/page_white_delete.png' /></a>
246
- </div>
247
- </div>
248
- <div id='ws_submenu_box' class='ws_main_container'>
249
- <div class='ws_toolbar'>
250
- <a id='ws_cut_item' class='ws_button' href='javascript:void(0)' title='Cut'><img src='<?php echo $images_url; ?>/cut.png' /></a>
251
- <a id='ws_copy_item' class='ws_button' href='javascript:void(0)' title='Copy'><img src='<?php echo $images_url; ?>/page_white_copy.png' /></a>
252
- <a id='ws_paste_item' class='ws_button' href='javascript:void(0)' title='Paste'><img src='<?php echo $images_url; ?>/page_white_paste.png' /></a>
253
- <a id='ws_new_item' class='ws_button' href='javascript:void(0)' title='New'><img src='<?php echo $images_url; ?>/page_white_add.png' /></a>
254
- <a id='ws_hide_item' class='ws_button' href='javascript:void(0)' title='Show/Hide'><img src='<?php echo $images_url; ?>/plugin_disabled.png' /></a>
255
- <a id='ws_delete_item' class='ws_button' href='javascript:void(0)' title='Delete'><img src='<?php echo $images_url; ?>/page_white_delete.png' /></a>
256
- </div>
257
- </div>
258
- </div>
259
-
260
- <form method="post" action="<?php echo admin_url('options-general.php?page=menu_editor&noheader=1'); ?>" id='ws_main_form' name='ws_main_form'>
261
- <div class="ws_main_container" style="width: 138px;">
262
- <?php wp_nonce_field('menu-editor-form'); ?>
263
- <input type="button" id='ws_save_menu' class="button-primary ws_main_button" value="Save Changes"
264
- style="margin-bottom: 20px;" />
265
- <input type="button" id='ws_load_menu' value="Load default menu" class="button ws_main_button" />
266
- <input type="button" id='ws_reset_menu' value="Reset menu" class="button ws_main_button" />
267
- <input type="hidden" name="data" id="ws_data" value="">
268
- </div>
269
- </form>
270
-
271
- </div>
272
- <script type='text/javascript'>
273
-
274
- var defaultMenu = <?php echo $default_menu_js; ?>;
275
- var customMenu = <?php echo $custom_menu_js; ?>;
276
-
277
- </script>
278
- <?php
279
- }
280
-
281
- /**
282
- * WPMenuEditor::getMenuAsJS()
283
- * Encode a menu tree as JSON
284
- *
285
- * @param array $tree
286
- * @return string
287
- */
288
- function getMenuAsJS($tree){
289
- return $this->json_encode($tree);
290
- }
291
-
292
- /**
293
- * WPMenuEditor::menu2assoc()
294
- * Convert a WP menu structure to an associative array
295
- *
296
- * @param array $item An element of the $menu array
297
- * @param integer $pos The position (index) of the menu item
298
- * @return array
299
- */
300
- function menu2assoc($item, $pos=0){
301
- $item = array(
302
- 'page_title' => $item[3],
303
- 'menu_title' => $item[0],
304
- 'access_level' => $item[1],
305
- 'file' => $item[2],
306
- 'css_class' => $item[4],
307
- 'hookname' => (isset($item[5])?$item[5]:''), //ID
308
- 'icon_url' => (isset($item[6])?$item[6]:''),
309
- 'position' => $pos,
310
- );
311
- return $item;
312
- }
313
-
314
- /**
315
- * WPMenuEditor::submenu2assoc()
316
- * Converts a WP submenu structure to an associative array
317
- *
318
- * @param array $item An element of the $submenu array
319
- * @param integer $pos The position (index) of that element
320
- * @return
321
- */
322
- function submenu2assoc($item, $pos=0){
323
- $item = array(
324
- 'menu_title' => $item[0],
325
- 'access_level' => $item[1],
326
- 'file' => $item[2],
327
- 'page_title' => (isset($item[3])?$item[3]:''),
328
- 'position' => $pos,
329
- );
330
- return $item;
331
- }
332
-
333
- /**
334
- * WPMenuEditor::build_lookups()
335
- * Populate lookup arrays with default values from $menu and $submenu. Used later to merge
336
- * a custom menu with the native WordPress menu structure somewhat gracefully.
337
- *
338
- * @param array $menu
339
- * @param array $submenu
340
- * @return array An array with two elements containing menu and submenu defaults.
341
- */
342
- function build_lookups($menu, $submenu){
343
- //Process the top menu
344
- $menu_defaults = array();
345
- foreach($menu as $pos => $item){
346
- $item = $this->menu2assoc($item, $pos);
347
- if ($item['file'] != '') { //skip separators (empty menus)
348
- $menu_defaults[$item['file']] = $item; //index by filename
349
- }
350
- }
351
-
352
- //Process the submenu
353
- $submenu_defaults = array();
354
- foreach($submenu as $parent => $items){
355
- foreach($items as $pos => $item){
356
- $item = $this->submenu2assoc($item, $pos);
357
- //save the default parent menu
358
- $item['parent'] = $parent;
359
- $submenu_defaults[$item['file']] = $item; //index by filename
360
- }
361
- }
362
-
363
- return array($menu_defaults, $submenu_defaults);
364
- }
365
-
366
- /**
367
- * WPMenuEditor::menu_merge()
368
- * Merge $menu and $submenu into the $tree. Adds/replaces defaults, inserts new items
369
- * and marks missing items as such.
370
- *
371
- * @param array $tree A menu in plugin's internal form
372
- * @param array $menu WordPress menu structure
373
- * @param array $submenu WordPress submenu structure
374
- * @return array Updated menu tree
375
- */
376
- function menu_merge($tree, $menu, $submenu){
377
- list($menu_defaults, $submenu_defaults) = $this->build_lookups($menu, $submenu);
378
-
379
- //Iterate over all menus and submenus and look up default values
380
- foreach ($tree as $topfile => &$topmenu){
381
-
382
- //Is this menu present in the default WP menu?
383
- if (isset($menu_defaults[$topfile])){
384
- //Yes, load defaults from that item
385
- $topmenu['defaults'] = $menu_defaults[$topfile];
386
- //Note that the original item was used
387
- $menu_defaults[$topfile]['used'] = true;
388
- } else {
389
- //Record the menu as missing, unless it's a menu separator
390
- if ( empty($topmenu['separator']) )
391
- $topmenu['missing'] = true;
392
- }
393
-
394
- if (is_array($topmenu['items'])) {
395
- //Iterate over submenu items
396
- foreach ($topmenu['items'] as $file => &$item){
397
- //Is this item present in the default WP menu?
398
- if (isset($submenu_defaults[$file])){
399
- //Yes, load defaults from that item
400
- $item['defaults'] = $submenu_defaults[$file];
401
- $submenu_defaults[$file]['used'] = true;
402
- } else {
403
- //Record as missing
404
- $item['missing'] = true;
405
- }
406
- }
407
- }
408
- }
409
-
410
- //If we don't unset these they will fuck up the next two loops where the same names are used.
411
- unset($topmenu);
412
- unset($item);
413
-
414
- //Note : Now we have some items marked as missing, and some items in lookup arrays
415
- //that are not marked as used. The missing items are handled elsewhere (e.g. tree2wp()),
416
- //but lets merge in the unused items now.
417
-
418
- //Find and merge unused toplevel menus
419
- foreach ($menu_defaults as $topfile => $topmenu){
420
- if ( !empty($topmenu['used']) ) continue;
421
-
422
- //Found an unused item. Build the tree entry.
423
- $entry = $this->blank_menu;
424
- $entry['defaults'] = $topmenu;
425
- $entry['items'] = array(); //prepare a place for menu items, if any.
426
- //Note that this item is unused
427
- $entry['unused'] = true;
428
- //Add the new entry to the menu tree
429
- $tree[$topfile] = $entry;
430
- }
431
- unset($topmenu);
432
-
433
- //Find and merge submenu items
434
- foreach($submenu_defaults as $file => $item){
435
- if ( !empty($item['used']) ) continue;
436
- //Found an unused item. Build an entry and attach it under the default toplevel menu.
437
- $entry = $this->blank_item;
438
- $entry['defaults'] = $item;
439
- //Note that this item is unused
440
- $entry['unused'] = true;
441
-
442
- //Check if the toplevel menu exists
443
- if (isset($tree[$item['parent']])) {
444
- //Okay, insert the item.
445
- $tree[$item['parent']]['items'][$item['file']] = $entry;
446
- } else {
447
- //Ooops? This should never happen. Some kind of inconsistency?
448
- }
449
- }
450
-
451
- //Resort the tree to ensure the found items are in the right spots
452
- $tree = $this->sort_menu_tree($tree);
453
-
454
- return $tree;
455
- }
456
-
457
- /**
458
- * WPMenuEditor::wp2tree()
459
- * Convert the WP menu structure to the internal representation. All properties set as defaults.
460
- *
461
- * @param array $menu
462
- * @param array $submenu
463
- * @return array
464
- */
465
- function wp2tree($menu, $submenu){
466
- $tree = array();
467
- $separator_count = 0;
468
- foreach ($menu as $pos => $item){
469
-
470
- $tree_item = $this->blank_menu;
471
- $tree_item['defaults'] = $this->menu2assoc($item, $pos);
472
- $tree_item['separator'] = empty($item[2]) || empty($item[0]);
473
-
474
- $item_file = $tree_item['defaults']['file'];
475
- if ( empty($item_file) ){
476
- $item_file = 'separator_'.$separator_count.'_';
477
- $separator_count++;
478
- }
479
-
480
- $tree[$item_file] = $tree_item;
481
- }
482
-
483
- //Attach all submenu items
484
- foreach($submenu as $parent=>$items){
485
- //Skip items that belong to a non-existent parent menu.
486
- //Rationale : All In One SEO Pack 1.6.10 (and possibly others) doth add such invalid submenus.
487
- if ( !isset($tree[$parent]) ) continue;
488
-
489
- foreach($items as $pos=>$item){
490
- //Add this item under the parent
491
- $tree[$parent]['items'][$item[2]] = array(
492
- 'menu_title' => null,
493
- 'access_level' => null,
494
- 'file' => null,
495
- 'page_title' => null,
496
- 'position' => null,
497
- 'defaults' => $this->submenu2assoc($item, $pos),
498
- );
499
- }
500
- }
501
-
502
- $tree = $this->sort_menu_tree($tree);
503
-
504
- return $tree;
505
- }
506
-
507
- /**
508
- * WPMenuEditor::apply_defaults()
509
- * Sets all undefined fields to the default value
510
- *
511
- * @param array $item Menu item in the plugin's internal form
512
- * @return array
513
- */
514
- function apply_defaults($item){
515
- foreach($item as $key => $value){
516
- //Is the field set?
517
- if ($value === null){
518
- //Use default, if available
519
- if (isset($item['defaults']) && isset($item['defaults'][$key])){
520
- $item[$key] = $item['defaults'][$key];
521
- }
522
- }
523
- }
524
- return $item;
525
- }
526
-
527
- /**
528
- * WPMenuEditor::compare_position()
529
- * Custom comparison function that compares menu items based on their position in the menu.
530
- *
531
- * @param array $a
532
- * @param array $b
533
- * @return int
534
- */
535
- function compare_position($a, $b){
536
- if ($a['position']!==null) {
537
- $p1 = $a['position'];
538
- } else {
539
- if ( isset($a['defaults']['position']) ){
540
- $p1 = $a['defaults']['position'];
541
- } else {
542
- $p1 = 0;
543
- }
544
- }
545
-
546
- if ($b['position']!==null) {
547
- $p2 = $b['position'];
548
- } else {
549
- if ( isset($b['defaults']['position']) ){
550
- $p2 = $b['defaults']['position'];
551
- } else {
552
- $p2 = 0;
553
- }
554
- }
555
-
556
- return $p1 - $p2;
557
- }
558
-
559
- /**
560
- * WPMenuEditor::sort_menu_tree()
561
- * Sort the menus and menu items of a given menu according to their positions
562
- *
563
- * @param array $tree A menu structure in the internal format
564
- * @return array Sorted menu in the internal format
565
- */
566
- function sort_menu_tree($tree){
567
- //Resort the tree to ensure the found items are in the right spots
568
- uasort($tree, array(&$this, 'compare_position'));
569
- //Resort all submenus as well
570
- foreach ($tree as $topfile => &$topmenu){
571
- if (!empty($topmenu['items'])){
572
- uasort($topmenu['items'], array(&$this, 'compare_position'));
573
- }
574
- }
575
-
576
- return $tree;
577
- }
578
-
579
- /**
580
- * WPMenuEditor::tree2wp()
581
- * Convert internal menu representation to the form used by WP.
582
- *
583
- * @param array $tree
584
- * @return array $menu and $submenu
585
- */
586
- function tree2wp($tree){
587
- $menu = array();
588
- $submenu = array();
589
-
590
- //Sort the menu by position
591
- uasort($tree, array(&$this, 'compare_position'));
592
-
593
- //Prepare the top menu
594
- foreach ($tree as &$topmenu){
595
-
596
- //Skip missing menus, unless they're user-created and thus might point to a non-standard file
597
- if ( !empty($topmenu['missing']) && empty($topmenu['custom']) ) continue;
598
- //Skip hidden entries
599
- if (!empty($topmenu['hidden'])) continue;
600
-
601
- //Build the WP item structure, using defaults where necessary
602
- $topmenu = $this->apply_defaults($topmenu);
603
- $menu[] = array(
604
- $topmenu['menu_title'],
605
- $topmenu['access_level'],
606
- $topmenu['file'],
607
- $topmenu['page_title'],
608
- $topmenu['css_class'],
609
- $topmenu['hookname'], //ID
610
- $topmenu['icon_url']
611
- );
612
-
613
- //Prepare the submenu of this menu
614
- if( !empty($topmenu['items']) ){
615
- $items = $topmenu['items'];
616
- //Sort by position
617
- uasort($items, array(&$this, 'compare_position'));
618
- foreach ($items as $item) {
619
-
620
- //Skip missing items, unless they're user-created
621
- if ( !empty($item['missing']) && empty($item['custom']) ) continue;
622
- //Skip hidden items
623
- if (!empty($item['hidden'])) {
624
- continue;
625
- }
626
-
627
- $item = $this->apply_defaults($item);
628
- $submenu[$topmenu['file']][] = array(
629
- $item['menu_title'],
630
- $item['access_level'],
631
- $item['file'],
632
- $item['page_title'],
633
- );
634
- }
635
- }
636
- }
637
-
638
- return array($menu, $submenu);
639
- }
640
-
641
- /**
642
- * WPMenuEditor::is_wp27()
643
- * Check if running WordPress 2.7 or later (unused)
644
- *
645
- * @return bool
646
- */
647
- function is_wp27(){
648
- global $wp_version;
649
- return function_exists('register_uninstall_hook');
650
- }
651
- } //class
652
-
653
- ?>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
menu-editor.css DELETED
@@ -1,177 +0,0 @@
1
- /* Admin Menu Editor CSS file */
2
-
3
- .ws_main_container {
4
- margin: 2px;
5
- width: 302px;
6
- min-height: 30px;
7
- float: left;
8
- display:block;
9
- border: 1px solid #cdd5d5;
10
- padding: 4px;
11
- }
12
-
13
- .ws_main_button {
14
- clear: both;
15
- display: block;
16
- margin: 4px;
17
- margin-left: auto;
18
- margin-right: auto;
19
- width: 120px;
20
- padding: 4px !important;
21
- }
22
-
23
- .ws_container {
24
- display: block;
25
- width: 290px;
26
- padding : 3px;
27
- margin: 2px;
28
- border: 1px solid #a9badb;
29
- background-color: #bdd3ff;
30
- }
31
-
32
- .ws_active {
33
- background-color : #8eb0f1 !important; /* make sure this overrides ws_menu_separator */
34
- }
35
-
36
- #ws_menu_box {
37
- }
38
-
39
- #ws_submenu_box {
40
- }
41
-
42
- .ws_menu {
43
- }
44
-
45
- .ws_menu_separator {
46
- background-color: #f0f0f0;
47
- }
48
-
49
- .ws_submenu {
50
- background-color: #f0fafa;
51
- min-height: 2em;
52
- }
53
-
54
-
55
- .ws_item_head {
56
- padding-left: 2px;
57
- padding-top: 2px;
58
- padding-bottom: 2px;
59
- cursor: default;
60
- }
61
-
62
- .ws_item_title {
63
- }
64
-
65
- .ws_edit_link {
66
- float: right;
67
- margin-right: 0px;
68
- cursor: pointer;
69
- display:block;
70
- height: 18px;
71
- width: 40px;
72
- background-image: url('images/bullet_arrow_down2.png');
73
- background-repeat: no-repeat;
74
- background-position: center;
75
- }
76
- a.ws_edit_link:hover {
77
- background-color: #ffffd0;
78
- background-image: url('images/bullet_arrow_down2.png');
79
- background-repeat: no-repeat;
80
- background-position: center;
81
- }
82
-
83
- .ws_edit_link_expanded {
84
- background-color: #ffffd0;
85
- border-bottom: none;
86
- border-color: #ffffd0;
87
- background-image: url('images/bullet_arrow_down2.png');
88
- background-repeat: no-repeat;
89
- background-position: center;
90
- }
91
-
92
- /* Menu/menu item flags */
93
- .ws_flag_container {
94
- float: right;
95
- margin-right: 4px;
96
- }
97
-
98
- .ws_flag {
99
- display: block;
100
- float: right;
101
- width: 16px;
102
- height: 16px;
103
- margin-left: 4px;
104
- background-repeat: no-repeat;
105
- }
106
-
107
- /* user-created items */
108
- .ws_custom_item_flag {
109
- background-image: url('images/page_white_add.png');
110
- }
111
-
112
- /* items not present in the default menu */
113
- .ws_missing_flag {
114
- background-image: url('images/plugin_error.png');
115
- }
116
-
117
- /* unused items - those that are in the default menu but not in the custom one */
118
- .ws_unused_flag {
119
- background-image: url('images/plugin_add.png');
120
- }
121
-
122
- /* hidden items */
123
- .ws_hidden_flag {
124
- background-image: url('images/plugin_disabled.png');
125
- }
126
-
127
- /* These classes could be used to apply different styles to items depending on their flags */
128
- .ws_missing { }
129
- .ws_custom_item { }
130
- .ws_hidden { }
131
- .ws_unused { }
132
-
133
-
134
- .ws_item { }
135
-
136
- .ws_editbox {
137
- display: block;
138
- background-color: #ffffd0;
139
- padding: 4px;
140
- }
141
-
142
- /* the reset-to-default button */
143
- .ws_reset_button {
144
- font-size: smaller;
145
- margin : 1px;
146
- cursor: pointer;
147
- }
148
-
149
- .ws_editbox input[type="text"] {
150
- width: 220px;
151
- }
152
-
153
- .ws_input_default {
154
- color: gray;
155
- }
156
-
157
- .ws_edit_field {
158
- margin-bottom: 8px;
159
- }
160
-
161
- .ws_toolbar {
162
- display: block;
163
- width: 290px;
164
- height: 32px;
165
- }
166
-
167
- .ws_button {
168
- display: block;
169
- margin: 2px;
170
- padding: 4px;
171
- border: 1px solid #c0c0e0;
172
- float: left;
173
- }
174
-
175
- a.ws_button:hover {
176
- background-color: #d0e0ff;
177
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
menu-editor.js DELETED
@@ -1,690 +0,0 @@
1
- //(c) W-Shadow
2
-
3
- function escapeJS (s) {
4
- s = s + '';
5
- return s.replace(/&/g,'&amp;').replace(/>/g,'&gt;').replace(/</g,'&lt;').
6
- replace(/"/g,'&quot;').replace(/'/g,"&#39;").replace(/\\/g,'&#92;');
7
- };
8
-
9
- (function ($){
10
-
11
- function outputWpMenu(menu){
12
- //Remove the current menu data
13
- $('.ws_container').remove();
14
- $('.ws_submenu').remove();
15
-
16
- //Display the new menu
17
- var i = 0;
18
- for (var filename in menu){
19
- outputTopMenu(menu[filename], filename, i);
20
- i++;
21
- }
22
-
23
- //Make the submenus sortable
24
- $('.ws_submenu').sortable({
25
- items: '> .ws_container',
26
- cursor: 'move',
27
- dropOnEmpty: true,
28
- });
29
-
30
- //Highlight the clicked menu item and show it's submenu
31
- $('.ws_item_head').click(function () {
32
- var p = $(this).parent();
33
- //Highlight the active item
34
- p.siblings().removeClass('ws_active');
35
- p.addClass('ws_active');
36
- //Show the appropriate submenu
37
- if (p.hasClass('ws_menu')) {
38
- $('.ws_submenu:visible').hide();
39
- $('#'+p.attr('submenu_id')).show();
40
- }
41
- });
42
-
43
- //Expand/collapse a menu item
44
- $('.ws_edit_link').click(function () {
45
- var box = $(this).parent().parent().find('.ws_editbox');
46
- $(this).toggleClass('ws_edit_link_expanded');
47
- //show/hide the editbox
48
- if ($(this).hasClass('ws_edit_link_expanded')){
49
- box.show();
50
- } else {
51
- //Make sure changes are applied before the menu is collapsed
52
- box.find('input').change();
53
- box.hide();
54
- }
55
- });
56
-
57
- //The "Default" button : Reset to default value when clicked
58
- $('.ws_reset_button').click(function () {
59
- //Find the related input field
60
- var field = $(this).siblings('input');
61
- if (field.length > 0) {
62
- //Set the value to the default
63
- field.val(field.attr('default'));
64
- field.addClass('ws_input_default');
65
- //Trigget the change event to ensure consistency
66
- field.change();
67
- }
68
- });
69
-
70
- //When a field is edited, change it's appearance if it's contents don't match the default value.
71
- $('.ws_edit_field input[type="text"]').change(function () {
72
- if ( $(this).attr('default') != $(this).val() ) {
73
- $(this).removeClass('ws_input_default');
74
- }
75
-
76
- //If the changed field is the menu title, update the header
77
- if ( $(this).parent().attr('field_name')=='menu_title' ){
78
- $(this).parent().parent().parent().find('.ws_item_title').html($(this).val()+'&nbsp;');
79
- }
80
- });
81
-
82
- //When the "Custom" checkbox is clicked, add/remove the ws_custom_item class to
83
- //the menu (or menu item) in question.
84
- $('.ws_custom_toggle').change(function(){
85
- //Find the container
86
- var my_container = $(this).parents('.ws_container:first');
87
- if ( $(this).is(':checked') ){
88
- addMenuFlag(my_container, 'custom_item');
89
- } else {
90
- removeMenuFlag(my_container, 'custom_item');
91
- }
92
- });
93
- }
94
-
95
- function outputTopMenu(menu, filename, ind){
96
- id = 'topmenu-'+ind;
97
- submenu_id = 'submenu-'+ind;
98
-
99
- var subclass = '';
100
- if ( menu.separator ) {
101
- subclass = subclass + ' ws_menu_separator';
102
- }
103
-
104
- //Create the menu HTML
105
- var s = '<div id="'+id+'" class="ws_container ws_menu '+subclass+'" submenu_id="'+submenu_id+'">'+
106
- '<div class="ws_item_head">'+
107
- '<a class="ws_edit_link"> </a>'+
108
- '<div class="ws_flag_container"> </div>'+
109
- '<span class="ws_item_title">'+
110
- ((menu.menu_title!=null)?menu.menu_title:menu.defaults.menu_title)+
111
- '&nbsp;</span>'+
112
- '</div>'+
113
- '<div class="ws_editbox" style="display: none;">'+buildEditboxFields(menu)+'</div>'+
114
- '</div>';
115
-
116
- var menu_obj = $(s).appendTo('#ws_menu_box');
117
-
118
- //Apply flags based on the item's state
119
- if (menu.missing && !menu.custom) {
120
- addMenuFlag(menu_obj, 'missing');
121
- }
122
- if (menu.hidden) {
123
- addMenuFlag(menu_obj, 'hidden');
124
- }
125
- if (menu.unused) {
126
- addMenuFlag(menu_obj, 'unused');
127
- }
128
- if (menu.custom) {
129
- addMenuFlag(menu_obj, 'custom_item');
130
- }
131
-
132
- //Create a container for menu items, even if there are none
133
- $('#ws_submenu_box').append('<div class="ws_submenu" id="'+submenu_id+'" style="display:none;"></div>');
134
-
135
- //Only show menus that have items.
136
- //Skip arrays (with a length) because filled menus are encoded as custom objects ().
137
- if (menu.items && (typeof menu.items != 'Array')){
138
- var i = 0;
139
- for (var item_file in menu.items){
140
- outputMenuEntry(menu.items[item_file], i, submenu_id);
141
- i++;
142
- }
143
- }
144
- }
145
-
146
- function outputMenuEntry(entry, ind, parent){
147
- if (!entry.defaults) return;
148
-
149
- var item = $(
150
- '<div class="ws_container ws_item">'+
151
- '<div class="ws_item_head">'+
152
- '<a class="ws_edit_link"> </a>'+
153
- '<div class="ws_flag_container"> </div>'+
154
- '<span class="ws_item_title">'+
155
- ((entry.menu_title!=null)?entry.menu_title:entry.defaults.menu_title)+
156
- '&nbsp;</span>'+
157
- '</div>'+
158
- '<div class="ws_editbox" style="display:none;">'+buildEditboxFields(entry)+'</div>'+
159
- '<div>'
160
- ).appendTo('#'+parent)
161
-
162
- //Apply flags based on the item's state
163
- if (entry.missing && !entry.custom) {
164
- addMenuFlag(item, 'missing');
165
- }
166
- if (entry.hidden) {
167
- addMenuFlag(item, 'hidden');
168
- }
169
- if (entry.unused) {
170
- addMenuFlag(item, 'unused');
171
- }
172
- if (entry.custom) {
173
- addMenuFlag(item, 'custom_item');
174
- }
175
- }
176
-
177
- function buildEditboxField(entry, field_name, field_caption){
178
- if (entry[field_name]===undefined) {
179
- return ''; //skip fields this entry doesn't have
180
- }
181
-
182
- return '<div class="ws_edit_field" field_name="'+field_name+'">' + (field_caption) + '<br />' +
183
- '<input type="text" value="'+escapeJS((entry[field_name]!=null)?entry[field_name]:entry.defaults[field_name])+
184
- '" default=\''+escapeJS(entry.defaults[field_name])+'\''+
185
- ' class="'+((entry[field_name]==null)?'ws_input_default':'')+'">'+
186
- '<span class="ws_reset_button">[default]</span></div>';
187
- }
188
-
189
- function buildEditboxFields(entry){
190
- var fields = {
191
- 'menu_title' : "Menu title",
192
- 'page_title' : "Page title",
193
- 'access_level' : 'Access level',
194
- 'file' : 'File',
195
- 'css_class' : 'CSS class',
196
- 'hookname' : 'CSS ID',
197
- 'icon_url' : 'Icon URL'
198
- };
199
- var s = '';
200
-
201
- for (var field_name in fields){
202
- s = s + buildEditboxField(entry, field_name, fields[field_name]);
203
- }
204
-
205
- //Add the "Custom item" checkbox
206
- var is_custom = false;
207
- if ( typeof(entry['custom']) != 'undefined' ){
208
- is_custom = entry['custom'];
209
- }
210
- s = s +
211
- '<div class="ws_edit_field">'+
212
- '<label title="Custom items are visible even if they\'re not present in the default WordPress menu">'+
213
- '<input type="checkbox" class="ws_custom_toggle"'+
214
- (is_custom?' checked="checked"':'')+ '> Custom</label>'+
215
- '</div>';
216
-
217
- return s;
218
- }
219
-
220
- //Encode the current menu structure as JSON
221
- function encodeMenuAsJSON(){
222
- var data = {};
223
- var separator_count = 0;
224
- var menu_position = 0;
225
-
226
- //Iterate over all menus
227
- $('#ws_menu_box .ws_menu').each(function(i) {
228
-
229
- var menu_obj = {};
230
- menu_obj.defaults = {};
231
-
232
- menu_position++;
233
- menu_obj.position = menu_position;
234
- menu_obj.defaults.position = menu_position; //the real default value will later overwrite this
235
-
236
- var filename = $(this).find('.ws_edit_field[field_name="file"] input').val();
237
- //Check if this is a separator
238
- if ( $(this).hasClass('ws_menu_separator') ){
239
- menu_obj.separator = true;
240
- if ( filename=='' ) {
241
- filename = 'separator_'+separator_count+'_';
242
- }
243
- separator_count++;
244
- }
245
-
246
- //Iterate over all fields of the menu
247
- $(this).find('.ws_edit_field').each(function() {
248
- //Get the name of this field
249
- field_name = $(this).attr('field_name');
250
- //Skip if unnamed
251
- if (!field_name) return true;
252
-
253
- input_box = $(this).find('input');
254
- //Save null if default used, custom value otherwise
255
- if (input_box.hasClass('ws_input_default')){
256
- menu_obj[field_name] = null;
257
- } else {
258
- menu_obj[field_name] = input_box.val();
259
- }
260
- menu_obj.defaults[field_name]=input_box.attr('default');
261
-
262
- });
263
- //Check if the menu is hidden
264
- menu_obj.hidden = $(this).hasClass('ws_hidden');
265
- //Check if this is a custom menu
266
- menu_obj.custom = $(this).hasClass('ws_custom_item');
267
-
268
- menu_obj.items = {};
269
-
270
- var item_position = 0;
271
-
272
- //Iterate over the menu's items, if any
273
- $('#'+$(this).attr('submenu_id')).find('.ws_item').each(function (i) {
274
- var filename = $(this).find('.ws_edit_field[field_name="file"] input').val();
275
-
276
- var item = {};
277
- item.defaults = {};
278
-
279
- //Save the position data (probably not all that useful)
280
- item_position++;
281
- item.position = item_position;
282
- item.defaults.position = item_position;
283
-
284
- //Iterate over all fields of the item
285
- $(this).find('.ws_edit_field').each(function() {
286
- //Get the name of this field
287
- field_name = $(this).attr('field_name');
288
- //Skip if unnamed
289
- if (!field_name) return true;
290
-
291
- input_box = $(this).find('input');
292
- //Save null if default used, custom value otherwise
293
- if (input_box.hasClass('ws_input_default')){
294
- item[field_name] = null;
295
- } else {
296
- item[field_name] = input_box.val();
297
- }
298
- item.defaults[field_name]=input_box.attr('default');
299
-
300
- });
301
- //Check if the item is hidden
302
- if ($(this).hasClass('ws_hidden')){
303
- item.hidden = true;
304
- }
305
- //Check if this is a custom item
306
- item.custom = $(this).hasClass('ws_custom_item');
307
-
308
- //Save the item in the parent menu
309
- menu_obj.items[filename] = item;
310
- });
311
- //*/
312
-
313
- //Attach the menu to the main struct
314
- data[filename] = menu_obj;
315
-
316
- });
317
-
318
- return $.toJSON(data);
319
- }
320
-
321
- var item_flags = {
322
- 'custom_item' : 'This is a custom menu item',
323
- 'unused' : 'This item was automatically (re)inserted into your custom menu because it is present in the default WordPress menu',
324
- 'missing' : 'This item is not present in the default WordPress menu. Tick the &quot;Custom&quot; checkbox if you want it to be visible anyway.',
325
- 'hidden' : 'This item is hidden'
326
- }
327
-
328
- //These function manipulate the menu flags (e.g. hidden, custom, etc)
329
- function addMenuFlag(item, flag){
330
- item = $(item);
331
-
332
- var item_class = 'ws_' + flag;
333
- var img_class = 'ws_' + flag + '_flag';
334
-
335
- item.addClass(item_class);
336
- //Add the flag image
337
- var flag_container = item.find('.ws_flag_container');
338
- if ( flag_container.find('.' + img_class).length == 0 ){
339
- flag_container.append('<div class="ws_flag '+img_class+'" title="'+item_flags[flag]+'"></div>');
340
- }
341
- }
342
-
343
- function removeMenuFlag(item, flag){
344
- item = $(item);
345
- var item_class = 'ws_' + flag;
346
- var img_class = 'ws_' + flag + '_flag';
347
-
348
- item.removeClass('ws_' + flag);
349
- item.find('.' + img_class).remove();
350
- }
351
-
352
- function toggleMenuFlag(item, flag){
353
- if (menuHasFlag(item, flag)){
354
- removeMenuFlag(item, flag);
355
- } else {
356
- addMenuFlag(item, flag);
357
- }
358
- }
359
-
360
- function menuHasFlag(item, flag){
361
- return $(item).hasClass('ws_'+flag);
362
- }
363
-
364
- function clearMenuFlags(item){
365
- item = $(item);
366
- item.find('.ws_flag').remove();
367
- for(var flag in item_flags){
368
- item.removeClass('ws_'+flag);
369
- }
370
- }
371
-
372
- //Cut & paste stuff
373
- var menu_in_clipboard = null;
374
- var submenu_in_clipboard = null;
375
- var item_in_clipboard = null;
376
- var ws_paste_count = 0;
377
-
378
- $(document).ready(function(){
379
-
380
- //Show the default menu
381
- outputWpMenu(customMenu);
382
-
383
- //Make the top menu box sortable (we only need to do this once)
384
- $('#ws_menu_box').sortable({
385
- items: '> .ws_container',
386
- cursor: 'move',
387
- dropOnEmpty: true,
388
- });
389
-
390
- //===== Toolbar buttons =======
391
- //Show/Hide menu
392
- $('#ws_hide_menu').click(function () {
393
- //Get the selected menu
394
- var selection = $('#ws_menu_box .ws_active');
395
- if (!selection.length) return;
396
-
397
- //Mark the menu as hidden/visible
398
- //selection.toggleClass('ws_hidden');
399
- toggleMenuFlag(selection, 'hidden');
400
-
401
- //Also mark all of it's submenus as hidden/visible
402
- if ( menuHasFlag(selection,'hidden') ){
403
- $('#' + selection.attr('submenu_id') + ' .ws_item').each(function(){
404
- addMenuFlag(this, 'hidden');
405
- });
406
- } else {
407
- $('#' + selection.attr('submenu_id') + ' .ws_item').each(function(){
408
- removeMenuFlag(this, 'hidden');
409
- });
410
- }
411
- });
412
-
413
- //Delete menu
414
- $('#ws_delete_menu').click(function () {
415
- //Get the selected menu
416
- var selection = $('#ws_menu_box .ws_active');
417
- if (!selection.length) return;
418
-
419
- if (confirm('Are you sure you want to delete this menu?')){
420
- //Delete the submenu first
421
- $('#' + selection.attr('submenu_id')).remove();
422
- //Delete the menu
423
- selection.remove();
424
- }
425
- });
426
-
427
- //Copy menu
428
- $('#ws_copy_menu').click(function () {
429
- //Get the selected menu
430
- var selection = $('#ws_menu_box .ws_active');
431
- if (!selection.length) return;
432
-
433
- //Store a copy in clipboard
434
- menu_in_clipboard = selection.clone(true); //just like that
435
- menu_in_clipboard.removeClass('ws_active');
436
- submenu_in_clipboard = $('#'+selection.attr('submenu_id')).clone(true);
437
- });
438
-
439
- //Cut menu
440
- $('#ws_cut_menu').click(function () {
441
- //Get the selected menu
442
- var selection = $('#ws_menu_box .ws_active');
443
- if (!selection.length) return;
444
-
445
- //Store a copy of both menu and it's submenu in clipboard
446
- menu_in_clipboard = selection.removeClass('ws_active').clone(true);
447
- menu_in_clipboard.removeClass('ws_active');
448
- submenu_in_clipboard = $('#'+selection.attr('submenu_id')).clone(true);
449
- //Remove the original menu and submenu
450
- selection.remove();
451
- $('#'+selection.attr('submenu_id')).remove;
452
- });
453
-
454
- //Paste menu
455
- $('#ws_paste_menu').click(function () {
456
- //Check if anything has been copied/cut
457
- if (!menu_in_clipboard) return;
458
- //Get the selected menu
459
- var selection = $('#ws_menu_box .ws_active');
460
-
461
- ws_paste_count++;
462
-
463
- //Clone new objects from the virtual clipboard
464
- var new_menu = menu_in_clipboard.clone(true);
465
- var new_submenu = submenu_in_clipboard.clone(true);
466
- //Close submenu editboxes
467
- new_submenu.find('.ws_editbox').hide();
468
-
469
- //The cloned menu must have a unique file name, unless it's a separator
470
- if (!new_menu.hasClass('ws_menu_separator')) {
471
- new_menu.find('.ws_edit_field[field_name="file"] input').val('custom_menu_'+ws_paste_count);
472
- }
473
-
474
- //The cloned submenu needs a unique ID (could be improved)
475
- new_submenu.attr('id', 'ws-pasted-obj-'+ws_paste_count);
476
- new_menu.attr('submenu_id', 'ws-pasted-obj-'+ws_paste_count);
477
-
478
- //Make the new submenu sortable
479
- new_submenu.sortable({
480
- items: '> .ws_container',
481
- cursor: 'move',
482
- dropOnEmpty: true,
483
- });
484
-
485
- if (selection.length > 0) {
486
- //If a menu is selected add the pasted item after it
487
- selection.after(new_menu);
488
- } else {
489
- //Otherwise add the pasted item at the end
490
- $('#ws_menu_box').append(new_menu);
491
- };
492
-
493
- //Insert the submenu in the box, too
494
- $('#ws_submenu_box').append(new_submenu);
495
-
496
- new_menu.show();
497
- new_submenu.hide();
498
- });
499
-
500
- //New menu
501
- $('#ws_new_menu').click(function () {
502
- ws_paste_count++;
503
-
504
- //This is a hack.
505
- //Clone another menu to use as a template
506
- var menu = $('#ws_menu_box .ws_menu:first').clone(true);
507
- //Also clone a submenu
508
- var submenu = $('#' + menu.attr('submenu_id')).clone(true);
509
- //Assign a new ID
510
- submenu.attr('id', 'ws-new-submenu-'+ws_paste_count);
511
- menu.attr('submenu_id', 'ws-new-submenu-'+ws_paste_count);
512
- //Remove all items from the submenu
513
- submenu.empty();
514
- //Make the submenu sortable
515
- submenu.sortable({
516
- items: '> .ws_container',
517
- cursor: 'move',
518
- dropOnEmpty: true,
519
- });
520
-
521
- //Clean up the menu's flags & classes
522
- menu.attr('class','ws_container ws_menu');
523
- clearMenuFlags(menu);
524
- addMenuFlag(menu, 'custom_item');
525
-
526
- //Check the "Custom" checkbox
527
- menu.find('.ws_custom_toggle').attr('checked', 'checked');
528
-
529
- var temp_id = 'custom_menu_'+ws_paste_count;
530
- //Assign a stub title
531
- menu.find('.ws_item_title').text('Custom Menu '+ws_paste_count);
532
- //All fields start out set to defaults
533
- menu.find('input').attr('default','').addClass('ws_input_default');
534
- //Set all fields
535
- menu.find('.ws_edit_field[field_name="page_title"] input').val('').attr('default','');
536
- menu.find('.ws_edit_field[field_name="menu_title"] input').val('Custom Menu '+ws_paste_count).attr('default','Custom Menu '+ws_paste_count);
537
- menu.find('.ws_edit_field[field_name="access_level"] input').val('read').attr('default','read');
538
- menu.find('.ws_edit_field[field_name="file"] input').val(temp_id).attr('default',temp_id);
539
- menu.find('.ws_edit_field[field_name="css_class"] input').val('menu-top').attr('default','menu-top');
540
- menu.find('.ws_edit_field[field_name="icon_url"] input').val('images/generic.png').attr('default','images/generic.png');
541
- menu.find('.ws_edit_field[field_name="hookname"] input').val(temp_id).attr('default',temp_id);
542
-
543
- //The menus's editbox is always open
544
- menu.find('.ws_editbox').show();
545
- //Make sure the edit link is in the right state, too
546
- menu.find('.ws_edit_link').addClass('ws_edit_link_expanded');
547
-
548
- //Finally, insert the menu into the box
549
- $('#ws_menu_box').append(menu);
550
- //And insert the submenu
551
- $('#ws_submenu_box').append(submenu);
552
- });
553
-
554
- //===== Item toolbar buttons =======
555
- //Show/Hide item
556
- $('#ws_hide_item').click(function () {
557
- //Get the selected item
558
- var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
559
- if (!selection.length) return;
560
-
561
- //Mark the item as hidden
562
- toggleMenuFlag(selection, 'hidden');
563
- });
564
-
565
- //Delete menu
566
- $('#ws_delete_item').click(function () {
567
- //Get the selected menu
568
- var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
569
- if (!selection.length) return;
570
-
571
- if (confirm('Are you sure you want to delete this menu item?')){
572
- //Delete the item
573
- selection.remove();
574
- }
575
- });
576
-
577
- //Copy item
578
- $('#ws_copy_item').click(function () {
579
- //Get the selected item
580
- var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
581
- if (!selection.length) return;
582
-
583
- //Store a copy in clipboard
584
- item_in_clipboard = selection.clone(true); //just like that
585
- item_in_clipboard.removeClass('ws_active');
586
- });
587
-
588
- //Cut item
589
- $('#ws_cut_item').click(function () {
590
- //Get the selected item
591
- var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
592
- if (!selection.length) return;
593
-
594
- //Store a the item in clipboard
595
- item_in_clipboard = selection.clone(true);
596
- item_in_clipboard.removeClass('ws_active');
597
- //Remove the original item
598
- selection.remove();
599
- });
600
-
601
- //Paste item
602
- $('#ws_paste_item').click(function () {
603
- //Check if anything has been copied/cut
604
- if (!item_in_clipboard) return;
605
- //Get the selected menu
606
- var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
607
-
608
- ws_paste_count++;
609
-
610
- //Clone a new object from the virtual clipboard
611
- var new_item = item_in_clipboard.clone(true);
612
- //The item's editbox is always closed
613
- new_item.find('.ws_editbox').hide();
614
-
615
- if (selection.length > 0) {
616
- //If an item is selected add the pasted item after it
617
- selection.after(new_item);
618
- } else {
619
- //Otherwise add the pasted item at the end
620
- $('#ws_submenu_box .ws_submenu:visible').append(new_item);
621
- };
622
-
623
- new_item.show();
624
- });
625
-
626
- //New item
627
- $('#ws_new_item').click(function () {
628
- if ($('.ws_submenu:visible').length<1) return; //abort if no submenu visible
629
-
630
- ws_paste_count++;
631
-
632
- //Clone another item to use as a template (hack)
633
- var menu = $('#ws_submenu_box .ws_item:first').clone(true);
634
-
635
- //Cleanup the items's flags & classes
636
- menu.attr('class','ws_container ws_item');
637
- clearMenuFlags(menu);
638
- addMenuFlag(menu, 'custom_item');
639
-
640
- //Check the "Custom" checkbox
641
- menu.find('.ws_custom_toggle').attr('checked', 'checked');
642
-
643
- var temp_id = 'custom_item_'+ws_paste_count;
644
- //Assign a stub title
645
- menu.find('.ws_item_title').text('Custom Item '+ws_paste_count);
646
- //All fields start out set to defaults
647
- menu.find('input').attr('default','').addClass('ws_input_default');
648
- //Set all fields
649
- menu.find('.ws_edit_field[field_name="page_title"] input').val('').attr('default','');
650
- menu.find('.ws_edit_field[field_name="menu_title"] input').val('Custom Item '+ws_paste_count).attr('default','Custom Item '+ws_paste_count);
651
- menu.find('.ws_edit_field[field_name="access_level"] input').val('read').attr('default','read');
652
- menu.find('.ws_edit_field[field_name="file"] input').val(temp_id).attr('default',temp_id);
653
-
654
- //The items's editbox is always open
655
- menu.find('.ws_editbox').show();
656
- //Make sure the edit link is in the right state, too
657
- menu.find('.ws_edit_link').addClass('ws_edit_link_expanded');
658
-
659
- //Finally, insert the item into the box
660
- $('.ws_submenu:visible').append(menu);
661
- });
662
-
663
- //==============================================
664
- // Main buttons
665
- //==============================================
666
-
667
- //Save Changes - encode the current menu as JSON and save
668
- $('#ws_save_menu').click(function () {
669
- var data = encodeMenuAsJSON();
670
- $('#ws_data').val(data);
671
- $('#ws_main_form').submit();
672
- });
673
-
674
- //Load default menu - load the default WordPress menu
675
- $('#ws_load_menu').click(function () {
676
- if (confirm('Are you sure you want to load the default WordPress menu into the editor?')){
677
- outputWpMenu(defaultMenu);
678
- }
679
- });
680
-
681
- //Reset menu - re-load the custom menu = discards any changes made by user
682
- $('#ws_reset_menu').click(function () {
683
- if (confirm('Are you sure you want to reset the custom menu? Any unsaved changes will be lost!')){
684
- outputWpMenu(customMenu);
685
- }
686
- });
687
-
688
- });
689
-
690
- })(jQuery);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
menu-editor.php CHANGED
@@ -3,22 +3,17 @@
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: 0.2
7
  Author: Janis Elsts
8
  Author URI: http://w-shadow.com/blog/
9
  */
10
 
11
- /*
12
- Created by Janis Elsts (email : whiteshadow@w-shadow.com)
13
- It's LGPL.
14
- */
15
-
16
  //Are we running in the Dashboard?
17
  if ( is_admin() ) {
18
 
19
  //Load the plugin
20
- require 'menu-editor-core.php';
21
- $wp_menu_editor = new WPMenuEditor(__FILE__);
22
 
23
  }//is_admin()
24
  ?>
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.0
7
  Author: Janis Elsts
8
  Author URI: http://w-shadow.com/blog/
9
  */
10
 
 
 
 
 
 
11
  //Are we running in the Dashboard?
12
  if ( is_admin() ) {
13
 
14
  //Load the plugin
15
+ require 'includes/menu-editor-core.php';
16
+ $wp_menu_editor = new WPMenuEditor(__FILE__, 'ws_menu_editor');
17
 
18
  }//is_admin()
19
  ?>
readme.txt CHANGED
@@ -1,10 +1,10 @@
1
  === Admin Menu Editor ===
2
  Contributors: whiteshadow
3
- Donate link: http://w-shadow.com/
4
  Tags: admin, dashboard, menu, security, wpmu
5
- Requires at least: 2.7.0
6
- Tested up to: 2.9.2
7
- Stable tag: 0.2
8
 
9
  Lets you directly edit the WordPress admin menu. You can re-order, hide or rename existing menus, add custom menus and more.
10
 
@@ -13,44 +13,72 @@ Admin Menu Editor lets you manually edit the Dashboard menu. You can reorder the
13
 
14
  **Features**
15
 
 
16
  * Sort menu items via drag & drop.
17
  * Move a menu item to a different submenu via cut & paste.
18
- * Edit any existing menu - change the title, access rights, menu icon and so on. Note that you can't lower the required access rights, but you can change them to be more restrictive.
19
  * Hide/show any menu or menu item. A hidden menu is invisible to all users, including administrators.
20
- * Create custom menus that point to any part of the Dashboard. For example, you could create a new menu leading directly to the "Pending comments" page.
 
 
21
 
22
  [Suggest new features and improvements here](http://feedback.w-shadow.com/forums/58572-admin-menu-editor)
23
 
24
  **Known Issues**
25
 
26
  * If you delete any of the default menus they will reappear after saving. This is by design. To get rid of a menu for good, either hide it or set it's access rights to a higher level.
27
- * Custom menus will only show up in the final menu if the "Custom" box is checked. If some of your menu items are only visible in the editor but not the Dashboard menu itself, this is probably the reason.
28
- * A plugin's menu that is moved to a different submenu will not work unless you also include the parent file in the "File" field. For example, if the plugin's page was originally in the "Settings" menu and had the "File" field set to "my_plugin", you'll need to change it to "options-general.php?page=my_plugin" and tick the "Custom" checkbox after moving it to a different menu.
 
29
 
30
  == Installation ==
31
 
32
  **Normal installation**
33
 
34
- 1. Download the admin-menu-editor.zip file to your local machine.
35
  1. Unzip the file.
36
  1. Upload the `admin-menu-editor` directory to your `/wp-content/plugins/` directory.
37
  1. Activate the plugin through the 'Plugins' menu in WordPress.
38
 
39
  That's it. You can access the the menu editor by going to *Settings -> Menu Editor*. The plugin will automatically load your current menu configuration the first time you run it.
40
 
41
- **WPMU/Multi-user installation**
42
 
43
- If you have a WPMU site, you can also install Admin Menu Editor as a global plugin. This will enable you to edit the Dashboard menu for all blogs and users at once.
44
 
45
- 1. Download the admin-menu-editor.zip file to your local machine.
46
  1. Unzip the file.
47
- 1. Upload the `admin-menu-editor` directory to your `/wp-content/mu-plugins/` directory.
48
- 1. Move the `admin-menu-editor-mu.php` file from `admin-menu-editor` to `/wp-content/mu-plugins/`.
 
 
 
 
 
 
 
 
49
 
50
- *Note : It is currently not possible to install this plugin both as a normal plugin and as a mu-plugin on the same site.*
51
 
52
  == Changelog ==
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  = 0.2 =
55
  * Provisional WPMU support.
56
  * Missing and unused menu items now get different icons in the menu editor.
1
  === Admin Menu Editor ===
2
  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.0
6
+ Tested up to: 3.0
7
+ Stable tag: 1.0
8
 
9
  Lets you directly edit the WordPress admin menu. You can re-order, hide or rename existing menus, add custom menus and more.
10
 
13
 
14
  **Features**
15
 
16
+ * Edit any existing menu - change the title, access rights, menu icon and so on.
17
  * Sort menu items via drag & drop.
18
  * Move a menu item to a different submenu via cut & paste.
 
19
  * Hide/show any menu or menu item. A hidden menu is invisible to all users, including administrators.
20
+ * Create custom menus that point to any part of the Dashboard or an external URL.
21
+
22
+ The [Pro version](http://wpplugins.com/plugin/146/admin-menu-editor-pro) of the plugin lets you also import/export menu configurations, make menu items open in a new window, and use [shortcodes](http://wpplugins.com/plugin/146/admin-menu-editor-pro?view=notes) in the Dashboard menu.
23
 
24
  [Suggest new features and improvements here](http://feedback.w-shadow.com/forums/58572-admin-menu-editor)
25
 
26
  **Known Issues**
27
 
28
  * If you delete any of the default menus they will reappear after saving. This is by design. To get rid of a menu for good, either hide it or set it's access rights to a higher level.
29
+ * If one of your menu items is only visible in the editor but not the Dashboard menu itself, make sure its "Custom" checkbox is ticked. The plugin will usually do this for you when you create a new menu.
30
+ * You can't lower a menu's required access rights, but you can change them to be more restrictive.
31
+
32
 
33
  == Installation ==
34
 
35
  **Normal installation**
36
 
37
+ 1. Download the admin-menu-editor.zip file to your computer.
38
  1. Unzip the file.
39
  1. Upload the `admin-menu-editor` directory to your `/wp-content/plugins/` directory.
40
  1. Activate the plugin through the 'Plugins' menu in WordPress.
41
 
42
  That's it. You can access the the menu editor by going to *Settings -> Menu Editor*. The plugin will automatically load your current menu configuration the first time you run it.
43
 
44
+ **WP MultiSite installation**
45
 
46
+ If you have WordPress set up in multisite ("Network") mode, you can also install Admin Menu Editor as a global plugin. This will enable you to edit the Dashboard menu for all sites and users at once.
47
 
48
+ 1. Download the admin-menu-editor.zip file to your computer.
49
  1. Unzip the file.
50
+ 1. Create a new directory named `mu-plugins` in your site's `wp-content` directory (unless it already exists).
51
+ 1. Upload the `admin-menu-editor` directory to `/wp-content/mu-plugins/`.
52
+ 1. Move `admin-menu-editor-mu.php` from `admin-menu-editor/includes` to `/wp-content/mu-plugins/`.
53
+
54
+ Plugins installed in the `mu-plugins` directory are treated as "always on", so you don't need to explicitly activate the menu editor. Just go to *Settings -> Menu Editor* and start customizing your admin menu :)
55
+
56
+ *Notes*
57
+ * Instead of installing Admin Menu Editor in `mu-plugins`, you can also install it normally and then activate it globally via "Network Activate". However, this will make the plugin visible to normal users when it is inactive (e.g. during upgrades).
58
+ * When Admin Menu Editor is installed in `mu-plugins` or activated via "Network Activate", only the "super admin" user can access the menu editor page. Other users will see the customized Dashboard menu, but be unable to edit it.
59
+ * It is currently not possible to install Admin Menu Editor as both a normal and global plugin on the same site.
60
 
 
61
 
62
  == Changelog ==
63
 
64
+ = 1.0 =
65
+ * Added a "Feedback" link.
66
+ * Added a dropdown list of all roles and capabilities to the menu editor.
67
+ * Added toolbar buttons for sorting menu items alphabetically.
68
+ * New "Add separator" button.
69
+ * New separator graphics.
70
+ * Minimum requirements upped to WP 3.0.
71
+ * Compatibility with WP 3.0 MultiSite mode.
72
+ * Plugin pages moved to different menus no longer stop working.
73
+ * Fixed moved pages not having a window title.
74
+ * Hide advanced menu fields by default (can be turned off in Screen Options).
75
+ * Changed a lot of UI text to be a bit more intuitive.
76
+ * In emergencies, administrators can now reset the custom menu by going to http:://example.com/wp-admin/?reset\_admin\_menu=1
77
+ * Fixed the "Donate" link in readme.txt
78
+ * Unbundle the JSON.php JSON parser/encoder and use the built-in class-json.php instead.
79
+ * Use the native JSON decoding routine if it's available.
80
+ * Replaced the cryptic "Cannot redeclare whatever" activation error message with a more useful one.
81
+
82
  = 0.2 =
83
  * Provisional WPMU support.
84
  * Missing and unused menu items now get different icons in the menu editor.
uninstall.php CHANGED
@@ -11,7 +11,10 @@ if( defined( 'ABSPATH') && defined('WP_UNINSTALL_PLUGIN') ) {
11
 
12
  //Remove the plugin's settings
13
  delete_option('ws_menu_editor');
14
-
 
 
 
15
  }
16
 
17
  ?>
11
 
12
  //Remove the plugin's settings
13
  delete_option('ws_menu_editor');
14
+ if ( function_exists('delete_site_option') ){
15
+ delete_site_option('ws_menu_editor');
16
+ }
17
+
18
  }
19
 
20
  ?>