Admin Menu Editor - Version 0.1.5

Version Description

  • First release on wordpress.org
  • Moved all images into a separate directory.
  • Added a readme.txt
Download this release

Release Info

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

Version 0.1.5

JSON.php ADDED
@@ -0,0 +1,805 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ?>
images/bullet_arrow_down2.png ADDED
Binary file
images/bullet_error.png ADDED
Binary file
images/cut.png ADDED
Binary file
images/delete.png ADDED
Binary file
images/page_white_add.png ADDED
Binary file
images/page_white_copy.png ADDED
Binary file
images/page_white_delete.png ADDED
Binary file
images/page_white_paste.png ADDED
Binary file
images/plugin_disabled.png ADDED
Binary file
jquery.json-1.3.js ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * jQuery JSON Plugin
3
+ * version: 1.0 (2008-04-17)
4
+ *
5
+ * This document is licensed as free software under the terms of the
6
+ * MIT License: http://www.opensource.org/licenses/mit-license.php
7
+ *
8
+ * Brantley Harris technically wrote this plugin, but it is based somewhat
9
+ * on the JSON.org website's http://www.json.org/json2.js, which proclaims:
10
+ * "NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.", a sentiment that
11
+ * I uphold. I really just cleaned it up.
12
+ *
13
+ * It is also based heavily on MochiKit's serializeJSON, which is
14
+ * copywrited 2005 by Bob Ippolito.
15
+ */
16
+
17
+ (function($) {
18
+ function toIntegersAtLease(n)
19
+ // Format integers to have at least two digits.
20
+ {
21
+ return n < 10 ? '0' + n : n;
22
+ }
23
+
24
+ Date.prototype.toJSON = function(date)
25
+ // Yes, it polutes the Date namespace, but we'll allow it here, as
26
+ // it's damned usefull.
27
+ {
28
+ return this.getUTCFullYear() + '-' +
29
+ toIntegersAtLease(this.getUTCMonth()) + '-' +
30
+ toIntegersAtLease(this.getUTCDate());
31
+ };
32
+
33
+ var escapeable = /["\\\x00-\x1f\x7f-\x9f]/g;
34
+ var meta = { // table of character substitutions
35
+ '\b': '\\b',
36
+ '\t': '\\t',
37
+ '\n': '\\n',
38
+ '\f': '\\f',
39
+ '\r': '\\r',
40
+ '"' : '\\"',
41
+ '\\': '\\\\'
42
+ };
43
+
44
+ $.quoteString = function(string)
45
+ // Places quotes around a string, inteligently.
46
+ // If the string contains no control characters, no quote characters, and no
47
+ // backslash characters, then we can safely slap some quotes around it.
48
+ // Otherwise we must also replace the offending characters with safe escape
49
+ // sequences.
50
+ {
51
+ if (escapeable.test(string))
52
+ {
53
+ return '"' + string.replace(escapeable, function (a)
54
+ {
55
+ var c = meta[a];
56
+ if (typeof c === 'string') {
57
+ return c;
58
+ }
59
+ c = a.charCodeAt();
60
+ return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16);
61
+ }) + '"';
62
+ }
63
+ return '"' + string + '"';
64
+ };
65
+
66
+ $.toJSON = function(o, compact)
67
+ {
68
+ var type = typeof(o);
69
+
70
+ if (type == "undefined")
71
+ return "undefined";
72
+ else if (type == "number" || type == "boolean")
73
+ return o + "";
74
+ else if (o === null)
75
+ return "null";
76
+
77
+ // Is it a string?
78
+ if (type == "string")
79
+ {
80
+ return $.quoteString(o);
81
+ }
82
+
83
+ // Does it have a .toJSON function?
84
+ if (type == "object" && typeof o.toJSON == "function")
85
+ return o.toJSON(compact);
86
+
87
+ // Is it an array?
88
+ if (type != "function" && typeof(o.length) == "number")
89
+ {
90
+ var ret = [];
91
+ for (var i = 0; i < o.length; i++) {
92
+ ret.push( $.toJSON(o[i], compact) );
93
+ }
94
+ if (compact)
95
+ return "[" + ret.join(",") + "]";
96
+ else
97
+ return "[" + ret.join(", ") + "]";
98
+ }
99
+
100
+ // If it's a function, we have to warn somebody!
101
+ if (type == "function") {
102
+ throw new TypeError("Unable to convert object of type 'function' to json.");
103
+ }
104
+
105
+ // It's probably an object, then.
106
+ var ret = [];
107
+ for (var k in o) {
108
+ var name;
109
+ type = typeof(k);
110
+
111
+ if (type == "number")
112
+ name = '"' + k + '"';
113
+ else if (type == "string")
114
+ name = $.quoteString(k);
115
+ else
116
+ continue; //skip non-string or number keys
117
+
118
+ var val = $.toJSON(o[k], compact);
119
+ if (typeof(val) != "string") {
120
+ // skip non-serializable values
121
+ continue;
122
+ }
123
+
124
+ if (compact)
125
+ ret.push(name + ":" + val);
126
+ else
127
+ ret.push(name + ": " + val);
128
+ }
129
+ return "{" + ret.join(", ") + "}";
130
+ };
131
+
132
+ $.compactJSON = function(o)
133
+ {
134
+ return $.toJSON(o, true);
135
+ };
136
+
137
+ $.evalJSON = function(src)
138
+ // Evals JSON that we know to be safe.
139
+ {
140
+ return eval("(" + src + ")");
141
+ };
142
+
143
+ $.secureEvalJSON = function(src)
144
+ // Evals JSON in a way that is *more* secure.
145
+ {
146
+ var filtered = src;
147
+ filtered = filtered.replace(/\\["\\\/bfnrtu]/g, '@');
148
+ filtered = filtered.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']');
149
+ filtered = filtered.replace(/(?:^|:|,)(?:\s*\[)+/g, '');
150
+
151
+ if (/^[\],:{}\s]*$/.test(filtered))
152
+ return eval("(" + src + ")");
153
+ else
154
+ throw new SyntaxError("Error parsing JSON, source is not valid.");
155
+ };
156
+ })(jQuery);
menu-editor-core.php ADDED
@@ -0,0 +1,617 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ function __construct($plugin_file=''){
17
+ //Set some plugin-specific options
18
+ $this->option_name = 'ws_menu_editor';
19
+ $this->defaults = array(
20
+ );
21
+
22
+ $this->settings_link = 'options-general.php?page=menu_editor';
23
+
24
+ $this->magic_hooks = true;
25
+ $this->magic_hook_priority = 99999;
26
+
27
+ //Call the default constructor
28
+ if ( empty($plugin_file) ) $plugin_file = __FILE__;
29
+ parent::__construct($plugin_file);
30
+
31
+ //Build some template arrays
32
+ $this->blank_menu = array(
33
+ 'page_title' => null,
34
+ 'menu_title' => null,
35
+ 'access_level' => null,
36
+ 'file' => null,
37
+ 'css_class' => null,
38
+ 'hookname' => null,
39
+ 'icon_url' => null,
40
+ 'position' => null,
41
+ );
42
+
43
+ $this->blank_item = array(
44
+ 'menu_title' => null,
45
+ 'access_level' => null,
46
+ 'file' => null,
47
+ 'page_title' => null,
48
+ 'position' => null,
49
+ );
50
+
51
+ }
52
+
53
+ //Backwards fompatible json_decode.
54
+ //We can't define this globally as that conflicts with the version created by Simple Tags.
55
+ function json_decode($data, $assoc=false){
56
+ $flag = $assoc?SERVICES_JSON_LOOSE_TYPE:0;
57
+ $json = new Services_JSON($flag);
58
+ return( $json->decode($data) );
59
+ }
60
+
61
+ //Backwards fompatible json_encode.
62
+ //Can't define this globally as that conflicts with Simple Tags.
63
+ function json_encode($data) {
64
+ $json = new Services_JSON();
65
+ return( $json->encode($data) );
66
+ }
67
+
68
+ /**
69
+ * WPMenuEditor::enqueue_scripts()
70
+ * Add the JS required by the editor to the page header
71
+ *
72
+ * @return void
73
+ */
74
+ function enqueue_scripts(){
75
+ wp_enqueue_script('jquery');
76
+ wp_enqueue_script('jquery-ui-sortable');
77
+
78
+ //jQuery JSON plugin
79
+ wp_enqueue_script('jquery-json', $this->plugin_dir_url.'/jquery.json-1.3.js', array('jquery'), '1.3');
80
+ //Editor's scipts
81
+ wp_enqueue_script('menu-editor', $this->plugin_dir_url.'/menu-editor.js', array('jquery'));
82
+ }
83
+
84
+ /**
85
+ * WPMenuEditor::print_editor_css()
86
+ * Add the editor's CSS file to the page header
87
+ *
88
+ * @return void
89
+ */
90
+ function print_editor_css(){
91
+ echo '<link type="text/css" rel="stylesheet" href="', $this->plugin_dir_url, '/menu-editor.css" />',"\n";
92
+ }
93
+
94
+ /**
95
+ * WPMenuEditor::hook_admin_menu()
96
+ * Create a configuration page and load the custom menu
97
+ *
98
+ * @return void
99
+ */
100
+ function hook_admin_menu(){
101
+ global $menu, $submenu;
102
+
103
+ $page = add_options_page('Menu Editor', 'Menu Editor', 'manage_options', 'menu_editor', array(&$this, 'page_menu_editor'));
104
+ //Output our JS & CSS on that page only
105
+ add_action("admin_print_scripts-$page", array(&$this, 'enqueue_scripts'));
106
+ add_action("admin_print_scripts-$page", array(&$this, 'print_editor_css'));
107
+
108
+ $this->default_wp_menu = $menu;
109
+ $this->default_wp_submenu = $submenu;
110
+
111
+ //Is there a custom menu to use?
112
+ if ( !empty($this->options['custom_menu']) ){
113
+ //Merge in data from the default menu
114
+ $tree = $this->menu_merge($this->options['custom_menu'], $menu, $submenu);
115
+ //Apply the custom menu
116
+ list($menu, $submenu) = $this->tree2wp($tree);
117
+ //Save for later - the editor page will need it
118
+ $this->custom_menu = $tree;
119
+ //Re-filter the menu (silly WP should do that itself, oh well)
120
+ $this->filter_menu();
121
+ }
122
+ }
123
+
124
+ /**
125
+ * WPMenuEditor::filter_menu()
126
+ * Loop over the Dashboard submenus and remove pages for which the current user does not have privs.
127
+ *
128
+ * @return void
129
+ */
130
+ function filter_menu(){
131
+ global $menu, $submenu, $_wp_submenu_nopriv, $_wp_menu_nopriv;
132
+
133
+ foreach ( array( 'submenu' ) as $sub_loop ) {
134
+ foreach ($$sub_loop as $parent => $sub) {
135
+ foreach ($sub as $index => $data) {
136
+ if ( ! current_user_can($data[1]) ) {
137
+ unset(${$sub_loop}[$parent][$index]);
138
+ $_wp_submenu_nopriv[$parent][$data[2]] = true;
139
+ }
140
+ }
141
+
142
+ if ( empty(${$sub_loop}[$parent]) )
143
+ unset(${$sub_loop}[$parent]);
144
+ }
145
+ }
146
+
147
+ }
148
+
149
+ /**
150
+ * WPMenuEditor::page_menu_editor()
151
+ * Output the menu editor page
152
+ *
153
+ * @return void
154
+ */
155
+ function page_menu_editor(){
156
+ global $menu, $submenu;
157
+ if ( !current_user_can('manage_options') ){
158
+ die("Access denied");
159
+ }
160
+
161
+ ?>
162
+ <div class="wrap">
163
+ <h2>Menu Editor</h2>
164
+ <?php
165
+ //Handle form submissions
166
+ if (isset($_POST['data'])){
167
+ check_admin_referer('menu-editor-form');
168
+
169
+ //Try to decode a menu tree encoded as JSON
170
+ $data = $this->json_decode($_POST['data'], true);
171
+ if (!$data){
172
+ echo "<!-- First decodind attempt failed, trying to fix with stripslashes() -->";
173
+ $fixed = stripslashes($_POST['data']);
174
+ $data = $this->json_decode( $fixed, true );
175
+ }
176
+
177
+ if ($data){
178
+ //Save the custom menu
179
+ $this->options['custom_menu'] = $data;
180
+ $this->save_options();
181
+ echo '<div id="message" class="updated fade"><p><strong>Settings saved. <a href="javascript:window.location.href=window.location.href">Reload the page</a> to see the modified menu.</strong></p></div>';
182
+ } else {
183
+ echo '<div id="message" class="error"><p><strong>Failed to decode input! The menu wasn\'t modified.</strong></p></div>';
184
+ }
185
+ }
186
+
187
+ //Build a tree struct. for the default menu
188
+ $default_menu = $this->wp2tree($this->default_wp_menu, $this->default_wp_submenu);
189
+
190
+ //Is there a custom menu?
191
+ if (!empty($this->options['custom_menu'])){
192
+ $custom_menu = $this->options['custom_menu'];
193
+ //Merge in the current defaults
194
+ $custom_menu = $this->menu_merge($custom_menu, $this->default_wp_menu, $this->default_wp_submenu);
195
+ } else {
196
+ //Start out with the default menu if there is no user-created one
197
+ $custom_menu = $default_menu;
198
+ }
199
+
200
+ //Encode both menus as JSON
201
+ $default_menu_js = $this->getMenuAsJS($default_menu);
202
+ $custom_menu_js = $this->getMenuAsJS($custom_menu);
203
+
204
+ $plugin_url = $this->plugin_dir_url;
205
+ $images_url = $this->plugin_dir_url . '/images';
206
+ ?>
207
+ <div id='ws_menu_editor'>
208
+ <div id='ws_menu_box' class='ws_main_container'>
209
+ <div class='ws_toolbar'>
210
+ <a id='ws_cut_menu' class='ws_button' href='javascript:void(0)' title='Cut'><img src='<?php echo $images_url; ?>/cut.png' /></a>
211
+ <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>
212
+ <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>
213
+ <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>
214
+ <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>
215
+ <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>
216
+ </div>
217
+ </div>
218
+ <div id='ws_submenu_box' class='ws_main_container'>
219
+ <div class='ws_toolbar'>
220
+ <a id='ws_cut_item' class='ws_button' href='javascript:void(0)' title='Cut'><img src='<?php echo $images_url; ?>/cut.png' /></a>
221
+ <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>
222
+ <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>
223
+ <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>
224
+ <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>
225
+ <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>
226
+ </div>
227
+ </div>
228
+ </div>
229
+
230
+ <div class="ws_main_container" style='width: 138px;'>
231
+ <form method="post" action="<?php echo admin_url('options-general.php?page=menu_editor'); ?>" id='ws_main_form' name='ws_main_form'>
232
+ <?php wp_nonce_field('menu-editor-form'); ?>
233
+ <input type="button" id='ws_save_menu' class="button-primary ws_main_button" value="Save Changes"
234
+ style="margin-bottom: 20px;" />
235
+ <input type="button" id='ws_load_menu' value="Load default menu" class="button ws_main_button" />
236
+ <input type="button" id='ws_reset_menu' value="Reset menu" class="button ws_main_button" />
237
+ <input type="hidden" name="data" id="ws_data" value="">
238
+ </form>
239
+ </div>
240
+
241
+ </div>
242
+ <script type='text/javascript'>
243
+
244
+ var defaultMenu = <?php echo $default_menu_js; ?>;
245
+ var customMenu = <?php echo $custom_menu_js; ?>;
246
+
247
+ </script>
248
+ <?php
249
+ }
250
+
251
+ /**
252
+ * WPMenuEditor::getMenuAsJS()
253
+ * Encode a menu tree as JSON
254
+ *
255
+ * @param array $tree
256
+ * @return string
257
+ */
258
+ function getMenuAsJS($tree){
259
+ return $this->json_encode($tree);
260
+ }
261
+
262
+ /**
263
+ * WPMenuEditor::menu2assoc()
264
+ * Convert a WP menu structure to an associative array
265
+ *
266
+ * @param array $item An element of the $menu array
267
+ * @param integer $pos The position (index) of the menu item
268
+ * @return array
269
+ */
270
+ function menu2assoc($item, $pos=0){
271
+ $item = array(
272
+ 'page_title' => $item[3],
273
+ 'menu_title' => $item[0],
274
+ 'access_level' => $item[1],
275
+ 'file' => $item[2],
276
+ 'css_class' => $item[4],
277
+ 'hookname' => (isset($item[5])?$item[5]:''), //ID
278
+ 'icon_url' => (isset($item[6])?$item[6]:''),
279
+ 'position' => $pos,
280
+ );
281
+ return $item;
282
+ }
283
+
284
+ /**
285
+ * WPMenuEditor::submenu2assoc()
286
+ * Converts a WP submenu structure to an associative array
287
+ *
288
+ * @param array $item An element of the $submenu array
289
+ * @param integer $pos The position (index) of that element
290
+ * @return
291
+ */
292
+ function submenu2assoc($item, $pos=0){
293
+ $item = array(
294
+ 'menu_title' => $item[0],
295
+ 'access_level' => $item[1],
296
+ 'file' => $item[2],
297
+ 'page_title' => (isset($item[3])?$item[3]:''),
298
+ 'position' => $pos,
299
+ );
300
+ return $item;
301
+ }
302
+
303
+ /**
304
+ * WPMenuEditor::build_lookups()
305
+ * Populate lookup arrays with default values from $menu and $submenu. Used later to merge
306
+ * a custom menu with the native WordPress menu structure somewhat gracefully.
307
+ *
308
+ * @param array $menu
309
+ * @param array $submenu
310
+ * @return array An array with two elements containing menu and submenu defaults.
311
+ */
312
+ function build_lookups($menu, $submenu){
313
+ //Process the top menu
314
+ $menu_defaults = array();
315
+ foreach($menu as $pos => $item){
316
+ $item = $this->menu2assoc($item, $pos);
317
+ if ($item['file'] != '') { //skip separators (empty menus)
318
+ $menu_defaults[$item['file']] = $item; //index by filename
319
+ }
320
+ }
321
+
322
+ //Process the submenu
323
+ $submenu_defaults = array();
324
+ foreach($submenu as $parent => $items){
325
+ foreach($items as $pos => $item){
326
+ $item = $this->submenu2assoc($item, $pos);
327
+ //save the default parent menu
328
+ $item['parent'] = $parent;
329
+ $submenu_defaults[$item['file']] = $item; //index by filename
330
+ }
331
+ }
332
+
333
+ return array($menu_defaults, $submenu_defaults);
334
+ }
335
+
336
+ /**
337
+ * WPMenuEditor::menu_merge()
338
+ * Merge $menu and $submenu into the $tree. Adds/replaces defaults, inserts new items
339
+ * and marks missing items as such.
340
+ *
341
+ * @param array $tree A menu in plugin's internal form
342
+ * @param array $menu WordPress menu structure
343
+ * @param array $submenu WordPress submenu structure
344
+ * @return array Updated menu tree
345
+ */
346
+ function menu_merge($tree, $menu, $submenu){
347
+ list($menu_defaults, $submenu_defaults) = $this->build_lookups($menu, $submenu);
348
+
349
+ //Iterate over all menus and submenus and look up default values
350
+ foreach ($tree as $topfile => &$topmenu){
351
+
352
+ //Is this menu present in the default WP menu?
353
+ if (isset($menu_defaults[$topfile])){
354
+ //Yes, load defaults from that item
355
+ $topmenu['defaults'] = $menu_defaults[$topfile];
356
+ //Note that the original item was used
357
+ $menu_defaults[$topfile]['used'] = true;
358
+ } else {
359
+ //Record the menu as missing, unless it's a menu separator
360
+ if ( empty($topmenu['separator']) /*strpos($topfile, 'separator_') !== false*/ )
361
+ $topmenu['missing'] = true;
362
+ }
363
+
364
+ if (is_array($topmenu['items'])) {
365
+ //Iterate over submenu items
366
+ foreach ($topmenu['items'] as $file => &$item){
367
+ //Is this item present in the default WP menu?
368
+ if (isset($submenu_defaults[$file])){
369
+ //Yes, load defaults from that item
370
+ $item['defaults'] = $submenu_defaults[$file];
371
+ $submenu_defaults[$file]['used'] = true;
372
+ } else {
373
+ //Record as missing
374
+ $item['missing'] = true;
375
+ }
376
+ }
377
+ }
378
+ }
379
+
380
+ //If we don't unset these they will fuck up the next two loops where the same names are used.
381
+ unset($topmenu);
382
+ unset($item);
383
+
384
+ //Note : Now we have some items marked as missing, and some items in lookup arrays
385
+ //that are not marked as used. The missing items are handled elsewhere (e.g. tree2wp()),
386
+ //but lets merge in the unused items now.
387
+
388
+ //Find and merge unused toplevel menus
389
+ foreach ($menu_defaults as $topfile => $topmenu){
390
+ if ( !empty($topmenu['used']) ) continue;
391
+
392
+ //Found an unused item. Build the tree entry.
393
+ $entry = $this->blank_menu;
394
+ $entry['defaults'] = $topmenu;
395
+ $entry['items'] = array(); //prepare a place for menu items, if any.
396
+ //Note that this item is unused
397
+ $entry['unused'] = true;
398
+ //Add the new entry to the menu tree
399
+ $tree[$topfile] = $entry;
400
+ }
401
+ unset($topmenu);
402
+
403
+ //Find and merge submenu items
404
+ foreach($submenu_defaults as $file => $item){
405
+ if ( !empty($item['used']) ) continue;
406
+ //Found an unused item. Build an entry and attach it under the default toplevel menu.
407
+ $entry = $this->blank_item;
408
+ $entry['defaults'] = $item;
409
+ //Note that this item is unused
410
+ $entry['unused'] = true;
411
+
412
+ //Check if the toplevel menu exists
413
+ if (isset($tree[$item['parent']])) {
414
+ //Okay, insert the item.
415
+ $tree[$item['parent']]['items'][$item['file']] = $entry;
416
+ } else {
417
+ //Ooops? This should never happen. Some kind of inconsistency?
418
+ }
419
+ }
420
+
421
+ //Resort the tree to ensure the found items are in the right spots
422
+ uasort($tree, array(&$this, 'compare_position'));
423
+ //Resort all submenus as well
424
+ foreach ($tree as $topfile => &$topmenu){
425
+ if (!empty($topmenu['items'])){
426
+ uasort($topmenu['items'], array(&$this, 'compare_position'));
427
+ }
428
+ }
429
+
430
+ return $tree;
431
+ }
432
+
433
+ /**
434
+ * WPMenuEditor::wp2tree()
435
+ * Convert the WP menu structure to the internal representation. All properties set as defaults.
436
+ *
437
+ * @param array $menu
438
+ * @param array $submenu
439
+ * @return array
440
+ */
441
+ function wp2tree($menu, $submenu){
442
+ $tree = array();
443
+ $separator_count = 0;
444
+ foreach ($menu as $pos => $item){
445
+ //Is this a separator?
446
+ if ($item[2] == ''){
447
+ //Yes. Most properties are unset for separators.
448
+ $tree['separator_'.$separator_count.'_'] = array(
449
+ 'page_title' => null,
450
+ 'menu_title' => null,
451
+ 'access_level' => null,
452
+ 'file' => null,
453
+ 'css_class' => null,
454
+ 'hookname' => null,
455
+ 'icon_url' => null,
456
+ 'position' => null,
457
+ 'defaults' => $this->menu2assoc($item, $pos),
458
+ 'separator' => true,
459
+ );
460
+ $separator_count++;
461
+ } else {
462
+ //No, a normal menu item
463
+ $tree[$item[2]] = array(
464
+ 'page_title' => null,
465
+ 'menu_title' => null,
466
+ 'access_level' => null,
467
+ 'file' => null,
468
+ 'css_class' => null,
469
+ 'hookname' => null,
470
+ 'icon_url' => null,
471
+ 'position' => null,
472
+ 'items' => array(),
473
+ 'defaults' => $this->menu2assoc($item, $pos),
474
+ );
475
+ }
476
+ }
477
+
478
+ //Attach all submenu items
479
+ foreach($submenu as $parent=>$items){
480
+ foreach($items as $pos=>$item){
481
+ //Add this item under the parent
482
+ $tree[$parent]['items'][$item[2]] = array(
483
+ 'menu_title' => null,
484
+ 'access_level' => null,
485
+ 'file' => null,
486
+ 'page_title' => null,
487
+ 'position' => null,
488
+ 'defaults' => $this->submenu2assoc($item, $pos),
489
+ );
490
+ }
491
+ }
492
+
493
+ return $tree;
494
+ }
495
+
496
+ /**
497
+ * WPMenuEditor::apply_defaults()
498
+ * Sets all undefined fields to the default value
499
+ *
500
+ * @param array $item Menu item in the plugin's internal form
501
+ * @return array
502
+ */
503
+ function apply_defaults($item){
504
+ foreach($item as $key => $value){
505
+ //Is the field set?
506
+ if ($value === null){
507
+ //Use default, if available
508
+ if (isset($item['defaults']) && isset($item['defaults'][$key])){
509
+ $item[$key] = $item['defaults'][$key];
510
+ }
511
+ }
512
+ }
513
+ return $item;
514
+ }
515
+
516
+ /**
517
+ * WPMenuEditor::compare_position()
518
+ * Custom comparison function that compares menu items based on their position in the menu.
519
+ *
520
+ * @param array $a
521
+ * @param array $b
522
+ * @return int
523
+ */
524
+ function compare_position($a, $b){
525
+ if ($a['position']!==null) {
526
+ $p1 = $a['position'];
527
+ } else {
528
+ if ( isset($a['defaults']['position']) ){
529
+ $p1 = $a['defaults']['position'];
530
+ } else {
531
+ $p1 = 0;
532
+ }
533
+ }
534
+
535
+ if ($b['position']!==null) {
536
+ $p2 = $b['position'];
537
+ } else {
538
+ if ( isset($b['defaults']['position']) ){
539
+ $p2 = $b['defaults']['position'];
540
+ } else {
541
+ $p2 = 0;
542
+ }
543
+ }
544
+
545
+ return $p1 - $p2;
546
+ }
547
+
548
+ /**
549
+ * WPMenuEditor::tree2wp()
550
+ * Convert internal menu representation to the form used by WP.
551
+ *
552
+ * @param array $tree
553
+ * @return array $menu and $submenu
554
+ */
555
+ function tree2wp($tree){
556
+ //Sort the menu by position
557
+ uasort($tree, array(&$this, 'compare_position'));
558
+
559
+ //Prepare the top menu
560
+ $menu = array();
561
+ foreach ($tree as &$topmenu){
562
+ //Skip missing entries -- disabled to allow user-created menus
563
+ //if (isset($topmenu['missing']) && $topmenu['missing']) continue;
564
+ //Skip hidden entries
565
+ if (!empty($topmenu['hidden'])) continue;
566
+ //Build the WP item structure, using defaults where necessary
567
+ $topmenu = $this->apply_defaults($topmenu);
568
+ $menu[] = array(
569
+ $topmenu['menu_title'],
570
+ $topmenu['access_level'],
571
+ $topmenu['file'],
572
+ $topmenu['page_title'],
573
+ $topmenu['css_class'],
574
+ $topmenu['hookname'], //ID
575
+ $topmenu['icon_url']
576
+ );
577
+ }
578
+
579
+ //Prepare the submenu
580
+ $submenu = array();
581
+ foreach ($tree as $x){
582
+ if (!isset($x['items'])) continue; //skip menus without items (usually separators)
583
+ $items = $x['items'];
584
+ //Sort by position
585
+ uasort($items, array(&$this, 'compare_position'));
586
+ foreach ($items as $item) {
587
+ //Skip hidden items
588
+ if (!empty($item['hidden'])) {
589
+ continue;
590
+ }
591
+
592
+ $item = $this->apply_defaults($item);
593
+ $submenu[$x['file']][] = array(
594
+ $item['menu_title'],
595
+ $item['access_level'],
596
+ $item['file'],
597
+ $item['page_title'],
598
+ );
599
+ }
600
+ }
601
+
602
+ return array($menu, $submenu);
603
+ }
604
+
605
+ /**
606
+ * WPMenuEditor::is_wp27()
607
+ * Check if running WordPress 2.7 or later (unused)
608
+ *
609
+ * @return bool
610
+ */
611
+ function is_wp27(){
612
+ global $wp_version;
613
+ return function_exists('register_uninstall_hook');
614
+ }
615
+ } //class
616
+
617
+ ?>
menu-editor.css ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ width: 120px;
17
+ margin: 4px;
18
+ padding: 4px;
19
+ }
20
+
21
+ .ws_container {
22
+ display: block;
23
+ width: 290px;
24
+ padding : 3px;
25
+ margin: 2px;
26
+ border: 1px solid #a9badb;
27
+ background-color: #bdd3ff;
28
+ }
29
+
30
+ .ws_active {
31
+ background-color : #8eb0f1 !important; /* make sure this overrides ws_menu_separator */
32
+ }
33
+
34
+ #ws_menu_box {
35
+ }
36
+
37
+ #ws_submenu_box {
38
+ }
39
+
40
+ .ws_menu {
41
+ }
42
+
43
+ .ws_menu_separator {
44
+ background-color: #f0f0f0;
45
+ }
46
+
47
+ .ws_submenu {
48
+ background-color: #f0fafa;
49
+ min-height: 2em;
50
+ }
51
+
52
+
53
+ .ws_item_head {
54
+ padding-left: 2px;
55
+ padding-top: 2px;
56
+ padding-bottom: 2px;
57
+ cursor: default;
58
+ }
59
+
60
+ .ws_item_title {
61
+ }
62
+
63
+ .ws_edit_link {
64
+ float: right;
65
+ margin-right: 0px;
66
+ cursor: pointer;
67
+ display:block;
68
+ height: 18px;
69
+ width: 40px;
70
+ background-image: url('images/bullet_arrow_down2.png');
71
+ background-repeat: no-repeat;
72
+ background-position: center;
73
+ }
74
+ a.ws_edit_link:hover {
75
+ background-color: #ffffd0;
76
+ background-image: url('images/bullet_arrow_down2.png');
77
+ background-repeat: no-repeat;
78
+ background-position: center;
79
+ }
80
+
81
+ .ws_edit_link_expanded {
82
+ background-color: #ffffd0;
83
+ border-bottom: none;
84
+ border-color: #ffffd0;
85
+ background-image: url('images/bullet_arrow_down2.png');
86
+ background-repeat: no-repeat;
87
+ background-position: center;
88
+ }
89
+
90
+ /* style for items not present in the default menu - these are usually user-created items */
91
+ .ws_missing {
92
+ background-image: url('images/page_white_add.png');
93
+ background-repeat: no-repeat;
94
+ background-position: 232px 5px;
95
+ }
96
+
97
+ /* style for hidden items */
98
+ .ws_hidden {
99
+ background-image: url('images/plugin_disabled.png');
100
+ background-repeat: no-repeat;
101
+ background-position: 232px 5px;
102
+ }
103
+
104
+ /* style for unused items - those that are in the default menu but not in the custom one */
105
+ .ws_unused {
106
+ background-image: url('images/bullet_error.png');
107
+ background-repeat: no-repeat;
108
+ background-position: 232px 5px;
109
+ }
110
+
111
+ .ws_item {
112
+ }
113
+
114
+ .ws_editbox {
115
+ display: block;
116
+ background-color: #ffffd0;
117
+ padding: 4px;
118
+ }
119
+
120
+ /* the reset-to-default button */
121
+ .ws_reset_button {
122
+ font-size: smaller;
123
+ margin : 1px;
124
+ cursor: pointer;
125
+ }
126
+
127
+ .ws_editbox input {
128
+ width: 220px;
129
+ }
130
+
131
+ .ws_input_default {
132
+ color: gray;
133
+ }
134
+
135
+ .ws_edit_field {
136
+ margin-bottom: 8px;
137
+ }
138
+
139
+ .ws_toolbar {
140
+ display: block;
141
+ width: 290px;
142
+ height: 32px;
143
+ }
144
+
145
+ .ws_button {
146
+ display: block;
147
+ margin: 2px;
148
+ padding: 4px;
149
+ border: 1px solid #c0c0e0;
150
+ float: left;
151
+ }
152
+
153
+ a.ws_button:hover {
154
+ background-color: #d0e0ff;
155
+ }
menu-editor.js ADDED
@@ -0,0 +1,586 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
58
+
59
+ //The "Default" button : Reset to default value when clicked
60
+ $('.ws_reset_button').click(function () {
61
+ //Find the related input field
62
+ var field = $(this).siblings('input');
63
+ if (field.length > 0) {
64
+ //Set the value to the default
65
+ field.val(field.attr('default'));
66
+ field.addClass('ws_input_default');
67
+ //Trigget the change event to ensure consistency
68
+ field.change();
69
+ }
70
+ });
71
+
72
+ //When a field is edited, change it's appearance if it's contents don't match the default value.
73
+ $('.ws_edit_field input[type="text"]').change(function () {
74
+ if ( $(this).attr('default') != $(this).val() ) {
75
+ $(this).removeClass('ws_input_default');
76
+ }
77
+
78
+ //If the changed field is the menu title, update the header
79
+ if ( $(this).parent().attr('field_name')=='menu_title' ){
80
+ $(this).parent().parent().parent().find('.ws_item_title').html($(this).val()+'&nbsp;');
81
+ }
82
+ });
83
+ }
84
+
85
+ function outputTopMenu(menu, filename, ind){
86
+ id = 'topmenu-'+ind;
87
+ submenu_id = 'submenu-'+ind;
88
+
89
+ //menu = menu_obj[filename];
90
+
91
+ var subclass = '';
92
+ //Apply subclasses based on the item's state
93
+ if ( menu.separator /*(!menu.defaults.menu_title) && (!menu.menu_title)*/ ) {
94
+ subclass = subclass + ' ws_menu_separator';
95
+ }
96
+ if (menu.missing) {
97
+ subclass = subclass + ' ws_missing';
98
+ }
99
+ if (menu.hidden) {
100
+ subclass = subclass + ' ws_hidden';
101
+ }
102
+ if (menu.unused) {
103
+ subclass = subclass + ' ws_unused';
104
+ }
105
+
106
+ var s = '<div id="'+id+'" class="ws_container ws_menu '+subclass+'" submenu_id="'+submenu_id+'">'+
107
+ '<div class="ws_item_head">'+
108
+ '<a class="ws_edit_link"> </a>'+
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
+ $('#ws_menu_box').append(s);
117
+ //Create a container for menu items, even if there are none
118
+ $('#ws_submenu_box').append('<div class="ws_submenu" id="'+submenu_id+'" style="display:none;"></div>');
119
+
120
+ //Only show menus that have items.
121
+ //Skip arrays (with a length) because filled menus are encoded as custom objects ().
122
+ if (menu.items && (typeof menu.items != 'Array')){
123
+ var i = 0;
124
+ for (var item_file in menu.items){
125
+ outputMenuEntry(menu.items[item_file], i, submenu_id);
126
+ i++;
127
+ }
128
+ }
129
+ }
130
+
131
+ function outputMenuEntry(entry, ind, parent){
132
+ if (!entry.defaults) return;
133
+
134
+ var subclass = '';
135
+ //Apply subclasses based on the item's state
136
+ if (entry.missing) {
137
+ subclass = subclass + ' ws_missing';
138
+ }
139
+ if (entry.hidden) {
140
+ subclass = subclass + ' ws_hidden';
141
+ }
142
+ if (entry.unused) {
143
+ subclass = subclass + ' ws_unused';
144
+ }
145
+
146
+ var item = $('#'+parent).append('<div class="ws_container ws_item '+subclass+'">'+
147
+ '<div class="ws_item_head">'+
148
+ '<a class="ws_edit_link"> </a>'+
149
+ '<span class="ws_item_title">'+
150
+ ((entry.menu_title!=null)?entry.menu_title:entry.defaults.menu_title)+
151
+ '&nbsp;</span>'+
152
+ '</div>'+
153
+ '<div class="ws_editbox" style="display:none;">'+buildEditboxFields(entry)+'</div>'+
154
+ '<div>');
155
+ }
156
+
157
+ function buildEditboxField(entry, field_name, field_caption){
158
+ if (entry[field_name]===undefined) {
159
+ return ''; //skip fields this entry doesn't have
160
+ }
161
+
162
+ return '<div class="ws_edit_field" field_name="'+field_name+'">' + (field_caption) + '<br />' +
163
+ '<input type="text" value="'+escapeJS((entry[field_name]!=null)?entry[field_name]:entry.defaults[field_name])+
164
+ '" default=\''+escapeJS(entry.defaults[field_name])+'\''+
165
+ ' class="'+((entry[field_name]==null)?'ws_input_default':'')+'">'+
166
+ '<span class="ws_reset_button">[default]</span></div>';
167
+ }
168
+
169
+ function buildEditboxFields(entry){
170
+ var fields = {
171
+ 'menu_title' : "Menu title",
172
+ 'page_title' : "Page title",
173
+ 'access_level' : 'Access level',
174
+ 'file' : 'File',
175
+ 'css_class' : 'CSS class',
176
+ 'hookname' : 'CSS ID',
177
+ 'icon_url' : 'Icon URL'
178
+ };
179
+ var s = '';
180
+
181
+ for (var field_name in fields){
182
+ s = s + buildEditboxField(entry, field_name, fields[field_name]);
183
+ }
184
+ return s;
185
+ }
186
+
187
+ //Encode the current menu structure as JSON
188
+ function encodeMenuAsJSON(){
189
+ var data = {};
190
+ var separator_count = 0;
191
+ var menu_position = 0;
192
+
193
+ //Iterate over all menus
194
+ $('#ws_menu_box .ws_menu').each(function(i) {
195
+
196
+ var menu_obj = {};
197
+ menu_obj.defaults = {};
198
+
199
+ menu_position++;
200
+ menu_obj.position = menu_position;
201
+ menu_obj.defaults.position = menu_position; //the real default value will later overwrite this
202
+
203
+ var filename = $(this).find('.ws_edit_field[field_name="file"] input').val();
204
+ //Check if this is a separator
205
+ if (filename==''){
206
+ filename = 'separator_'+separator_count+'_';
207
+ menu_obj.separator = true;
208
+ separator_count++;
209
+ }
210
+
211
+ //Iterate over all fields of the menu
212
+ $(this).find('.ws_edit_field').each(function() {
213
+ //Get the name of this field
214
+ field_name = $(this).attr('field_name');
215
+ //Skip if unnamed
216
+ if (!field_name) return true;
217
+
218
+ input_box = $(this).find('input');
219
+ //Save null if default used, custom value otherwise
220
+ if (input_box.hasClass('ws_input_default')){
221
+ menu_obj[field_name] = null;
222
+ } else {
223
+ menu_obj[field_name] = input_box.val();
224
+ }
225
+ menu_obj.defaults[field_name]=input_box.attr('default');
226
+
227
+ });
228
+ //Check if the menu is hidden
229
+ if ($(this).hasClass('ws_hidden')){
230
+ menu_obj['hidden'] = true;
231
+ }
232
+
233
+ menu_obj.items = {};
234
+
235
+ var item_position = 0;
236
+
237
+ //Iterate over the menu's items, if any
238
+ $('#'+$(this).attr('submenu_id')).find('.ws_item').each(function (i) {
239
+ var filename = $(this).find('.ws_edit_field[field_name="file"] input').val();
240
+
241
+ var item = {};
242
+ item.defaults = {};
243
+
244
+ //Save the position data (probably not all that useful)
245
+ item_position++;
246
+ item.position = item_position;
247
+ item.defaults.position = item_position;
248
+
249
+ //Iterate over all fields of the item
250
+ $(this).find('.ws_edit_field').each(function() {
251
+ //Get the name of this field
252
+ field_name = $(this).attr('field_name');
253
+ //Skip if unnamed
254
+ if (!field_name) return true;
255
+
256
+ input_box = $(this).find('input');
257
+ //Save null if default used, custom value otherwise
258
+ if (input_box.hasClass('ws_input_default')){
259
+ item[field_name] = null;
260
+ } else {
261
+ item[field_name] = input_box.val();
262
+ }
263
+ item.defaults[field_name]=input_box.attr('default');
264
+
265
+ });
266
+ //Check if the item is hidden
267
+ if ($(this).hasClass('ws_hidden')){
268
+ item.hidden = true;
269
+ }
270
+ //Save the item in the parent menu
271
+ menu_obj.items[filename] = item;
272
+ });
273
+ //*/
274
+
275
+ //Attach the menu to the main struct
276
+ data[filename] = menu_obj;
277
+
278
+ });
279
+
280
+ return $.toJSON(data);
281
+ }
282
+
283
+ var menu_in_clipboard = null;
284
+ var submenu_in_clipboard = null;
285
+ var item_in_clipboard = null;
286
+ var ws_paste_count = 0;
287
+
288
+ $(document).ready(function(){
289
+
290
+ //Show the default menu
291
+ outputWpMenu(customMenu);
292
+
293
+ //Make the top menu box sortable (we only need to do this once)
294
+ $('#ws_menu_box').sortable({
295
+ items: '> .ws_container',
296
+ cursor: 'move',
297
+ dropOnEmpty: true,
298
+ });
299
+
300
+ //===== Toolbar buttons =======
301
+ //Show/Hide menu
302
+ $('#ws_hide_menu').click(function () {
303
+ //Get the selected menu
304
+ var selection = $('#ws_menu_box .ws_active');
305
+ if (!selection.length) return;
306
+
307
+ //Mark the menu as hidden
308
+ selection.toggleClass('ws_hidden');
309
+ //Also mark all of it's submenus as hidden/visible
310
+ if (selection.hasClass('ws_hidden')){
311
+ $('#' + selection.attr('submenu_id') + ' .ws_item').addClass('ws_hidden');
312
+ } else {
313
+ $('#' + selection.attr('submenu_id') + ' .ws_item').removeClass('ws_hidden');
314
+ }
315
+ });
316
+
317
+ //Delete menu
318
+ $('#ws_delete_menu').click(function () {
319
+ //Get the selected menu
320
+ var selection = $('#ws_menu_box .ws_active');
321
+ if (!selection.length) return;
322
+
323
+ if (confirm('Are you sure you want to delete this menu?')){
324
+ //Delete the submenu first
325
+ $('#' + selection.attr('submenu_id')).remove();
326
+ //Delete the menu
327
+ selection.remove();
328
+ }
329
+ });
330
+
331
+ //Copy menu
332
+ $('#ws_copy_menu').click(function () {
333
+ //Get the selected menu
334
+ var selection = $('#ws_menu_box .ws_active');
335
+ if (!selection.length) return;
336
+
337
+ //Store a copy in clipboard
338
+ menu_in_clipboard = selection.clone(true); //just like that
339
+ menu_in_clipboard.removeClass('ws_active');
340
+ submenu_in_clipboard = $('#'+selection.attr('submenu_id')).clone(true);
341
+ });
342
+
343
+ //Cut menu
344
+ $('#ws_cut_menu').click(function () {
345
+ //Get the selected menu
346
+ var selection = $('#ws_menu_box .ws_active');
347
+ if (!selection.length) return;
348
+
349
+ //Store a copy of both menu and it's submenu in clipboard
350
+ menu_in_clipboard = selection.removeClass('ws_active').clone(true);
351
+ menu_in_clipboard.removeClass('ws_active');
352
+ submenu_in_clipboard = $('#'+selection.attr('submenu_id')).clone(true);
353
+ //Remove the original menu and submenu
354
+ selection.remove();
355
+ $('#'+selection.attr('submenu_id')).remove;
356
+ });
357
+
358
+ //Paste menu
359
+ $('#ws_paste_menu').click(function () {
360
+ //Check if anything has been copied/cut
361
+ if (!menu_in_clipboard) return;
362
+ //Get the selected menu
363
+ var selection = $('#ws_menu_box .ws_active');
364
+
365
+ ws_paste_count++;
366
+
367
+ //Clone new objects from the virtual clipboard
368
+ var new_menu = menu_in_clipboard.clone(true);
369
+ var new_submenu = submenu_in_clipboard.clone(true);
370
+ //Close submenu editboxes
371
+ new_submenu.find('.ws_editbox').hide();
372
+
373
+ //The cloned menu must have a unique file name, unless it's a separator
374
+ if (!new_menu.hasClass('ws_menu_separator')) {
375
+ new_menu.find('.ws_edit_field[field_name="file"] input').val('custom_menu_'+ws_paste_count);
376
+ }
377
+
378
+ //The cloned submenu needs a unique ID (could be improved)
379
+ new_submenu.attr('id', 'ws-pasted-obj-'+ws_paste_count);
380
+ new_menu.attr('submenu_id', 'ws-pasted-obj-'+ws_paste_count);
381
+
382
+ //Make the new submenu sortable
383
+ new_submenu.sortable({
384
+ items: '> .ws_container',
385
+ cursor: 'move',
386
+ dropOnEmpty: true,
387
+ });
388
+
389
+ if (selection.length > 0) {
390
+ //If a menu is selected add the pasted item after it
391
+ selection.after(new_menu);
392
+ } else {
393
+ //Otherwise add the pasted item at the end
394
+ $('#ws_menu_box').append(new_menu);
395
+ };
396
+
397
+ //Insert the submenu in the box, too
398
+ $('#ws_submenu_box').append(new_submenu);
399
+
400
+ new_menu.show();
401
+ new_submenu.hide();
402
+ });
403
+
404
+ //New menu
405
+ $('#ws_new_menu').click(function () {
406
+ ws_paste_count++;
407
+
408
+ //This is a hack.
409
+ //Clone another menu to use as a template
410
+ var menu = $('#ws_menu_box .ws_menu:first').clone(true);
411
+ //Also clone a submenu
412
+ var submenu = $('#' + menu.attr('submenu_id')).clone(true);
413
+ //Assign a new ID
414
+ submenu.attr('id', 'ws-new-submenu-'+ws_paste_count);
415
+ menu.attr('submenu_id', 'ws-new-submenu-'+ws_paste_count);
416
+ //Remove all items from the submenu
417
+ submenu.empty();
418
+ //Make the submenu sortable
419
+ submenu.sortable({
420
+ items: '> .ws_container',
421
+ cursor: 'move',
422
+ dropOnEmpty: true,
423
+ });
424
+
425
+ //Cleanup the menu's classes
426
+ menu.attr('class','ws_container ws_menu ws_missing');
427
+
428
+
429
+ var temp_id = 'custom_menu_'+ws_paste_count;
430
+ //Assign a stub title
431
+ menu.find('.ws_item_title').text('Custom Menu '+ws_paste_count);
432
+ //All fields start out set to defaults
433
+ menu.find('input').attr('default','').addClass('ws_input_default');
434
+ //Set all fields
435
+ menu.find('.ws_edit_field[field_name="page_title"] input').val('').attr('default','');
436
+ menu.find('.ws_edit_field[field_name="menu_title"] input').val('Custom Menu '+ws_paste_count).attr('default','Custom Menu '+ws_paste_count);
437
+ menu.find('.ws_edit_field[field_name="access_level"] input').val('read').attr('default','read');
438
+ menu.find('.ws_edit_field[field_name="file"] input').val(temp_id).attr('default',temp_id);
439
+ menu.find('.ws_edit_field[field_name="css_class"] input').val('menu-top').attr('default','menu-top');
440
+ menu.find('.ws_edit_field[field_name="icon_url"] input').val('images/generic.png').attr('default','images/generic.png');
441
+ menu.find('.ws_edit_field[field_name="hookname"] input').val(temp_id).attr('default',temp_id);
442
+
443
+ //The menus's editbox is always open
444
+ menu.find('.ws_editbox').show();
445
+ //Make sure the edit link is in the right state, too
446
+ menu.find('.ws_edit_link').addClass('ws_edit_link_expanded');
447
+
448
+ //Finally, insert the menu into the box
449
+ $('#ws_menu_box').append(menu);
450
+ //And insert the submenu
451
+ $('#ws_submenu_box').append(submenu);
452
+ });
453
+
454
+ //===== Item toolbar buttons =======
455
+ //Show/Hide item
456
+ $('#ws_hide_item').click(function () {
457
+ //Get the selected item
458
+ var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
459
+ if (!selection.length) return;
460
+
461
+ //Mark the item as hidden
462
+ selection.toggleClass('ws_hidden');
463
+ });
464
+
465
+ //Delete menu
466
+ $('#ws_delete_item').click(function () {
467
+ //Get the selected menu
468
+ var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
469
+ if (!selection.length) return;
470
+
471
+ if (confirm('Are you sure you want to delete this menu item?')){
472
+ //Delete the item
473
+ selection.remove();
474
+ }
475
+ });
476
+
477
+ //Copy item
478
+ $('#ws_copy_item').click(function () {
479
+ //Get the selected item
480
+ var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
481
+ if (!selection.length) return;
482
+
483
+ //Store a copy in clipboard
484
+ item_in_clipboard = selection.clone(true); //just like that
485
+ item_in_clipboard.removeClass('ws_active');
486
+ });
487
+
488
+ //Cut item
489
+ $('#ws_cut_item').click(function () {
490
+ //Get the selected item
491
+ var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
492
+ if (!selection.length) return;
493
+
494
+ //Store a the item in clipboard
495
+ item_in_clipboard = selection.clone(true);
496
+ item_in_clipboard.removeClass('ws_active');
497
+ //Remove the original item
498
+ selection.remove();
499
+ });
500
+
501
+ //Paste item
502
+ $('#ws_paste_item').click(function () {
503
+ //Check if anything has been copied/cut
504
+ if (!item_in_clipboard) return;
505
+ //Get the selected menu
506
+ var selection = $('#ws_submenu_box .ws_submenu:visible .ws_active');
507
+
508
+ ws_paste_count++;
509
+
510
+ //Clone a new object from the virtual clipboard
511
+ var new_item = item_in_clipboard.clone(true);
512
+ //The item's editbox is always closed
513
+ new_item.find('.ws_editbox').hide();
514
+
515
+ if (selection.length > 0) {
516
+ //If an item is selected add the pasted item after it
517
+ selection.after(new_item);
518
+ } else {
519
+ //Otherwise add the pasted item at the end
520
+ $('#ws_submenu_box .ws_submenu:visible').append(new_item);
521
+ };
522
+
523
+ new_item.show();
524
+ });
525
+
526
+ //New item
527
+ $('#ws_new_item').click(function () {
528
+ if ($('.ws_submenu:visible').length<1) return; //abort if no submenu visible
529
+
530
+ ws_paste_count++;
531
+
532
+ //Clone another item to use as a template (hack)
533
+ var menu = $('#ws_submenu_box .ws_item:first').clone(true);
534
+
535
+ //Cleanup the items's classes
536
+ menu.attr('class','ws_container ws_item ws_missing');
537
+
538
+ var temp_id = 'custom_item_'+ws_paste_count;
539
+ //Assign a stub title
540
+ menu.find('.ws_item_title').text('Custom Item '+ws_paste_count);
541
+ //All fields start out set to defaults
542
+ menu.find('input').attr('default','').addClass('ws_input_default');
543
+ //Set all fields
544
+ menu.find('.ws_edit_field[field_name="page_title"] input').val('').attr('default','');
545
+ menu.find('.ws_edit_field[field_name="menu_title"] input').val('Custom Item '+ws_paste_count).attr('default','Custom Item '+ws_paste_count);
546
+ menu.find('.ws_edit_field[field_name="access_level"] input').val('read').attr('default','read');
547
+ menu.find('.ws_edit_field[field_name="file"] input').val(temp_id).attr('default',temp_id);
548
+
549
+ //The items's editbox is always open
550
+ menu.find('.ws_editbox').show();
551
+ //Make sure the edit link is in the right state, too
552
+ menu.find('.ws_edit_link').addClass('ws_edit_link_expanded');
553
+
554
+ //Finally, insert the item into the box
555
+ $('.ws_submenu:visible').append(menu);
556
+ });
557
+
558
+ //==============================================
559
+ // Main buttons
560
+ //==============================================
561
+
562
+ //Save Changes - encode the current menu as JSON and save
563
+ $('#ws_save_menu').click(function () {
564
+ var data = encodeMenuAsJSON();
565
+ $('#ws_data').val(data);
566
+ $('#ws_main_form').submit();
567
+ });
568
+
569
+ //Load default menu - load the default WordPress menu
570
+ $('#ws_load_menu').click(function () {
571
+ if (confirm('Are you sure you want to load the default WordPress menu into the editor?')){
572
+ outputWpMenu(defaultMenu);
573
+ }
574
+ });
575
+
576
+ //Reset menu - re-load the custom menu = discards any changes made by user
577
+ $('#ws_reset_menu').click(function () {
578
+ if (confirm('Are you sure you want to reset the custom menu? Any unsaved changes will be lost!')){
579
+ outputWpMenu(customMenu);
580
+ }
581
+ });
582
+
583
+ });
584
+
585
+
586
+ })(jQuery);
menu-editor.php ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /*
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.1.5
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
+ ?>
readme.txt ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ === Admin Menu Editor ===
2
+ Contributors: whiteshadow
3
+ Donate link: http://w-shadow.com/
4
+ Tags: admin, dashboard, menu, security
5
+ Requires at least: 2.7.0
6
+ Tested up to: 2.9
7
+ Stable tag: 0.1.5
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
+
11
+ == Description ==
12
+ Admin Menu Editor lets you manually edit the Dashboard menu. You can reorder the menus, show/hide specific items, change access rights, and more.
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
+ **Known Issues**
23
+
24
+ * If you delete any of the default menus they will reappear after saving. This is by design.
25
+ * You can't use arbitrary URLs as menu targets because WordPress will automatically strip off the "http:/".
26
+ * 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.
27
+
28
+ == Installation ==
29
+
30
+ To do a new installation of the plugin, please follow these steps
31
+
32
+ 1. Download the admin-menu-editor.zip file to your local machine.
33
+ 1. Unzip the file
34
+ 1. Upload `admin-menu-editor` folder to the `/wp-content/plugins/` directory
35
+ 1. Activate the plugin through the 'Plugins' menu in WordPress.
36
+
37
+ 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.
38
+
39
+ To upgrade your installation
40
+
41
+ 1. De-activate the plugin
42
+ 1. Get and upload the new files (do steps 1. - 3. from "new installation" instructions)
43
+ 1. Reactivate the plugin. Your settings should have been retained from the previous version.
44
+
45
+ == Changelog ==
46
+
47
+ = 0.1.5 =
48
+ * First release on wordpress.org
49
+ * Moved all images into a separate directory.
50
+ * Added a readme.txt
shadow_plugin_framework.php ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * @author W-Shadow
5
+ * @copyright 2008
6
+ */
7
+
8
+ //Make sure the needed constants are defined
9
+ if ( ! defined( 'WP_CONTENT_URL' ) )
10
+ define( 'WP_CONTENT_URL', get_option( 'siteurl' ) . '/wp-content' );
11
+ if ( ! defined( 'WP_CONTENT_DIR' ) )
12
+ define( 'WP_CONTENT_DIR', ABSPATH . 'wp-content' );
13
+ 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.1.2';
20
+
21
+ protected $options = array();
22
+ public $option_name = ''; //should be set or overriden by the plugin
23
+ protected $defaults = array(); //should be set or overriden by the plugin
24
+
25
+ public $plugin_file = ''; //Filename of the plugin.
26
+ public $plugin_basename = ''; //Basename of the plugin, as returned by plugin_basename().
27
+ public $plugin_dir_url = ''; //The URL of the plugin's folder
28
+
29
+ protected $magic_hooks = false; //Automagically set up hooks for all methods named "hook_[hookname]" .
30
+ protected $magic_hook_priority = 10; //Priority for magically set hooks.
31
+
32
+ protected $settings_link = ''; //If set, this will be automatically added after "Deactivate"/"Edit".
33
+
34
+ /**
35
+ * ShadowPluginFramework::__construct()
36
+ * Initializes the plugin and loads settings from the database.
37
+ *
38
+ * @param string $plugin_file Plugin's filename. Usuallly you can just use __FILE__.
39
+ * @return void
40
+ */
41
+ protected function __construct( $plugin_file = ''){
42
+ if ($plugin_file == ''){
43
+ //Try to guess the name of the file that included this file.
44
+ //XXXXXX - not implemented yet.
45
+ }
46
+ $this->plugin_file = $plugin_file;
47
+ $this->plugin_basename = plugin_basename($this->plugin_file);
48
+ $this->plugin_dir_url = WP_PLUGIN_URL . '/' . dirname($this->plugin_basename);
49
+
50
+ /************************************
51
+ Load settings
52
+ ************************************/
53
+ //The provided $option_name overrides the default only if it is set to something useful
54
+ if ( $this->option_name == '' ) {
55
+ //Generate a unique name
56
+ $this->option_name = 'plugin_'.md5($this->plugin_basename);
57
+ }
58
+
59
+ //Do we need to load the plugin's settings?
60
+ if ($this->option_name != null){
61
+ $this->load_options();
62
+ }
63
+
64
+ /************************************
65
+ Add the default hooks
66
+ ************************************/
67
+ add_action('activate_'.$this->plugin_basename, array(&$this,'activate'));
68
+ add_action('deactivate_'.$this->plugin_basename, array(&$this,'deactivate'));
69
+
70
+ if ($this->settings_link)
71
+ add_filter('plugin_action_links', array(&$this, 'plugin_action_links'), 10, 2);
72
+
73
+ if ($this->magic_hooks)
74
+ $this->set_magic_hooks();
75
+ }
76
+
77
+ /**
78
+ * ShadowPluginFramework::load_options()
79
+ * Loads the plugin's configuration : loads an option specified by $this->option_name into $this->options.
80
+ *
81
+ * @return boolean TRUE if options were loaded okay and FALSE otherwise.
82
+ */
83
+ function load_options(){
84
+ $this->options = get_option($this->option_name);
85
+ if(!is_array($this->options)){
86
+ $this->options = $this->defaults;
87
+ return false;
88
+ } else {
89
+ $this->options = array_merge($this->defaults, $this->options);
90
+ return true;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * ShadowPluginFramework::set_magic_hooks()
96
+ * Automagically sets up hooks for all methods named "hook_[tag]". Uses the Reflection API.
97
+ *
98
+ * @return void
99
+ */
100
+ function set_magic_hooks(){
101
+ $class = new ReflectionClass(get_class($this));
102
+ $methods = $class->getMethods();
103
+
104
+ foreach ($methods as $method){
105
+ //Check if the method name starts with "hook_"
106
+ if (strpos($method->name, 'hook_') === 0){
107
+ //Get the hook's tag from the method name
108
+ $hook = substr($method->name, 5);
109
+ //Add the hook. Uses add_filter because add_action is simply a wrapper of the same.
110
+ add_filter($hook, array(&$this, $method->name),
111
+ $this->magic_hook_priority, $method->getNumberOfParameters());
112
+ }
113
+ }
114
+
115
+ unset($class);
116
+ }
117
+
118
+ /**
119
+ * ShadowPluginFramework::save_options()
120
+ * Saves the $options array to the database.
121
+ *
122
+ * @return void
123
+ */
124
+ function save_options(){
125
+ if ($this->option_name)
126
+ update_option($this->option_name, $this->options);
127
+ }
128
+
129
+ /**
130
+ * ShadowPluginFramework::activate()
131
+ * Stub function for the activation hook. Simply stores the default configuration.
132
+ *
133
+ * @return void
134
+ */
135
+ function activate(){
136
+ $this->save_options();
137
+ }
138
+
139
+ /**
140
+ * ShadowPluginFramework::deactivate()
141
+ * Stub function for the deactivation hook. Does nothing.
142
+ *
143
+ * @return void
144
+ */
145
+ function deactivate(){
146
+
147
+ }
148
+
149
+ /**
150
+ * ShadowPluginFramework::plugin_action_links()
151
+ * Adds a "Settings" link to the plugin's action links. Default handler for the 'plugin_action_links' hook.
152
+ *
153
+ * @param array $links
154
+ * @param string $file
155
+ * @return array
156
+ */
157
+ function plugin_action_links($links, $file) {
158
+ if ($file == $this->plugin_basename)
159
+ $links[] = "<a href='" . $this->settings_link . "'>" . __('Settings') . "</a>";
160
+ return $links;
161
+ }
162
+
163
+ /**
164
+ * ShadowPluginFramework::uninstall()
165
+ * Default uninstaller. Removes the plugins configuration record (if available).
166
+ *
167
+ * @return void
168
+ */
169
+ function uninstall(){
170
+ if ($this->option_name)
171
+ delete_option($this->option_name);
172
+ }
173
+
174
+ }
175
+
176
+ ?>
uninstall.php ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * @author W-Shadow
5
+ * @copyright 2009
6
+ *
7
+ * The uninstallation script.
8
+ */
9
+
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
+ ?>