Posts 2 Posts - Version 1.5

Version Description

  • added admin dropdowns
  • fixed SQL error related to user connections
  • fixed 'labels' handling and added 'column_title' subkey
  • refactor metabox JavaScript using Backbone.js
  • lazy-load connection candidates, for faster page loads
  • lazy-load PHP classes using spl_register_autoload()
Download this release

Release Info

Developer scribu
Plugin Icon wp plugin Posts 2 Posts
Version 1.5
Comparing to
See all releases

Code changes from version 1.4.3 to 1.5

Files changed (60) hide show
  1. admin/box-factory.php +10 -26
  2. admin/box.css +7 -10
  3. admin/box.js +360 -234
  4. admin/box.php +25 -15
  5. admin/column-factory.php +6 -39
  6. admin/column-post.php +34 -0
  7. admin/column-user.php +46 -0
  8. admin/column.php +25 -70
  9. admin/dropdown-factory.php +19 -0
  10. admin/dropdown-post.php +17 -0
  11. admin/dropdown-user.php +37 -0
  12. admin/dropdown.php +61 -0
  13. admin/factory.php +49 -9
  14. admin/field-create.php +25 -0
  15. admin/field-delete.php +23 -0
  16. admin/field-generic.php +27 -0
  17. admin/field-order.php +23 -0
  18. admin/field-title-attachment.php +13 -0
  19. admin/field-title-post.php +22 -0
  20. admin/field-title-user.php +11 -0
  21. admin/field-title.php +26 -0
  22. admin/field.php +7 -0
  23. admin/fields.php +0 -159
  24. admin/mustache.js +610 -0
  25. admin/templates/tab-list.html +1 -1
  26. admin/{tools.php → tools-page.php} +0 -2
  27. autoload.php +35 -0
  28. command.php +1 -2
  29. core/api.php +40 -58
  30. core/{type-factory.php → connection-type-factory.php} +0 -2
  31. core/{type.php → connection-type.php} +6 -38
  32. core/{directed-type.php → directed-connection-type.php} +0 -0
  33. core/indeterminate-connection-type.php +17 -0
  34. core/{indeterminate-type.php → indeterminate-directed-connection-type.php} +0 -0
  35. core/item-any.php +19 -0
  36. core/item-attachment.php +12 -0
  37. core/item-post.php +17 -0
  38. core/item-user.php +41 -0
  39. core/item.php +0 -82
  40. core/list.php +12 -14
  41. core/query-post.php +1 -3
  42. core/query-user.php +1 -3
  43. core/reciprocal-connection-type.php +18 -0
  44. core/shortcodes.php +35 -0
  45. core/side-attachment.php +23 -0
  46. core/side-post.php +138 -0
  47. core/side-user.php +122 -0
  48. core/side.php +0 -277
  49. core/storage.php +0 -2
  50. core/url-query.php +0 -24
  51. core/util.php +75 -0
  52. core/{extra.php → widget.php} +0 -84
  53. posts-to-posts.php +24 -21
  54. readme.txt +10 -3
  55. scb/AdminPage.php +223 -143
  56. scb/BoxesPage.php +38 -18
  57. scb/Cron.php +55 -41
  58. scb/Forms.php +370 -120
  59. scb/PostMetabox.php +182 -0
  60. scb/load.php +2 -2
admin/box-factory.php CHANGED
@@ -4,22 +4,18 @@ define( 'P2P_BOX_NONCE', 'p2p-box' );
4
 
5
  class P2P_Box_Factory extends P2P_Factory {
6
 
 
 
7
  function __construct() {
8
- add_action( 'p2p_registered_connection_type', array( $this, 'filter_ctypes' ), 10, 2 );
9
 
10
- add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
11
  add_action( 'save_post', array( $this, 'save_post' ), 10, 2 );
12
  add_action( 'wp_ajax_p2p_box', array( $this, 'wp_ajax_p2p_box' ) );
13
  }
14
 
15
- function filter_ctypes( $ctype, $args ) {
16
- if ( isset( $args['admin_box'] ) ) {
17
- $box_args = _p2p_pluck( $args, 'admin_box' );
18
- if ( !is_array( $box_args ) )
19
- $box_args = array( 'show' => $box_args );
20
- } else {
21
- $box_args = array();
22
- }
23
 
24
  foreach ( array( 'can_create_post' ) as $key ) {
25
  if ( isset( $args[ $key ] ) ) {
@@ -28,18 +24,15 @@ class P2P_Box_Factory extends P2P_Factory {
28
  }
29
 
30
  $box_args = wp_parse_args( $box_args, array(
31
- 'show' => 'any',
32
  'context' => 'side',
33
  'priority' => 'default',
34
  'can_create_post' => true
35
  ) );
36
 
37
- $this->register( $ctype->name, $box_args );
38
-
39
- return $args;
40
  }
41
 
42
- function add_meta_boxes( $post_type ) {
43
  $this->filter( 'post', $post_type );
44
  }
45
 
@@ -100,7 +93,7 @@ class P2P_Box_Factory extends P2P_Factory {
100
  if ( isset( $_POST['p2p_connections'] ) ) {
101
  // Loop through the hidden fields instead of through $_POST['p2p_meta'] because empty checkboxes send no data.
102
  foreach ( $_POST['p2p_connections'] as $p2p_id ) {
103
- $data = stripslashes_deep( scbForms::get_value( array( 'p2p_meta', $p2p_id ), $_POST, array() ) );
104
 
105
  $connection = p2p_get_connection( $p2p_id );
106
 
@@ -112,7 +105,7 @@ class P2P_Box_Factory extends P2P_Factory {
112
 
113
  $data = scbForms::validate_post_data( $fields, $data );
114
 
115
- scbForms::update_meta( $fields, self::addslashes_deep( $data ), $p2p_id, 'p2p' );
116
  }
117
  }
118
 
@@ -126,13 +119,6 @@ class P2P_Box_Factory extends P2P_Factory {
126
  }
127
  }
128
 
129
- private function addslashes_deep( $value ) {
130
- if ( is_array( $value ) )
131
- return array_map( array( __CLASS__, __METHOD__ ), $value );
132
-
133
- return addslashes( $value );
134
- }
135
-
136
  /**
137
  * Controller for all box ajax requests.
138
  */
@@ -162,5 +148,3 @@ class P2P_Box_Factory extends P2P_Factory {
162
  }
163
  }
164
 
165
- new P2P_Box_Factory;
166
-
4
 
5
  class P2P_Box_Factory extends P2P_Factory {
6
 
7
+ protected $key = 'admin_box';
8
+
9
  function __construct() {
10
+ parent::__construct();
11
 
12
+ add_action( 'add_meta_boxes', array( $this, 'add_items' ) );
13
  add_action( 'save_post', array( $this, 'save_post' ), 10, 2 );
14
  add_action( 'wp_ajax_p2p_box', array( $this, 'wp_ajax_p2p_box' ) );
15
  }
16
 
17
+ function expand_arg( $args ) {
18
+ $box_args = parent::expand_arg( $args );
 
 
 
 
 
 
19
 
20
  foreach ( array( 'can_create_post' ) as $key ) {
21
  if ( isset( $args[ $key ] ) ) {
24
  }
25
 
26
  $box_args = wp_parse_args( $box_args, array(
 
27
  'context' => 'side',
28
  'priority' => 'default',
29
  'can_create_post' => true
30
  ) );
31
 
32
+ return $box_args;
 
 
33
  }
34
 
35
+ function add_items( $post_type ) {
36
  $this->filter( 'post', $post_type );
37
  }
38
 
93
  if ( isset( $_POST['p2p_connections'] ) ) {
94
  // Loop through the hidden fields instead of through $_POST['p2p_meta'] because empty checkboxes send no data.
95
  foreach ( $_POST['p2p_connections'] as $p2p_id ) {
96
+ $data = scbForms::get_value( array( 'p2p_meta', $p2p_id ), $_POST, array() );
97
 
98
  $connection = p2p_get_connection( $p2p_id );
99
 
105
 
106
  $data = scbForms::validate_post_data( $fields, $data );
107
 
108
+ scbForms::update_meta( $fields, $data, $p2p_id, 'p2p' );
109
  }
110
  }
111
 
119
  }
120
  }
121
 
 
 
 
 
 
 
 
122
  /**
123
  * Controller for all box ajax requests.
124
  */
148
  }
149
  }
150
 
 
 
admin/box.css CHANGED
@@ -183,13 +183,12 @@ tr:hover td.p2p-col-order {
183
  .p2p-navigation div {
184
  float: left;
185
  line-height: 13px;
186
- padding-top: 3px;
187
- padding-bottom: 3px;
188
- margin-right: 5px;
189
  }
190
 
191
  .p2p-navigation .p2p-spinner {
192
- margin-top: 3px;
 
193
  }
194
 
195
 
@@ -209,6 +208,10 @@ body.rtl .p2p-tab-search input {
209
  padding-right: 0;
210
  }
211
 
 
 
 
 
212
  body.rtl .p2p-col-create,
213
  body.rtl .p2p-col-delete {
214
  border-left: 1px solid transparent;
@@ -219,9 +222,3 @@ body.rtl .p2p-col-create:hover,
219
  body.rtl .p2p-col-delete:hover {
220
  border-left-color: #ddd;
221
  }
222
-
223
- body.rtl .p2p-navigation div {
224
- margin-right: 0;
225
- margin-left: 5px;
226
- }
227
-
183
  .p2p-navigation div {
184
  float: left;
185
  line-height: 13px;
186
+ padding: 6px 5px 0;
 
 
187
  }
188
 
189
  .p2p-navigation .p2p-spinner {
190
+ margin-top: 5px;
191
+ margin-left: 5px;
192
  }
193
 
194
 
208
  padding-right: 0;
209
  }
210
 
211
+ body.rtl .p2p-navigation .p2p-spinner {
212
+ margin-right: 5px;
213
+ }
214
+
215
  body.rtl .p2p-col-create,
216
  body.rtl .p2p-col-delete {
217
  border-left: 1px solid transparent;
222
  body.rtl .p2p-col-delete:hover {
223
  border-left-color: #ddd;
224
  }
 
 
 
 
 
 
admin/box.js CHANGED
@@ -1,4 +1,318 @@
 
1
  (function() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  jQuery(function() {
4
  var clearVal, setVal;
@@ -23,259 +337,71 @@
23
  };
24
  jQuery('.p2p-search input[placeholder]').each(setVal).focus(clearVal).blur(setVal);
25
  }
 
26
  return jQuery('.p2p-box').each(function() {
27
- var $connections, $createButton, $createInput, $metabox, $searchInput, $spinner, PostsTab, ajax_request, append_connection, clear_connections, create_connection, delete_connection, refresh_candidates, remove_row, row_ajax_request, searchTab, switch_to_tab, toggle_tabs;
28
  $metabox = jQuery(this);
29
- $connections = $metabox.find('.p2p-connections');
30
  $spinner = jQuery('<img>', {
31
  'src': P2PAdmin.spinner,
32
  'class': 'p2p-spinner'
33
  });
34
- ajax_request = function(data, callback, type) {
35
- var handler;
36
- if (type == null) {
37
- type = 'POST';
38
- }
39
- jQuery.extend(data, {
 
 
 
 
 
 
 
40
  action: 'p2p_box',
41
- nonce: P2PAdmin.nonce,
42
- p2p_type: $metabox.data('p2p_type'),
43
- direction: $metabox.data('direction'),
44
- from: jQuery('#post_ID').val(),
45
- s: searchTab.params.s,
46
- paged: searchTab.params.paged
47
  });
48
- handler = function(response) {
49
  try {
50
  response = jQuery.parseJSON(response);
51
- if (response.error) {
52
- return alert(response.error);
53
- } else {
54
- return callback(response);
55
- }
56
  } catch (e) {
57
- return typeof console !== "undefined" && console !== null ? console.error('Malformed response', response) : void 0;
58
- }
59
- };
60
- return jQuery.ajax({
61
- type: type,
62
- url: ajaxurl,
63
- data: data,
64
- success: handler
65
- });
66
- };
67
- PostsTab = (function() {
68
-
69
- function PostsTab(selector) {
70
- var _this = this;
71
- this.tab = $metabox.find(selector);
72
- this.params = {
73
- subaction: 'search',
74
- s: ''
75
- };
76
- this.init_pagination_data();
77
- this.tab.delegate('.p2p-prev, .p2p-next', 'click', function(ev) {
78
- return _this.change_page(ev.target);
79
- });
80
- }
81
-
82
- PostsTab.prototype.init_pagination_data = function() {
83
- this.params.paged = this.tab.find('.p2p-current').data('num') || 1;
84
- return this.total_pages = this.tab.find('.p2p-total').data('num') || 1;
85
- };
86
-
87
- PostsTab.prototype.change_page = function(button) {
88
- var $navButton, new_page;
89
- $navButton = jQuery(button);
90
- new_page = this.params.paged;
91
- if ($navButton.hasClass('inactive')) {
92
  return;
93
  }
94
- if ($navButton.hasClass('p2p-prev')) {
95
- new_page--;
96
  } else {
97
- new_page++;
98
- }
99
- $spinner.appendTo(this.tab.find('.p2p-navigation'));
100
- return this.find_posts(new_page);
101
- };
102
-
103
- PostsTab.prototype.find_posts = function(new_page) {
104
- var _this = this;
105
- if ((0 < new_page && new_page <= this.total_pages)) {
106
- this.params.paged = new_page;
107
- }
108
- return ajax_request(this.params, function(response) {
109
- return _this.update_rows(response);
110
- }, 'GET');
111
- };
112
-
113
- PostsTab.prototype.update_rows = function(response) {
114
- $spinner.remove();
115
- this.tab.find('button, .p2p-results, .p2p-navigation, .p2p-notice').remove();
116
- this.tab.append(response.rows);
117
- return this.init_pagination_data();
118
- };
119
-
120
- return PostsTab;
121
-
122
- })();
123
- searchTab = new PostsTab('.p2p-tab-search');
124
- row_ajax_request = function($td, data, callback) {
125
- $td.find('.p2p-icon').css('background-image', 'url(' + P2PAdmin.spinner + ')');
126
- return ajax_request(data, callback);
127
- };
128
- remove_row = function($td) {
129
- var $table;
130
- $table = $td.closest('table');
131
- $td.closest('tr').remove();
132
- if (!$table.find('tbody tr').length) {
133
- return $table.hide();
134
- }
135
- };
136
- append_connection = function(response) {
137
- $connections.show().find('tbody').append(response.row);
138
- if ('one' === $metabox.data('cardinality')) {
139
- return $metabox.find('.p2p-create-connections').hide();
140
- }
141
- };
142
- refresh_candidates = function(results) {
143
- $metabox.find('.p2p-create-connections').show();
144
- return searchTab.update_rows(results);
145
- };
146
- clear_connections = function(ev) {
147
- var $td, data,
148
- _this = this;
149
- ev.preventDefault();
150
- if (!confirm(P2PAdmin.deleteConfirmMessage)) {
151
- return;
152
- }
153
- $td = jQuery(ev.target).closest('td');
154
- data = {
155
- subaction: 'clear_connections'
156
- };
157
- row_ajax_request($td, data, function(response) {
158
- $connections.hide().find('tbody').html('');
159
- return refresh_candidates(response);
160
- });
161
- return null;
162
- };
163
- delete_connection = function(ev) {
164
- var $td, data,
165
- _this = this;
166
- ev.preventDefault();
167
- $td = jQuery(ev.target).closest('td');
168
- data = {
169
- subaction: 'disconnect',
170
- p2p_id: $td.find('input').val()
171
- };
172
- row_ajax_request($td, data, function(response) {
173
- remove_row($td);
174
- return refresh_candidates(response);
175
- });
176
- return null;
177
- };
178
- create_connection = function(ev) {
179
- var $td, data,
180
- _this = this;
181
- ev.preventDefault();
182
- $td = jQuery(ev.target).closest('td');
183
- data = {
184
- subaction: 'connect',
185
- to: $td.find('div').data('item-id')
186
- };
187
- row_ajax_request($td, data, function(response) {
188
- append_connection(response);
189
- if ($metabox.data('duplicate_connections')) {
190
- return $td.find('.p2p-icon').css('background-image', '');
191
- } else {
192
- return remove_row($td);
193
  }
194
  });
195
- return null;
196
  };
197
- toggle_tabs = function(ev) {
198
- ev.preventDefault();
199
- $metabox.find('.p2p-create-connections-tabs').toggle();
200
- return null;
201
- };
202
- switch_to_tab = function(ev) {
203
- var $tab;
204
- ev.preventDefault();
205
- $tab = jQuery(this);
206
- $metabox.find('.wp-tab-bar li').removeClass('wp-tab-active');
207
- $tab.addClass('wp-tab-active');
208
- return $metabox.find('.tabs-panel').hide().end().find($tab.data('ref')).show().find(':text').focus();
209
- };
210
- $metabox.delegate('th.p2p-col-delete .p2p-icon', 'click', clear_connections).delegate('td.p2p-col-delete .p2p-icon', 'click', delete_connection).delegate('td.p2p-col-create div', 'click', create_connection).delegate('.p2p-toggle-tabs', 'click', toggle_tabs).delegate('.wp-tab-bar li', 'click', switch_to_tab);
211
- if ($connections.find('th.p2p-col-order').length) {
212
- $connections.find('tbody').sortable({
213
- handle: 'td.p2p-col-order',
214
- helper: function(e, ui) {
215
- ui.children().each(function() {
216
- var $this;
217
- $this = jQuery(this);
218
- return $this.width($this.width());
219
- });
220
- return ui;
221
- }
222
- });
223
- }
224
- $searchInput = $metabox.find('.p2p-tab-search :text');
225
- $searchInput.keypress(function(ev) {
226
- if (ev.keyCode === 13) {
227
- ev.preventDefault();
228
- }
229
- return null;
230
- }).keyup(function(ev) {
231
- var delayed;
232
- if (delayed !== void 0) {
233
- clearTimeout(delayed);
234
- }
235
- delayed = setTimeout(function() {
236
- var searchStr;
237
- searchStr = $searchInput.val();
238
- if (searchStr === searchTab.params.s) {
239
- return;
240
- }
241
- searchTab.params.s = searchStr;
242
- $spinner.insertAfter($searchInput).show();
243
- return searchTab.find_posts(1);
244
- }, 400);
245
- return null;
246
  });
247
- $createButton = $metabox.find('.p2p-tab-create-post button');
248
- $createInput = $metabox.find('.p2p-tab-create-post :text');
249
- $createButton.click(function(ev) {
250
- var $button, data, title;
251
- ev.preventDefault();
252
- $button = jQuery(this);
253
- if ($button.hasClass('inactive')) {
254
- return;
255
- }
256
- title = $createInput.val();
257
- if (title === '') {
258
- $createInput.focus();
259
- return;
260
- }
261
- $button.addClass('inactive');
262
- data = {
263
- subaction: 'create_post',
264
- post_title: title
265
- };
266
- ajax_request(data, function(response) {
267
- append_connection(response);
268
- $createInput.val('');
269
- return $button.removeClass('inactive');
270
- });
271
- return null;
272
  });
273
- return $createInput.keypress(function(ev) {
274
- if (13 === ev.keyCode) {
275
- $createButton.click();
276
- ev.preventDefault();
277
- }
278
- return null;
 
 
 
 
279
  });
280
  });
281
  });
1
+ // Generated by CoffeeScript 1.4.0
2
  (function() {
3
+ var Candidates, CandidatesView, Connections, ConnectionsView, CreatePostView, ENTER_KEY, MetaboxView, get_mustache_template, remove_row, row_wait;
4
+
5
+ ENTER_KEY = 13;
6
+
7
+ row_wait = function($td) {
8
+ return $td.find('.p2p-icon').css('background-image', 'url(' + P2PAdmin.spinner + ')');
9
+ };
10
+
11
+ remove_row = function($td) {
12
+ var $table;
13
+ $table = $td.closest('table');
14
+ $td.closest('tr').remove();
15
+ if (!$table.find('tbody tr').length) {
16
+ return $table.hide();
17
+ }
18
+ };
19
+
20
+ get_mustache_template = function(name) {
21
+ return jQuery('#p2p-template-' + name).html();
22
+ };
23
+
24
+ Candidates = Backbone.Model.extend({
25
+ sync: function() {
26
+ var params,
27
+ _this = this;
28
+ params = _.extend({}, this.attributes, {
29
+ subaction: 'search'
30
+ });
31
+ return this.ajax_request(params, function(response) {
32
+ var _ref;
33
+ _this.total_pages = ((_ref = response.navigation) != null ? _ref['total-pages-raw'] : void 0) || 1;
34
+ return _this.trigger('sync', response);
35
+ });
36
+ },
37
+ validate: function(attrs) {
38
+ var _ref;
39
+ if ((0 < (_ref = attrs['paged']) && _ref <= this.total_pages)) {
40
+ return null;
41
+ }
42
+ return 'invalid page';
43
+ }
44
+ });
45
+
46
+ Connections = Backbone.Model.extend({
47
+ createItemAndConnect: function(title) {
48
+ var data,
49
+ _this = this;
50
+ data = {
51
+ subaction: 'create_post',
52
+ post_title: title
53
+ };
54
+ return this.ajax_request(data, function(response) {
55
+ return _this.trigger('create:from_new_item', response);
56
+ });
57
+ },
58
+ create: function($td) {
59
+ var data,
60
+ _this = this;
61
+ data = {
62
+ subaction: 'connect',
63
+ to: $td.find('div').data('item-id')
64
+ };
65
+ return this.ajax_request(data, function(response) {
66
+ return _this.trigger('create', response, $td);
67
+ });
68
+ },
69
+ "delete": function($td) {
70
+ var data,
71
+ _this = this;
72
+ data = {
73
+ subaction: 'disconnect',
74
+ p2p_id: $td.find('input').val()
75
+ };
76
+ return this.ajax_request(data, function(response) {
77
+ return _this.trigger('delete', response, $td);
78
+ });
79
+ },
80
+ clear: function() {
81
+ var data,
82
+ _this = this;
83
+ data = {
84
+ subaction: 'clear_connections'
85
+ };
86
+ return this.ajax_request(data, function(response) {
87
+ return _this.trigger('clear', response);
88
+ });
89
+ }
90
+ });
91
+
92
+ ConnectionsView = Backbone.View.extend({
93
+ events: {
94
+ 'click th.p2p-col-delete .p2p-icon': 'clear',
95
+ 'click td.p2p-col-delete .p2p-icon': 'delete'
96
+ },
97
+ initialize: function(options) {
98
+ this.ajax_request = options.ajax_request;
99
+ this.maybe_make_sortable();
100
+ this.collection.on('create', this.afterCreate, this);
101
+ this.collection.on('create:from_new_item', this.afterCreate, this);
102
+ this.collection.on('delete', this.afterDelete, this);
103
+ this.collection.on('clear', this.afterClear, this);
104
+ return options.candidates.on('promote', this.create, this);
105
+ },
106
+ maybe_make_sortable: function() {
107
+ if (this.$('th.p2p-col-order').length) {
108
+ return this.$('tbody').sortable({
109
+ handle: 'td.p2p-col-order',
110
+ helper: function(e, ui) {
111
+ ui.children().each(function() {
112
+ var $this;
113
+ $this = jQuery(this);
114
+ return $this.width($this.width());
115
+ });
116
+ return ui;
117
+ }
118
+ });
119
+ }
120
+ },
121
+ clear: function(ev) {
122
+ var $td;
123
+ ev.preventDefault();
124
+ if (!confirm(P2PAdmin.deleteConfirmMessage)) {
125
+ return;
126
+ }
127
+ $td = jQuery(ev.target).closest('td');
128
+ row_wait($td);
129
+ return this.collection.clear();
130
+ },
131
+ afterClear: function() {
132
+ return this.$el.hide().find('tbody').html('');
133
+ },
134
+ "delete": function(ev) {
135
+ var $td;
136
+ ev.preventDefault();
137
+ $td = jQuery(ev.target).closest('td');
138
+ row_wait($td);
139
+ this.collection["delete"]($td);
140
+ return null;
141
+ },
142
+ afterDelete: function(response, $td) {
143
+ return remove_row($td);
144
+ },
145
+ create: function($td) {
146
+ this.collection.create($td);
147
+ return null;
148
+ },
149
+ afterCreate: function(response) {
150
+ this.$el.show().find('tbody').append(response.row);
151
+ return this.collection.trigger('append', response);
152
+ }
153
+ });
154
+
155
+ CandidatesView = Backbone.View.extend({
156
+ template: Mustache.compile(get_mustache_template('tab-list')),
157
+ events: {
158
+ 'keypress :text': 'handleReturn',
159
+ 'keyup :text': 'handleSearch',
160
+ 'click .p2p-prev, .p2p-next': 'changePage',
161
+ 'click td.p2p-col-create div': 'promote'
162
+ },
163
+ initialize: function(options) {
164
+ this.spinner = options.spinner;
165
+ options.connections.on('create', this.afterConnectionCreated, this);
166
+ options.connections.on('delete', this.refreshCandidates, this);
167
+ options.connections.on('clear', this.refreshCandidates, this);
168
+ this.collection.on('sync', this.refreshCandidates, this);
169
+ this.collection.on('error', this.afterInvalid, this);
170
+ return this.collection.on('invalid', this.afterInvalid, this);
171
+ },
172
+ afterConnectionCreated: function(response, $td) {
173
+ if (this.options.duplicate_connections) {
174
+ return $td.find('.p2p-icon').css('background-image', '');
175
+ } else {
176
+ return remove_row($td);
177
+ }
178
+ },
179
+ promote: function(ev) {
180
+ var $td;
181
+ $td = jQuery(ev.target).closest('td');
182
+ row_wait($td);
183
+ this.collection.trigger('promote', $td);
184
+ return false;
185
+ },
186
+ handleReturn: function(ev) {
187
+ if (ev.keyCode === ENTER_KEY) {
188
+ ev.preventDefault();
189
+ }
190
+ return null;
191
+ },
192
+ handleSearch: function(ev) {
193
+ var $searchInput, delayed,
194
+ _this = this;
195
+ if (delayed !== void 0) {
196
+ clearTimeout(delayed);
197
+ }
198
+ $searchInput = jQuery(ev.target);
199
+ delayed = setTimeout(function() {
200
+ var searchStr;
201
+ searchStr = $searchInput.val();
202
+ if (searchStr === _this.collection.get('s')) {
203
+ return;
204
+ }
205
+ _this.spinner.insertAfter(_this.searchInput).show();
206
+ return _this.collection.save({
207
+ 's': searchStr,
208
+ 'paged': 1
209
+ });
210
+ }, 400);
211
+ return null;
212
+ },
213
+ changePage: function(ev) {
214
+ var $navButton, new_page;
215
+ $navButton = jQuery(ev.currentTarget);
216
+ new_page = this.collection.get('paged');
217
+ if ($navButton.hasClass('p2p-prev')) {
218
+ new_page--;
219
+ } else {
220
+ new_page++;
221
+ }
222
+ this.spinner.appendTo(this.$('.p2p-navigation'));
223
+ return this.collection.save('paged', new_page);
224
+ },
225
+ refreshCandidates: function(response) {
226
+ this.spinner.remove();
227
+ this.$('button, .p2p-results, .p2p-navigation, .p2p-notice').remove();
228
+ return this.$el.append(this.template(response));
229
+ },
230
+ afterInvalid: function() {
231
+ return this.spinner.remove();
232
+ }
233
+ });
234
+
235
+ CreatePostView = Backbone.View.extend({
236
+ events: {
237
+ 'click button': 'createItem',
238
+ 'keypress :text': 'handleReturn'
239
+ },
240
+ initialize: function(options) {
241
+ this.ajax_request = options.ajax_request;
242
+ this.createButton = this.$('button');
243
+ this.createInput = this.$(':text');
244
+ return this.collection.on('create:from_new_item', this.afterItemCreated, this);
245
+ },
246
+ handleReturn: function(ev) {
247
+ if (ev.keyCode === ENTER_KEY) {
248
+ this.createButton.click();
249
+ ev.preventDefault();
250
+ }
251
+ return null;
252
+ },
253
+ createItem: function(ev) {
254
+ var title;
255
+ ev.preventDefault();
256
+ if (this.createButton.hasClass('inactive')) {
257
+ return false;
258
+ }
259
+ title = this.createInput.val();
260
+ if (title === '') {
261
+ this.createInput.focus();
262
+ return;
263
+ }
264
+ this.createButton.addClass('inactive');
265
+ this.collection.createItemAndConnect(title);
266
+ return null;
267
+ },
268
+ afterItemCreated: function() {
269
+ this.createInput.val('');
270
+ return this.createButton.removeClass('inactive');
271
+ }
272
+ });
273
+
274
+ MetaboxView = Backbone.View.extend({
275
+ events: {
276
+ 'click .p2p-toggle-tabs': 'toggleTabs',
277
+ 'click .wp-tab-bar li': 'setActiveTab'
278
+ },
279
+ initialize: function(options) {
280
+ this.spinner = options.spinner;
281
+ this.initializedCandidates = false;
282
+ options.connections.on('append', this.afterConnectionAppended, this);
283
+ options.connections.on('clear', this.afterConnectionDeleted, this);
284
+ return options.connections.on('delete', this.afterConnectionDeleted, this);
285
+ },
286
+ toggleTabs: function(ev) {
287
+ var $tabs;
288
+ ev.preventDefault();
289
+ $tabs = this.$('.p2p-create-connections-tabs');
290
+ $tabs.toggle();
291
+ if (!this.initializedCandidates && $tabs.is(':visible')) {
292
+ this.options.candidates.sync();
293
+ this.initializedCandidates = true;
294
+ }
295
+ return null;
296
+ },
297
+ setActiveTab: function(ev) {
298
+ var $tab;
299
+ ev.preventDefault();
300
+ $tab = jQuery(ev.currentTarget);
301
+ this.$('.wp-tab-bar li').removeClass('wp-tab-active');
302
+ $tab.addClass('wp-tab-active');
303
+ return this.$el.find('.tabs-panel').hide().end().find($tab.data('ref')).show().find(':text').focus();
304
+ },
305
+ afterConnectionAppended: function(response) {
306
+ if ('one' === this.options.cardinality) {
307
+ return this.$('.p2p-create-connections').hide();
308
+ }
309
+ },
310
+ afterConnectionDeleted: function(response) {
311
+ if ('one' === this.options.cardinality) {
312
+ return this.$('.p2p-create-connections').show();
313
+ }
314
+ }
315
+ });
316
 
317
  jQuery(function() {
318
  var clearVal, setVal;
337
  };
338
  jQuery('.p2p-search input[placeholder]').each(setVal).focus(clearVal).blur(setVal);
339
  }
340
+ Mustache.compilePartial('table-row', get_mustache_template('table-row'));
341
  return jQuery('.p2p-box').each(function() {
342
+ var $metabox, $spinner, ajax_request, candidates, candidatesView, connections, connectionsView, createPostView, ctype, metaboxView;
343
  $metabox = jQuery(this);
 
344
  $spinner = jQuery('<img>', {
345
  'src': P2PAdmin.spinner,
346
  'class': 'p2p-spinner'
347
  });
348
+ candidates = new Candidates({
349
+ 's': '',
350
+ 'paged': 1
351
+ });
352
+ candidates.total_pages = $metabox.find('.p2p-total').data('num') || 1;
353
+ ctype = {
354
+ p2p_type: $metabox.data('p2p_type'),
355
+ direction: $metabox.data('direction'),
356
+ from: jQuery('#post_ID').val()
357
+ };
358
+ ajax_request = function(options, callback) {
359
+ var params;
360
+ params = _.extend({}, options, candidates.attributes, ctype, {
361
  action: 'p2p_box',
362
+ nonce: P2PAdmin.nonce
 
 
 
 
 
363
  });
364
+ return jQuery.post(ajaxurl, params, function(response) {
365
  try {
366
  response = jQuery.parseJSON(response);
 
 
 
 
 
367
  } catch (e) {
368
+ if (typeof console !== "undefined" && console !== null) {
369
+ console.error('Malformed response', response);
370
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  return;
372
  }
373
+ if (response.error) {
374
+ return alert(response.error);
375
  } else {
376
+ return callback(response);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  }
378
  });
 
379
  };
380
+ candidates.ajax_request = ajax_request;
381
+ connections = new Connections;
382
+ connections.ajax_request = ajax_request;
383
+ connectionsView = new ConnectionsView({
384
+ el: $metabox.find('.p2p-connections'),
385
+ collection: connections,
386
+ candidates: candidates
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  });
388
+ candidatesView = new CandidatesView({
389
+ el: $metabox.find('.p2p-tab-search'),
390
+ collection: candidates,
391
+ connections: connections,
392
+ spinner: $spinner,
393
+ duplicate_connections: $metabox.data('duplicate_connections')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  });
395
+ createPostView = new CreatePostView({
396
+ el: $metabox.find('.p2p-tab-create-post'),
397
+ collection: connections
398
+ });
399
+ return metaboxView = new MetaboxView({
400
+ el: $metabox,
401
+ spinner: $spinner,
402
+ cardinality: $metabox.data('cardinality'),
403
+ candidates: candidates,
404
+ connections: connections
405
  });
406
  });
407
  });
admin/box.php CHANGED
@@ -1,10 +1,5 @@
1
  <?php
2
 
3
- interface P2P_Field {
4
- function get_title();
5
- function render( $p2p_id, $item );
6
- }
7
-
8
  class P2P_Box {
9
  private $ctype;
10
 
@@ -35,9 +30,15 @@ class P2P_Box {
35
  if ( self::$enqueued_scripts )
36
  return;
37
 
38
- wp_enqueue_style( 'p2p-box', plugins_url( 'box.css', __FILE__ ), array(), P2P_PLUGIN_VERSION );
 
 
 
 
 
 
 
39
 
40
- wp_enqueue_script( 'p2p-box', plugins_url( 'box.js', __FILE__ ), array( 'jquery' ), P2P_PLUGIN_VERSION, true );
41
  wp_localize_script( 'p2p-box', 'P2PAdmin', array(
42
  'nonce' => wp_create_nonce( P2P_BOX_NONCE ),
43
  'spinner' => admin_url( 'images/wpspin_light.gif' ),
@@ -46,6 +47,19 @@ class P2P_Box {
46
 
47
  self::$enqueued_scripts = true;
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  }
50
 
51
  function render( $post ) {
@@ -115,7 +129,6 @@ class P2P_Box {
115
  // Search tab
116
  $tab_content = P2P_Mustache::render( 'tab-search', array(
117
  'placeholder' => $this->labels->search_items,
118
- 'candidates' => $this->post_rows( $post->ID )
119
  ) );
120
 
121
  $data['tabs'][] = array(
@@ -165,6 +178,7 @@ class P2P_Box {
165
 
166
  protected function post_rows( $current_post_id, $page = 1, $search = '' ) {
167
  $extra_qv = array_merge( self::$admin_box_qv, array(
 
168
  'p2p:search' => $search,
169
  'p2p:page' => $page,
170
  'p2p:per_page' => 5
@@ -191,7 +205,6 @@ class P2P_Box {
191
  'current-page' => number_format_i18n( $candidate->current_page ),
192
  'total-pages' => number_format_i18n( $candidate->total_pages ),
193
 
194
- 'current-page-raw' => $candidate->current_page,
195
  'total-pages-raw' => $candidate->total_pages,
196
 
197
  'prev-inactive' => ( 1 == $candidate->current_page ) ? 'inactive' : '',
@@ -203,7 +216,7 @@ class P2P_Box {
203
  );
204
  }
205
 
206
- return P2P_Mustache::render( 'tab-list', $data );
207
  }
208
 
209
 
@@ -280,11 +293,8 @@ class P2P_Box {
280
  }
281
 
282
  private function refresh_candidates() {
283
- $rows = $this->post_rows( $_REQUEST['from'], $_REQUEST['paged'], $_REQUEST['s'] );
284
-
285
- $results = compact( 'rows' );
286
-
287
- die( json_encode( $results ) );
288
  }
289
 
290
  protected function can_create_post() {
1
  <?php
2
 
 
 
 
 
 
3
  class P2P_Box {
4
  private $ctype;
5
 
30
  if ( self::$enqueued_scripts )
31
  return;
32
 
33
+ wp_enqueue_style( 'p2p-box', plugins_url( 'box.css', __FILE__ ),
34
+ array(), P2P_PLUGIN_VERSION );
35
+
36
+ wp_register_script( 'mustache', plugins_url( 'mustache.js', __FILE__ ),
37
+ array(), '0.7.2', true );
38
+
39
+ wp_enqueue_script( 'p2p-box', plugins_url( 'box.js', __FILE__ ),
40
+ array( 'backbone', 'mustache' ), P2P_PLUGIN_VERSION, true );
41
 
 
42
  wp_localize_script( 'p2p-box', 'P2PAdmin', array(
43
  'nonce' => wp_create_nonce( P2P_BOX_NONCE ),
44
  'spinner' => admin_url( 'images/wpspin_light.gif' ),
47
 
48
  self::$enqueued_scripts = true;
49
 
50
+ add_action( 'admin_footer', array( __CLASS__, 'add_templates' ) );
51
+ }
52
+
53
+ static function add_templates() {
54
+ self::add_template( 'tab-list' );
55
+ self::add_template( 'table-row' );
56
+ }
57
+
58
+ private static function add_template( $slug ) {
59
+ echo html( 'script', array(
60
+ 'type' => 'text/html',
61
+ 'id' => "p2p-template-$slug"
62
+ ), file_get_contents( dirname( __FILE__ ) . "/templates/$slug.html" ) );
63
  }
64
 
65
  function render( $post ) {
129
  // Search tab
130
  $tab_content = P2P_Mustache::render( 'tab-search', array(
131
  'placeholder' => $this->labels->search_items,
 
132
  ) );
133
 
134
  $data['tabs'][] = array(
178
 
179
  protected function post_rows( $current_post_id, $page = 1, $search = '' ) {
180
  $extra_qv = array_merge( self::$admin_box_qv, array(
181
+ 'p2p:context' => 'admin_box_candidates',
182
  'p2p:search' => $search,
183
  'p2p:page' => $page,
184
  'p2p:per_page' => 5
205
  'current-page' => number_format_i18n( $candidate->current_page ),
206
  'total-pages' => number_format_i18n( $candidate->total_pages ),
207
 
 
208
  'total-pages-raw' => $candidate->total_pages,
209
 
210
  'prev-inactive' => ( 1 == $candidate->current_page ) ? 'inactive' : '',
216
  );
217
  }
218
 
219
+ return $data;
220
  }
221
 
222
 
293
  }
294
 
295
  private function refresh_candidates() {
296
+ die( json_encode( $this->post_rows(
297
+ $_REQUEST['from'], $_REQUEST['paged'], $_REQUEST['s'] ) ) );
 
 
 
298
  }
299
 
300
  protected function can_create_post() {
admin/column-factory.php CHANGED
@@ -2,53 +2,20 @@
2
 
3
  class P2P_Column_Factory extends P2P_Factory {
4
 
5
- function __construct() {
6
- add_action( 'p2p_registered_connection_type', array( $this, 'filter_ctypes' ), 10, 2 );
7
-
8
- add_action( 'admin_print_styles', array( $this, 'add_columns' ) );
9
- }
10
-
11
- function filter_ctypes( $ctype, $args ) {
12
- if ( isset( $args['admin_column'] ) ) {
13
- $column_args = _p2p_pluck( $args, 'admin_column' );
14
- if ( !is_array( $column_args ) )
15
- $column_args = array( 'show' => $column_args );
16
- } else {
17
- $column_args = array();
18
- }
19
 
20
- $column_args = wp_parse_args( $column_args, array(
21
- 'show' => false,
22
- ) );
23
-
24
- $this->register( $ctype->name, $column_args );
25
-
26
- return $args;
27
- }
28
-
29
- function add_columns() {
30
- $screen = get_current_screen();
31
-
32
- $screen_map = array(
33
- 'edit' => 'post',
34
- 'users' => 'user'
35
- );
36
-
37
- if ( !isset( $screen_map[ $screen->base ] ) )
38
- return;
39
-
40
- $object_type = $screen_map[ $screen->base ];
41
 
42
- $this->filter( $object_type, $screen->post_type );
 
43
  }
44
 
45
  function add_item( $directed, $object_type, $post_type, $title ) {
46
  $class = 'P2P_Column_' . ucfirst( $object_type );
47
  $column = new $class( $directed );
48
 
49
- $column->styles();
50
  }
51
  }
52
 
53
- new P2P_Column_Factory;
54
-
2
 
3
  class P2P_Column_Factory extends P2P_Factory {
4
 
5
+ protected $key = 'admin_column';
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
+ function __construct() {
8
+ parent::__construct();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ add_action( 'load-edit.php', array( $this, 'add_items' ) );
11
+ add_action( 'load-users.php', array( $this, 'add_items' ) );
12
  }
13
 
14
  function add_item( $directed, $object_type, $post_type, $title ) {
15
  $class = 'P2P_Column_' . ucfirst( $object_type );
16
  $column = new $class( $directed );
17
 
18
+ add_action( 'admin_print_styles', array( $column, 'styles' ) );
19
  }
20
  }
21
 
 
 
admin/column-post.php ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Column_Post extends P2P_Column {
4
+
5
+ function __construct( $directed ) {
6
+ parent::__construct( $directed );
7
+
8
+ $screen = get_current_screen();
9
+
10
+ add_action( "manage_{$screen->post_type}_posts_custom_column", array( $this, 'display_column' ), 10, 2 );
11
+ }
12
+
13
+ protected function get_items() {
14
+ global $wp_query;
15
+
16
+ return $wp_query->posts;
17
+ }
18
+
19
+ function get_admin_link( $item ) {
20
+ $args = array(
21
+ 'connected_type' => $this->ctype->name,
22
+ 'connected_direction' => $this->ctype->flip_direction()->get_direction(),
23
+ 'connected_items' => $item->get_id(),
24
+ 'post_type' => get_current_screen()->post_type
25
+ );
26
+
27
+ return add_query_arg( $args, admin_url( 'edit.php' ) );
28
+ }
29
+
30
+ function display_column( $column, $item_id ) {
31
+ echo parent::render_column( $column, $item_id );
32
+ }
33
+ }
34
+
admin/column-user.php ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Column_User extends P2P_Column {
4
+
5
+ function __construct( $directed ) {
6
+ parent::__construct( $directed );
7
+
8
+ add_action( 'pre_user_query', array( __CLASS__, 'user_query' ), 9 );
9
+
10
+ add_filter( 'manage_users_custom_column', array( $this, 'display_column' ), 10, 3 );
11
+ }
12
+
13
+ protected function get_items() {
14
+ global $wp_list_table;
15
+
16
+ return $wp_list_table->items;
17
+ }
18
+
19
+ // Add the query vars to the global user query (on the user admin screen)
20
+ static function user_query( $query ) {
21
+ if ( isset( $query->_p2p_capture ) )
22
+ return;
23
+
24
+ // Don't overwrite existing P2P query
25
+ if ( isset( $query->query_vars['connected_type'] ) )
26
+ return;
27
+
28
+ _p2p_append( $query->query_vars, wp_array_slice_assoc( $_GET,
29
+ P2P_URL_Query::get_custom_qv() ) );
30
+ }
31
+
32
+ function get_admin_link( $item ) {
33
+ $args = array(
34
+ 'connected_type' => $this->ctype->name,
35
+ 'connected_direction' => $this->ctype->flip_direction()->get_direction(),
36
+ 'connected_items' => $item->get_id(),
37
+ );
38
+
39
+ return add_query_arg( $args, admin_url( 'users.php' ) );
40
+ }
41
+
42
+ function display_column( $content, $column, $item_id ) {
43
+ return parent::render_column( $column, $item_id );
44
+ }
45
+ }
46
+
admin/column.php CHANGED
@@ -6,34 +6,48 @@ abstract class P2P_Column {
6
 
7
  protected $connected = array();
8
 
9
- function __construct( $directed, $items ) {
10
  $this->ctype = $directed;
 
11
  $this->column_id = sprintf( 'p2p-%s-%s',
12
  $this->ctype->get_direction(),
13
  $this->ctype->name
14
  );
15
 
16
- $extra_qv = array(
17
- 'p2p:per_page' => -1,
18
- 'p2p:context' => 'admin_column'
19
- );
20
-
21
- $connected = $this->ctype->get_connected( $items, $extra_qv, 'abstract' );
22
-
23
- $this->connected = p2p_triage_connected( $connected->items );
24
-
25
  $screen = get_current_screen();
26
 
27
  add_filter( "manage_{$screen->id}_columns", array( $this, 'add_column' ) );
28
  }
29
 
30
  function add_column( $columns ) {
 
 
 
31
 
32
- $columns[ $this->column_id ] = $this->ctype->get( 'current', 'title' );
 
 
 
 
33
 
34
  return $columns;
35
  }
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  function styles() {
38
  ?>
39
  <style type="text/css">
@@ -64,62 +78,3 @@ abstract class P2P_Column {
64
  }
65
  }
66
 
67
-
68
- class P2P_Column_Post extends P2P_Column {
69
-
70
- function __construct( $directed ) {
71
- global $wp_query;
72
-
73
- $this->ctype = $directed;
74
-
75
- $extra_qv = array( 'p2p:context' => 'admin_column' );
76
-
77
- parent::__construct( $directed, $wp_query->posts );
78
-
79
- $screen = get_current_screen();
80
-
81
- add_action( "manage_{$screen->post_type}_posts_custom_column", array( $this, 'display_column' ), 10, 2 );
82
- }
83
-
84
- function get_admin_link( $item ) {
85
- $args = array(
86
- 'connected_type' => $this->ctype->name,
87
- 'connected_direction' => $this->ctype->flip_direction()->get_direction(),
88
- 'connected_items' => $item->get_id(),
89
- 'post_type' => get_current_screen()->post_type
90
- );
91
-
92
- return add_query_arg( $args, admin_url( 'edit.php' ) );
93
- }
94
-
95
- function display_column( $column, $item_id ) {
96
- echo parent::render_column( $column, $item_id );
97
- }
98
- }
99
-
100
-
101
- class P2P_Column_User extends P2P_Column {
102
-
103
- function __construct( $directed ) {
104
- global $wp_list_table;
105
-
106
- parent::__construct( $directed, $wp_list_table->items );
107
-
108
- add_filter( 'manage_users_custom_column', array( $this, 'display_column' ), 10, 3 );
109
- }
110
-
111
- function get_admin_link( $item ) {
112
- $args = array(
113
- 'connected_type' => $this->ctype->name,
114
- 'connected_direction' => $this->ctype->flip_direction()->get_direction(),
115
- 'connected_items' => $item->get_id(),
116
- );
117
-
118
- return add_query_arg( $args, admin_url( 'users.php' ) );
119
- }
120
-
121
- function display_column( $content, $column, $item_id ) {
122
- return parent::render_column( $column, $item_id );
123
- }
124
- }
125
-
6
 
7
  protected $connected = array();
8
 
9
+ function __construct( $directed ) {
10
  $this->ctype = $directed;
11
+
12
  $this->column_id = sprintf( 'p2p-%s-%s',
13
  $this->ctype->get_direction(),
14
  $this->ctype->name
15
  );
16
 
 
 
 
 
 
 
 
 
 
17
  $screen = get_current_screen();
18
 
19
  add_filter( "manage_{$screen->id}_columns", array( $this, 'add_column' ) );
20
  }
21
 
22
  function add_column( $columns ) {
23
+ $this->prepare_items();
24
+
25
+ $labels = $this->ctype->get( 'current', 'labels' );
26
 
27
+ $title = isset( $labels->column_title )
28
+ ? $labels->column_title
29
+ : $labels->title;
30
+
31
+ $columns[ $this->column_id ] = $title;
32
 
33
  return $columns;
34
  }
35
 
36
+ protected abstract function get_items();
37
+
38
+ protected function prepare_items() {
39
+ $items = $this->get_items();
40
+
41
+ $extra_qv = array(
42
+ 'p2p:per_page' => -1,
43
+ 'p2p:context' => 'admin_column'
44
+ );
45
+
46
+ $connected = $this->ctype->get_connected( $items, $extra_qv, 'abstract' );
47
+
48
+ $this->connected = p2p_list_cluster( $connected->items, '_p2p_get_other_id' );
49
+ }
50
+
51
  function styles() {
52
  ?>
53
  <style type="text/css">
78
  }
79
  }
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
admin/dropdown-factory.php ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Dropdown_Factory extends P2P_Factory {
4
+
5
+ protected $key = 'admin_dropdown';
6
+
7
+ function __construct() {
8
+ parent::__construct();
9
+
10
+ add_action( 'load-edit.php', array( $this, 'add_items' ) );
11
+ add_action( 'load-users.php', array( $this, 'add_items' ) );
12
+ }
13
+
14
+ function add_item( $directed, $object_type, $post_type, $title ) {
15
+ $class = 'P2P_Dropdown_' . ucfirst( $object_type );
16
+ $item = new $class( $directed, $title );
17
+ }
18
+ }
19
+
admin/dropdown-post.php ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Dropdown_Post extends P2P_Dropdown {
4
+
5
+ function __construct( $directed, $title ) {
6
+ parent::__construct( $directed, $title );
7
+
8
+ add_filter( 'request', array( __CLASS__, 'massage_query' ) );
9
+
10
+ add_action( 'restrict_manage_posts', array( $this, 'show_dropdown' ) );
11
+ }
12
+
13
+ static function massage_query( $request ) {
14
+ return array_merge( $request, self::get_qv() );
15
+ }
16
+ }
17
+
admin/dropdown-user.php ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Dropdown_User extends P2P_Dropdown_Post {
4
+
5
+ function __construct( $directed, $title ) {
6
+ parent::__construct( $directed, $title );
7
+
8
+ add_action( 'pre_user_query', array( __CLASS__, 'massage_query' ), 9 );
9
+
10
+ add_action( 'restrict_manage_users', array( $this, 'show_dropdown' ) );
11
+ }
12
+
13
+ static function massage_query( $query ) {
14
+ if ( isset( $query->_p2p_capture ) )
15
+ return;
16
+
17
+ // Don't overwrite existing P2P query
18
+ if ( isset( $query->query_vars['connected_type'] ) )
19
+ return;
20
+
21
+ _p2p_append( $query->query_vars, self::get_qv() );
22
+ }
23
+
24
+ protected function render_dropdown() {
25
+ return html( 'div', array(
26
+ 'style' => 'float: right; margin-left: 16px'
27
+ ),
28
+ parent::render_dropdown(),
29
+ html( 'input', array(
30
+ 'type' => 'submit',
31
+ 'class' => 'button',
32
+ 'value' => __( 'Filter', P2P_TEXTDOMAIN )
33
+ ) )
34
+ );
35
+ }
36
+ }
37
+
admin/dropdown.php ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ abstract class P2P_Dropdown {
4
+
5
+ protected $ctype;
6
+ protected $title;
7
+
8
+ function __construct( $directed, $title ) {
9
+ $this->ctype = $directed;
10
+ $this->title = $title;
11
+ }
12
+
13
+ function show_dropdown() {
14
+ echo $this->render_dropdown();
15
+ }
16
+
17
+ protected function render_dropdown() {
18
+ $direction = $this->ctype->flip_direction()->get_direction();
19
+
20
+ return scbForms::input( array(
21
+ 'type' => 'select',
22
+ 'name' => array( 'p2p', $this->ctype->name, $direction ),
23
+ 'choices' => self::get_choices( $this->ctype ),
24
+ 'text' => $this->title,
25
+ ), $_GET );
26
+ }
27
+
28
+ protected static function get_qv() {
29
+ if ( !isset( $_GET['p2p'] ) )
30
+ return array();
31
+
32
+ $args = array();
33
+
34
+ $tmp = reset( $_GET['p2p'] );
35
+
36
+ $args['connected_type'] = key( $_GET['p2p'] );
37
+
38
+ list( $args['connected_direction'], $args['connected_items'] ) = each( $tmp );
39
+
40
+ if ( !$args['connected_items'] )
41
+ return array();
42
+
43
+ return $args;
44
+ }
45
+
46
+ protected static function get_choices( $directed ) {
47
+ $extra_qv = array(
48
+ 'p2p:per_page' => -1,
49
+ 'p2p:context' => 'admin_dropdown'
50
+ );
51
+
52
+ $connected = $directed->get_connected( 'any', $extra_qv, 'abstract' );
53
+
54
+ $options = array();
55
+ foreach ( $connected->items as $item )
56
+ $options[ $item->get_id() ] = $item->get_title();
57
+
58
+ return $options;
59
+ }
60
+ }
61
+
admin/factory.php CHANGED
@@ -2,22 +2,61 @@
2
 
3
  abstract class P2P_Factory {
4
 
 
 
5
  protected $queue = array();
6
 
7
- function register( $p2p_type, $args ) {
8
- if ( isset( $this->queue[$p2p_type] ) )
9
- return false;
10
 
11
- $args = (object) $args;
 
 
12
 
13
- if ( !$args->show )
14
  return false;
15
 
16
- $this->queue[$p2p_type] = $args;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
- return true;
 
 
19
  }
20
 
 
21
  function filter( $object_type, $post_type ) {
22
  foreach ( $this->queue as $p2p_type => $args ) {
23
  $ctype = p2p_type( $p2p_type );
@@ -36,6 +75,9 @@ abstract class P2P_Factory {
36
  }
37
  }
38
 
 
 
 
39
  protected static function get_title( $directions, $ctype ) {
40
  $title = array(
41
  'from' => $ctype->get_field( 'title', 'from' ),
@@ -57,7 +99,5 @@ abstract class P2P_Factory {
57
 
58
  return $ctype->_directions_for_admin( $direction, $show_ui );
59
  }
60
-
61
- abstract function add_item( $directed, $object_type, $post_type, $title );
62
  }
63
 
2
 
3
  abstract class P2P_Factory {
4
 
5
+ protected $key;
6
+
7
  protected $queue = array();
8
 
9
+ function __construct() {
10
+ add_action( 'p2p_registered_connection_type', array( $this, 'check_ctype' ), 10, 2 );
11
+ }
12
 
13
+ // Check if a newly registered connection type needs an item to be produced.
14
+ function check_ctype( $ctype, $args ) {
15
+ $sub_args = $this->expand_arg( $args );
16
 
17
+ if ( !$sub_args['show'] )
18
  return false;
19
 
20
+ $this->queue[ $ctype->name ] = (object) $sub_args;
21
+ }
22
+
23
+ // Collect sub-args from main connection type args and set defaults
24
+ protected function expand_arg( $args ) {
25
+ if ( isset( $args[ $this->key ] ) ) {
26
+ $sub_args = $args[ $this->key ];
27
+
28
+ if ( !is_array( $sub_args ) ) {
29
+ $sub_args = array( 'show' => $sub_args );
30
+ }
31
+ } else {
32
+ $sub_args = array( 'show' => false );
33
+ }
34
+
35
+ $sub_args = wp_parse_args( $sub_args, array(
36
+ 'show' => 'any',
37
+ ) );
38
+
39
+ return $sub_args;
40
+ }
41
+
42
+ // Begin processing item queue for a particular screen.
43
+ function add_items() {
44
+ $screen = get_current_screen();
45
+
46
+ $screen_map = array(
47
+ 'edit' => 'post',
48
+ 'users' => 'user'
49
+ );
50
+
51
+ if ( !isset( $screen_map[ $screen->base ] ) )
52
+ return;
53
 
54
+ $object_type = $screen_map[ $screen->base ];
55
+
56
+ $this->filter( $object_type, $screen->post_type );
57
  }
58
 
59
+ // Filter item queue based on object type.
60
  function filter( $object_type, $post_type ) {
61
  foreach ( $this->queue as $p2p_type => $args ) {
62
  $ctype = p2p_type( $p2p_type );
75
  }
76
  }
77
 
78
+ // Produce an item and add it to the screen.
79
+ abstract function add_item( $directed, $object_type, $post_type, $title );
80
+
81
  protected static function get_title( $directions, $ctype ) {
82
  $title = array(
83
  'from' => $ctype->get_field( 'title', 'from' ),
99
 
100
  return $ctype->_directions_for_admin( $direction, $show_ui );
101
  }
 
 
102
  }
103
 
admin/field-create.php ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Field_Create implements P2P_Field {
4
+
5
+ protected $title_field;
6
+
7
+ function __construct( $title_field ) {
8
+ $this->title_field = $title_field;
9
+ }
10
+
11
+ function get_title() {
12
+ // Not needed
13
+ return '';
14
+ }
15
+
16
+ function render( $p2p_id, $item ) {
17
+ $data = array_merge( $this->title_field->get_data( $item ), array(
18
+ 'title' => $item->get_title(),
19
+ 'item-id' => $item->get_id(),
20
+ ) );
21
+
22
+ return P2P_Mustache::render( 'column-create', $data );
23
+ }
24
+ }
25
+
admin/field-delete.php ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Field_Delete implements P2P_Field {
4
+
5
+ function get_title() {
6
+ $data = array(
7
+ 'title' => __( 'Delete all connections', P2P_TEXTDOMAIN )
8
+ );
9
+
10
+ return P2P_Mustache::render( 'column-delete-all', $data );
11
+ }
12
+
13
+ function render( $p2p_id, $_ ) {
14
+ $data = array(
15
+ 'p2p_id' => $p2p_id,
16
+ 'title' => __( 'Delete connection', P2P_TEXTDOMAIN )
17
+ );
18
+
19
+ return P2P_Mustache::render( 'column-delete', $data );
20
+ }
21
+ }
22
+
23
+
admin/field-generic.php ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Field_Generic implements P2P_Field {
4
+
5
+ protected $key;
6
+ protected $data;
7
+
8
+ function __construct( $key, $data ) {
9
+ $this->key = $key;
10
+ $this->data = $data;
11
+ }
12
+
13
+ function get_title() {
14
+ return $this->data['title'];
15
+ }
16
+
17
+ function render( $p2p_id, $_ ) {
18
+ $args = $this->data;
19
+ $args['name'] = array( 'p2p_meta', $p2p_id, $this->key );
20
+
21
+ if ( 'select' == $args['type'] && !isset( $args['text'] ) )
22
+ $args['text'] = '';
23
+
24
+ return scbForms::input_from_meta( $args, $p2p_id, 'p2p' );
25
+ }
26
+ }
27
+
admin/field-order.php ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Field_Order implements P2P_Field {
4
+
5
+ protected $sort_key;
6
+
7
+ function __construct( $sort_key ) {
8
+ $this->sort_key = $sort_key;
9
+ }
10
+
11
+ function get_title() {
12
+ return '';
13
+ }
14
+
15
+ function render( $p2p_id, $_ ) {
16
+ return html( 'input', array(
17
+ 'type' => 'hidden',
18
+ 'name' => "p2p_order[$this->sort_key][]",
19
+ 'value' => $p2p_id
20
+ ) );
21
+ }
22
+ }
23
+
admin/field-title-attachment.php ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Field_Title_Attachment extends P2P_Field_Title {
4
+
5
+ function get_data( $item ) {
6
+ $data = array(
7
+ 'title-attr' => $item->get_object()->post_title,
8
+ );
9
+
10
+ return $data;
11
+ }
12
+ }
13
+
admin/field-title-post.php ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Field_Title_Post extends P2P_Field_Title {
4
+
5
+ function get_data( $item ) {
6
+ $data = array(
7
+ 'title-attr' => $item->get_permalink()
8
+ );
9
+
10
+ $post = $item->get_object();
11
+
12
+ if ( 'publish' != $post->post_status ) {
13
+ $status_obj = get_post_status_object( $post->post_status );
14
+ if ( $status_obj ) {
15
+ $data['status']['text'] = $status_obj->label;
16
+ }
17
+ }
18
+
19
+ return $data;
20
+ }
21
+ }
22
+
admin/field-title-user.php ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Field_Title_User extends P2P_Field_Title {
4
+
5
+ function get_data( $user ) {
6
+ return array(
7
+ 'title-attr' => '',
8
+ );
9
+ }
10
+ }
11
+
admin/field-title.php ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ abstract class P2P_Field_Title implements P2P_Field {
4
+
5
+ protected $title;
6
+
7
+ function __construct( $title = '' ) {
8
+ $this->title = $title;
9
+ }
10
+
11
+ function get_title() {
12
+ return $this->title;
13
+ }
14
+
15
+ function render( $p2p_id, $item ) {
16
+ $data = array_merge( $this->get_data( $item ), array(
17
+ 'title' => $item->get_title(),
18
+ 'url' => $item->get_editlink(),
19
+ ) );
20
+
21
+ return P2P_Mustache::render( 'column-title', $data );
22
+ }
23
+
24
+ abstract function get_data( $item );
25
+ }
26
+
admin/field.php ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ interface P2P_Field {
4
+ function get_title();
5
+ function render( $p2p_id, $item );
6
+ }
7
+
admin/fields.php DELETED
@@ -1,159 +0,0 @@
1
- <?php
2
-
3
- class P2P_Field_Delete implements P2P_Field {
4
-
5
- function get_title() {
6
- $data = array(
7
- 'title' => __( 'Delete all connections', P2P_TEXTDOMAIN )
8
- );
9
-
10
- return P2P_Mustache::render( 'column-delete-all', $data );
11
- }
12
-
13
- function render( $p2p_id, $_ ) {
14
- $data = array(
15
- 'p2p_id' => $p2p_id,
16
- 'title' => __( 'Delete connection', P2P_TEXTDOMAIN )
17
- );
18
-
19
- return P2P_Mustache::render( 'column-delete', $data );
20
- }
21
- }
22
-
23
-
24
- class P2P_Field_Order implements P2P_Field {
25
-
26
- protected $sort_key;
27
-
28
- function __construct( $sort_key ) {
29
- $this->sort_key = $sort_key;
30
- }
31
-
32
- function get_title() {
33
- return '';
34
- }
35
-
36
- function render( $p2p_id, $_ ) {
37
- return html( 'input', array(
38
- 'type' => 'hidden',
39
- 'name' => "p2p_order[$this->sort_key][]",
40
- 'value' => $p2p_id
41
- ) );
42
- }
43
- }
44
-
45
-
46
- class P2P_Field_Generic implements P2P_Field {
47
-
48
- protected $key;
49
- protected $data;
50
-
51
- function __construct( $key, $data ) {
52
- $this->key = $key;
53
- $this->data = $data;
54
- }
55
-
56
- function get_title() {
57
- return $this->data['title'];
58
- }
59
-
60
- function render( $p2p_id, $_ ) {
61
- $args = $this->data;
62
- $args['name'] = array( 'p2p_meta', $p2p_id, $this->key );
63
-
64
- if ( 'select' == $args['type'] && !isset( $args['text'] ) )
65
- $args['text'] = '';
66
-
67
- return scbForms::input_from_meta( $args, $p2p_id, 'p2p' );
68
- }
69
- }
70
-
71
-
72
- class P2P_Field_Create implements P2P_Field {
73
-
74
- protected $title_field;
75
-
76
- function __construct( $title_field ) {
77
- $this->title_field = $title_field;
78
- }
79
-
80
- function get_title() {
81
- // Not needed
82
- return '';
83
- }
84
-
85
- function render( $p2p_id, $item ) {
86
- $data = array_merge( $this->title_field->get_data( $item ), array(
87
- 'title' => $item->get_title(),
88
- 'item-id' => $item->get_id(),
89
- ) );
90
-
91
- return P2P_Mustache::render( 'column-create', $data );
92
- }
93
- }
94
-
95
-
96
- abstract class P2P_Field_Title implements P2P_Field {
97
-
98
- protected $title;
99
-
100
- function __construct( $title = '' ) {
101
- $this->title = $title;
102
- }
103
-
104
- function get_title() {
105
- return $this->title;
106
- }
107
-
108
- function render( $p2p_id, $item ) {
109
- $data = array_merge( $this->get_data( $item ), array(
110
- 'title' => $item->get_title(),
111
- 'url' => $item->get_editlink(),
112
- ) );
113
-
114
- return P2P_Mustache::render( 'column-title', $data );
115
- }
116
-
117
- abstract function get_data( $item );
118
- }
119
-
120
- class P2P_Field_Title_Post extends P2P_Field_Title {
121
-
122
- function get_data( $item ) {
123
- $data = array(
124
- 'title-attr' => $item->get_permalink()
125
- );
126
-
127
- $post = $item->get_object();
128
-
129
- if ( 'publish' != $post->post_status ) {
130
- $status_obj = get_post_status_object( $post->post_status );
131
- if ( $status_obj ) {
132
- $data['status']['text'] = $status_obj->label;
133
- }
134
- }
135
-
136
- return $data;
137
- }
138
- }
139
-
140
- class P2P_Field_Title_Attachment extends P2P_Field_Title {
141
-
142
- function get_data( $item ) {
143
- $data = array(
144
- 'title-attr' => $item->get_object()->post_title,
145
- );
146
-
147
- return $data;
148
- }
149
- }
150
-
151
- class P2P_Field_Title_User extends P2P_Field_Title {
152
-
153
- function get_data( $user ) {
154
- return array(
155
- 'title-attr' => '',
156
- );
157
- }
158
- }
159
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
admin/mustache.js ADDED
@@ -0,0 +1,610 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*!
2
+ * mustache.js - Logic-less {{mustache}} templates with JavaScript
3
+ * http://github.com/janl/mustache.js
4
+ */
5
+
6
+ /*global define: false*/
7
+
8
+ (function (root, factory) {
9
+ if (typeof exports === "object" && exports) {
10
+ module.exports = factory; // CommonJS
11
+ } else if (typeof define === "function" && define.amd) {
12
+ define(factory); // AMD
13
+ } else {
14
+ root.Mustache = factory; // <script>
15
+ }
16
+ }(this, (function () {
17
+
18
+ var exports = {};
19
+
20
+ exports.name = "mustache.js";
21
+ exports.version = "0.7.2";
22
+ exports.tags = ["{{", "}}"];
23
+
24
+ exports.Scanner = Scanner;
25
+ exports.Context = Context;
26
+ exports.Writer = Writer;
27
+
28
+ var whiteRe = /\s*/;
29
+ var spaceRe = /\s+/;
30
+ var nonSpaceRe = /\S/;
31
+ var eqRe = /\s*=/;
32
+ var curlyRe = /\s*\}/;
33
+ var tagRe = /#|\^|\/|>|\{|&|=|!/;
34
+
35
+ // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
36
+ // See https://github.com/janl/mustache.js/issues/189
37
+ function testRe(re, string) {
38
+ return RegExp.prototype.test.call(re, string);
39
+ }
40
+
41
+ function isWhitespace(string) {
42
+ return !testRe(nonSpaceRe, string);
43
+ }
44
+
45
+ var isArray = Array.isArray || function (obj) {
46
+ return Object.prototype.toString.call(obj) === "[object Array]";
47
+ };
48
+
49
+ function escapeRe(string) {
50
+ return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
51
+ }
52
+
53
+ var entityMap = {
54
+ "&": "&amp;",
55
+ "<": "&lt;",
56
+ ">": "&gt;",
57
+ '"': '&quot;',
58
+ "'": '&#39;',
59
+ "/": '&#x2F;'
60
+ };
61
+
62
+ function escapeHtml(string) {
63
+ return String(string).replace(/[&<>"'\/]/g, function (s) {
64
+ return entityMap[s];
65
+ });
66
+ }
67
+
68
+ // Export the escaping function so that the user may override it.
69
+ // See https://github.com/janl/mustache.js/issues/244
70
+ exports.escape = escapeHtml;
71
+
72
+ function Scanner(string) {
73
+ this.string = string;
74
+ this.tail = string;
75
+ this.pos = 0;
76
+ }
77
+
78
+ /**
79
+ * Returns `true` if the tail is empty (end of string).
80
+ */
81
+ Scanner.prototype.eos = function () {
82
+ return this.tail === "";
83
+ };
84
+
85
+ /**
86
+ * Tries to match the given regular expression at the current position.
87
+ * Returns the matched text if it can match, the empty string otherwise.
88
+ */
89
+ Scanner.prototype.scan = function (re) {
90
+ var match = this.tail.match(re);
91
+
92
+ if (match && match.index === 0) {
93
+ this.tail = this.tail.substring(match[0].length);
94
+ this.pos += match[0].length;
95
+ return match[0];
96
+ }
97
+
98
+ return "";
99
+ };
100
+
101
+ /**
102
+ * Skips all text until the given regular expression can be matched. Returns
103
+ * the skipped string, which is the entire tail if no match can be made.
104
+ */
105
+ Scanner.prototype.scanUntil = function (re) {
106
+ var match, pos = this.tail.search(re);
107
+
108
+ switch (pos) {
109
+ case -1:
110
+ match = this.tail;
111
+ this.pos += this.tail.length;
112
+ this.tail = "";
113
+ break;
114
+ case 0:
115
+ match = "";
116
+ break;
117
+ default:
118
+ match = this.tail.substring(0, pos);
119
+ this.tail = this.tail.substring(pos);
120
+ this.pos += pos;
121
+ }
122
+
123
+ return match;
124
+ };
125
+
126
+ function Context(view, parent) {
127
+ this.view = view;
128
+ this.parent = parent;
129
+ this.clearCache();
130
+ }
131
+
132
+ Context.make = function (view) {
133
+ return (view instanceof Context) ? view : new Context(view);
134
+ };
135
+
136
+ Context.prototype.clearCache = function () {
137
+ this._cache = {};
138
+ };
139
+
140
+ Context.prototype.push = function (view) {
141
+ return new Context(view, this);
142
+ };
143
+
144
+ Context.prototype.lookup = function (name) {
145
+ var value = this._cache[name];
146
+
147
+ if (!value) {
148
+ if (name === ".") {
149
+ value = this.view;
150
+ } else {
151
+ var context = this;
152
+
153
+ while (context) {
154
+ if (name.indexOf(".") > 0) {
155
+ var names = name.split("."), i = 0;
156
+
157
+ value = context.view;
158
+
159
+ while (value && i < names.length) {
160
+ value = value[names[i++]];
161
+ }
162
+ } else {
163
+ value = context.view[name];
164
+ }
165
+
166
+ if (value != null) {
167
+ break;
168
+ }
169
+
170
+ context = context.parent;
171
+ }
172
+ }
173
+
174
+ this._cache[name] = value;
175
+ }
176
+
177
+ if (typeof value === "function") {
178
+ value = value.call(this.view);
179
+ }
180
+
181
+ return value;
182
+ };
183
+
184
+ function Writer() {
185
+ this.clearCache();
186
+ }
187
+
188
+ Writer.prototype.clearCache = function () {
189
+ this._cache = {};
190
+ this._partialCache = {};
191
+ };
192
+
193
+ Writer.prototype.compile = function (template, tags) {
194
+ var fn = this._cache[template];
195
+
196
+ if (!fn) {
197
+ var tokens = exports.parse(template, tags);
198
+ fn = this._cache[template] = this.compileTokens(tokens, template);
199
+ }
200
+
201
+ return fn;
202
+ };
203
+
204
+ Writer.prototype.compilePartial = function (name, template, tags) {
205
+ var fn = this.compile(template, tags);
206
+ this._partialCache[name] = fn;
207
+ return fn;
208
+ };
209
+
210
+ Writer.prototype.compileTokens = function (tokens, template) {
211
+ var fn = compileTokens(tokens);
212
+ var self = this;
213
+
214
+ return function (view, partials) {
215
+ if (partials) {
216
+ if (typeof partials === "function") {
217
+ self._loadPartial = partials;
218
+ } else {
219
+ for (var name in partials) {
220
+ self.compilePartial(name, partials[name]);
221
+ }
222
+ }
223
+ }
224
+
225
+ return fn(self, Context.make(view), template);
226
+ };
227
+ };
228
+
229
+ Writer.prototype.render = function (template, view, partials) {
230
+ return this.compile(template)(view, partials);
231
+ };
232
+
233
+ Writer.prototype._section = function (name, context, text, callback) {
234
+ var value = context.lookup(name);
235
+
236
+ switch (typeof value) {
237
+ case "object":
238
+ if (isArray(value)) {
239
+ var buffer = "";
240
+
241
+ for (var i = 0, len = value.length; i < len; ++i) {
242
+ buffer += callback(this, context.push(value[i]));
243
+ }
244
+
245
+ return buffer;
246
+ }
247
+
248
+ return value ? callback(this, context.push(value)) : "";
249
+ case "function":
250
+ var self = this;
251
+ var scopedRender = function (template) {
252
+ return self.render(template, context);
253
+ };
254
+
255
+ var result = value.call(context.view, text, scopedRender);
256
+ return result != null ? result : "";
257
+ default:
258
+ if (value) {
259
+ return callback(this, context);
260
+ }
261
+ }
262
+
263
+ return "";
264
+ };
265
+
266
+ Writer.prototype._inverted = function (name, context, callback) {
267
+ var value = context.lookup(name);
268
+
269
+ // Use JavaScript's definition of falsy. Include empty arrays.
270
+ // See https://github.com/janl/mustache.js/issues/186
271
+ if (!value || (isArray(value) && value.length === 0)) {
272
+ return callback(this, context);
273
+ }
274
+
275
+ return "";
276
+ };
277
+
278
+ Writer.prototype._partial = function (name, context) {
279
+ if (!(name in this._partialCache) && this._loadPartial) {
280
+ this.compilePartial(name, this._loadPartial(name));
281
+ }
282
+
283
+ var fn = this._partialCache[name];
284
+
285
+ return fn ? fn(context) : "";
286
+ };
287
+
288
+ Writer.prototype._name = function (name, context) {
289
+ var value = context.lookup(name);
290
+
291
+ if (typeof value === "function") {
292
+ value = value.call(context.view);
293
+ }
294
+
295
+ return (value == null) ? "" : String(value);
296
+ };
297
+
298
+ Writer.prototype._escaped = function (name, context) {
299
+ return exports.escape(this._name(name, context));
300
+ };
301
+
302
+ /**
303
+ * Low-level function that compiles the given `tokens` into a function
304
+ * that accepts three arguments: a Writer, a Context, and the template.
305
+ */
306
+ function compileTokens(tokens) {
307
+ var subRenders = {};
308
+
309
+ function subRender(i, tokens, template) {
310
+ if (!subRenders[i]) {
311
+ var fn = compileTokens(tokens);
312
+ subRenders[i] = function (writer, context) {
313
+ return fn(writer, context, template);
314
+ };
315
+ }
316
+
317
+ return subRenders[i];
318
+ }
319
+
320
+ return function (writer, context, template) {
321
+ var buffer = "";
322
+ var token, sectionText;
323
+
324
+ for (var i = 0, len = tokens.length; i < len; ++i) {
325
+ token = tokens[i];
326
+
327
+ switch (token[0]) {
328
+ case "#":
329
+ sectionText = template.slice(token[3], token[5]);
330
+ buffer += writer._section(token[1], context, sectionText, subRender(i, token[4], template));
331
+ break;
332
+ case "^":
333
+ buffer += writer._inverted(token[1], context, subRender(i, token[4], template));
334
+ break;
335
+ case ">":
336
+ buffer += writer._partial(token[1], context);
337
+ break;
338
+ case "&":
339
+ buffer += writer._name(token[1], context);
340
+ break;
341
+ case "name":
342
+ buffer += writer._escaped(token[1], context);
343
+ break;
344
+ case "text":
345
+ buffer += token[1];
346
+ break;
347
+ }
348
+ }
349
+
350
+ return buffer;
351
+ };
352
+ }
353
+
354
+ /**
355
+ * Forms the given array of `tokens` into a nested tree structure where
356
+ * tokens that represent a section have two additional items: 1) an array of
357
+ * all tokens that appear in that section and 2) the index in the original
358
+ * template that represents the end of that section.
359
+ */
360
+ function nestTokens(tokens) {
361
+ var tree = [];
362
+ var collector = tree;
363
+ var sections = [];
364
+
365
+ var token;
366
+ for (var i = 0, len = tokens.length; i < len; ++i) {
367
+ token = tokens[i];
368
+ switch (token[0]) {
369
+ case '#':
370
+ case '^':
371
+ sections.push(token);
372
+ collector.push(token);
373
+ collector = token[4] = [];
374
+ break;
375
+ case '/':
376
+ var section = sections.pop();
377
+ section[5] = token[2];
378
+ collector = sections.length > 0 ? sections[sections.length - 1][4] : tree;
379
+ break;
380
+ default:
381
+ collector.push(token);
382
+ }
383
+ }
384
+
385
+ return tree;
386
+ }
387
+
388
+ /**
389
+ * Combines the values of consecutive text tokens in the given `tokens` array
390
+ * to a single token.
391
+ */
392
+ function squashTokens(tokens) {
393
+ var squashedTokens = [];
394
+
395
+ var token, lastToken;
396
+ for (var i = 0, len = tokens.length; i < len; ++i) {
397
+ token = tokens[i];
398
+ if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
399
+ lastToken[1] += token[1];
400
+ lastToken[3] = token[3];
401
+ } else {
402
+ lastToken = token;
403
+ squashedTokens.push(token);
404
+ }
405
+ }
406
+
407
+ return squashedTokens;
408
+ }
409
+
410
+ function escapeTags(tags) {
411
+ return [
412
+ new RegExp(escapeRe(tags[0]) + "\\s*"),
413
+ new RegExp("\\s*" + escapeRe(tags[1]))
414
+ ];
415
+ }
416
+
417
+ /**
418
+ * Breaks up the given `template` string into a tree of token objects. If
419
+ * `tags` is given here it must be an array with two string values: the
420
+ * opening and closing tags used in the template (e.g. ["<%", "%>"]). Of
421
+ * course, the default is to use mustaches (i.e. Mustache.tags).
422
+ */
423
+ exports.parse = function (template, tags) {
424
+ template = template || '';
425
+ tags = tags || exports.tags;
426
+
427
+ if (typeof tags === 'string') tags = tags.split(spaceRe);
428
+ if (tags.length !== 2) {
429
+ throw new Error('Invalid tags: ' + tags.join(', '));
430
+ }
431
+
432
+ var tagRes = escapeTags(tags);
433
+ var scanner = new Scanner(template);
434
+
435
+ var sections = []; // Stack to hold section tokens
436
+ var tokens = []; // Buffer to hold the tokens
437
+ var spaces = []; // Indices of whitespace tokens on the current line
438
+ var hasTag = false; // Is there a {{tag}} on the current line?
439
+ var nonSpace = false; // Is there a non-space char on the current line?
440
+
441
+ // Strips all whitespace tokens array for the current line
442
+ // if there was a {{#tag}} on it and otherwise only space.
443
+ function stripSpace() {
444
+ if (hasTag && !nonSpace) {
445
+ while (spaces.length) {
446
+ tokens.splice(spaces.pop(), 1);
447
+ }
448
+ } else {
449
+ spaces = [];
450
+ }
451
+
452
+ hasTag = false;
453
+ nonSpace = false;
454
+ }
455
+
456
+ var start, type, value, chr;
457
+ while (!scanner.eos()) {
458
+ start = scanner.pos;
459
+ value = scanner.scanUntil(tagRes[0]);
460
+
461
+ if (value) {
462
+ for (var i = 0, len = value.length; i < len; ++i) {
463
+ chr = value.charAt(i);
464
+
465
+ if (isWhitespace(chr)) {
466
+ spaces.push(tokens.length);
467
+ } else {
468
+ nonSpace = true;
469
+ }
470
+
471
+ tokens.push(["text", chr, start, start + 1]);
472
+ start += 1;
473
+
474
+ if (chr === "\n") {
475
+ stripSpace(); // Check for whitespace on the current line.
476
+ }
477
+ }
478
+ }
479
+
480
+ start = scanner.pos;
481
+
482
+ // Match the opening tag.
483
+ if (!scanner.scan(tagRes[0])) {
484
+ break;
485
+ }
486
+
487
+ hasTag = true;
488
+ type = scanner.scan(tagRe) || "name";
489
+
490
+ // Skip any whitespace between tag and value.
491
+ scanner.scan(whiteRe);
492
+
493
+ // Extract the tag value.
494
+ if (type === "=") {
495
+ value = scanner.scanUntil(eqRe);
496
+ scanner.scan(eqRe);
497
+ scanner.scanUntil(tagRes[1]);
498
+ } else if (type === "{") {
499
+ var closeRe = new RegExp("\\s*" + escapeRe("}" + tags[1]));
500
+ value = scanner.scanUntil(closeRe);
501
+ scanner.scan(curlyRe);
502
+ scanner.scanUntil(tagRes[1]);
503
+ type = "&";
504
+ } else {
505
+ value = scanner.scanUntil(tagRes[1]);
506
+ }
507
+
508
+ // Match the closing tag.
509
+ if (!scanner.scan(tagRes[1])) {
510
+ throw new Error('Unclosed tag at ' + scanner.pos);
511
+ }
512
+
513
+ // Check section nesting.
514
+ if (type === '/') {
515
+ if (sections.length === 0) {
516
+ throw new Error('Unopened section "' + value + '" at ' + start);
517
+ }
518
+
519
+ var section = sections.pop();
520
+
521
+ if (section[1] !== value) {
522
+ throw new Error('Unclosed section "' + section[1] + '" at ' + start);
523
+ }
524
+ }
525
+
526
+ var token = [type, value, start, scanner.pos];
527
+ tokens.push(token);
528
+
529
+ if (type === '#' || type === '^') {
530
+ sections.push(token);
531
+ } else if (type === "name" || type === "{" || type === "&") {
532
+ nonSpace = true;
533
+ } else if (type === "=") {
534
+ // Set the tags for the next time around.
535
+ tags = value.split(spaceRe);
536
+
537
+ if (tags.length !== 2) {
538
+ throw new Error('Invalid tags at ' + start + ': ' + tags.join(', '));
539
+ }
540
+
541
+ tagRes = escapeTags(tags);
542
+ }
543
+ }
544
+
545
+ // Make sure there are no open sections when we're done.
546
+ var section = sections.pop();
547
+ if (section) {
548
+ throw new Error('Unclosed section "' + section[1] + '" at ' + scanner.pos);
549
+ }
550
+
551
+ return nestTokens(squashTokens(tokens));
552
+ };
553
+
554
+ // The high-level clearCache, compile, compilePartial, and render functions
555
+ // use this default writer.
556
+ var _writer = new Writer();
557
+
558
+ /**
559
+ * Clears all cached templates and partials in the default writer.
560
+ */
561
+ exports.clearCache = function () {
562
+ return _writer.clearCache();
563
+ };
564
+
565
+ /**
566
+ * Compiles the given `template` to a reusable function using the default
567
+ * writer.
568
+ */
569
+ exports.compile = function (template, tags) {
570
+ return _writer.compile(template, tags);
571
+ };
572
+
573
+ /**
574
+ * Compiles the partial with the given `name` and `template` to a reusable
575
+ * function using the default writer.
576
+ */
577
+ exports.compilePartial = function (name, template, tags) {
578
+ return _writer.compilePartial(name, template, tags);
579
+ };
580
+
581
+ /**
582
+ * Compiles the given array of tokens (the output of a parse) to a reusable
583
+ * function using the default writer.
584
+ */
585
+ exports.compileTokens = function (tokens, template) {
586
+ return _writer.compileTokens(tokens, template);
587
+ };
588
+
589
+ /**
590
+ * Renders the `template` with the given `view` and `partials` using the
591
+ * default writer.
592
+ */
593
+ exports.render = function (template, view, partials) {
594
+ return _writer.render(template, view, partials);
595
+ };
596
+
597
+ // This is here for backwards compatibility with 0.4.x.
598
+ exports.to_html = function (template, view, partials, send) {
599
+ var result = exports.render(template, view, partials);
600
+
601
+ if (typeof send === "function") {
602
+ send(result);
603
+ } else {
604
+ return result;
605
+ }
606
+ };
607
+
608
+ return exports;
609
+
610
+ }())));
admin/templates/tab-list.html CHANGED
@@ -10,7 +10,7 @@
10
  <div class="p2p-navigation">
11
  <div class="p2p-prev button {{prev-inactive}}" title="{{prev-label}}">&lsaquo;</div>
12
  <div>
13
- <span class="p2p-current" data-num="{{current-page-raw}}">{{current-page}}</span>
14
  {{of-label}}
15
  <span class="p2p-total" data-num="{{total-pages-raw}}">{{total-pages}}</span>
16
  </div>
10
  <div class="p2p-navigation">
11
  <div class="p2p-prev button {{prev-inactive}}" title="{{prev-label}}">&lsaquo;</div>
12
  <div>
13
+ <span class="p2p-current">{{current-page}}</span>
14
  {{of-label}}
15
  <span class="p2p-total" data-num="{{total-pages-raw}}">{{total-pages}}</span>
16
  </div>
admin/{tools.php → tools-page.php} RENAMED
@@ -133,5 +133,3 @@ class P2P_Tools_Page extends scbAdminPage {
133
  }
134
  }
135
 
136
- new P2P_Tools_Page;
137
-
133
  }
134
  }
135
 
 
 
autoload.php ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Autoload {
4
+
5
+ protected function __construct( $prefix, $basedir ) {
6
+ $this->prefix = $prefix;
7
+ $this->basedir = $basedir;
8
+ }
9
+
10
+ static function register( $prefix, $basedir ) {
11
+ $loader = new self( $prefix, $basedir );
12
+
13
+ spl_autoload_register( array( $loader, 'autoload' ) );
14
+ }
15
+
16
+ function autoload( $class ) {
17
+ if ( $class[0] === '\\' ) {
18
+ $class = substr( $class, 1 );
19
+ }
20
+
21
+ if ( strpos( $class, $this->prefix ) !== 0 ) {
22
+ return;
23
+ }
24
+
25
+ $path = str_replace( $this->prefix, '', $class );
26
+ $path = str_replace( '_', '-', strtolower( $path ) );
27
+
28
+ $file = sprintf( '%s/%s.php', $this->basedir, $path );
29
+
30
+ if ( is_file( $file ) ) {
31
+ require $file;
32
+ }
33
+ }
34
+ }
35
+
command.php CHANGED
@@ -30,10 +30,9 @@ class P2P_CLI_Command extends WP_CLI_Command {
30
 
31
  if ( isset( $assoc_args['items'] ) ) {
32
  foreach ( _p2p_extract_post_types( $ctype->side ) as $ptype ) {
33
- $command = array( 'wp', 'post', 'generate' );
34
  $assoc_args = array( 'post_type' => $ptype );
35
 
36
- WP_CLI::launch( WP_CLI::compose_args( $command, $assoc_args ) );
37
  }
38
  }
39
 
30
 
31
  if ( isset( $assoc_args['items'] ) ) {
32
  foreach ( _p2p_extract_post_types( $ctype->side ) as $ptype ) {
 
33
  $assoc_args = array( 'post_type' => $ptype );
34
 
35
+ WP_CLI::launch( 'wp post generate' . \WP_CLI\Utils\assoc_args_to_str( $assoc_args ) );
36
  }
37
  }
38
 
core/api.php CHANGED
@@ -1,19 +1,17 @@
1
  <?php
2
 
3
  /**
4
- * Register a connection between two post types.
5
  *
6
- * This creates the appropriate meta box in the admin edit screen.
7
- *
8
- * Takes the following parameters, as an associative array:
9
  *
10
  * - 'name' - string A unique identifier for this connection type.
11
  *
12
- * - 'from' - string|array The first end of the connection.
13
  *
14
  * - 'from_query_vars' - array Additional query vars to pass to WP_Query. Default: none.
15
  *
16
- * - 'to' - string|array The second end of the connection.
17
  *
18
  * - 'to_query_vars' - array Additional query vars to pass to WP_Query. Default: none.
19
  *
@@ -23,7 +21,7 @@
23
  *
24
  * - 'duplicate_connections' - bool Whether to allow more than one connection between the same two posts. Default: false.
25
  *
26
- * - 'self_connections' - bool Whether to allow a post to connect to itself. Default: false.
27
  *
28
  * - 'sortable' - bool|string Whether to allow connections to be ordered via drag-and-drop. Can be 'from', 'to', 'any' or false. Default: false.
29
  *
@@ -35,11 +33,13 @@
35
  *
36
  * - 'reciprocal' - bool For indeterminate connections: True means all connections are displayed in a single box. False means 'from' connections are shown in one box and 'to' connections are shown in another box. Default: false.
37
  *
38
- * - 'admin_box' - bool|string|array Whether and where to show the admin connections box.
39
  *
40
  * - 'can_create_post' - bool Whether to allow post creation via the connection box. Default: true.
41
  *
42
- * @param array $args
 
 
43
  *
44
  * @return bool|object False on failure, P2P_Connection_Type instance on success.
45
  */
@@ -77,6 +77,9 @@ function p2p_register_connection_type( $args ) {
77
  $args['admin_box']['context'] = _p2p_pluck( $args, 'context' );
78
  }
79
 
 
 
 
80
  $ctype = P2P_Connection_Type_Factory::register( $args );
81
 
82
  do_action( 'p2p_registered_connection_type', $ctype, $args );
@@ -126,35 +129,35 @@ function p2p_connection_exists( $p2p_type, $args = array() ) {
126
  * @return array
127
  */
128
  function p2p_get_connections( $p2p_type, $args = array() ) {
129
- extract( wp_parse_args( $args, array(
130
  'direction' => 'from',
131
  'from' => 'any',
132
  'to' => 'any',
133
  'fields' => 'all',
134
- ) ), EXTR_SKIP );
135
 
136
  $r = array();
137
 
138
- foreach ( _p2p_expand_direction( $direction ) as $direction ) {
139
- $args = array( $from, $to );
140
 
141
  if ( 'to' == $direction ) {
142
- $args = array_reverse( $args );
143
  }
144
 
145
- if ( 'object_id' == $fields )
146
- $field = ( 'to' == $direction ) ? 'p2p_from' : 'p2p_to';
147
  else
148
- $field = $fields;
149
 
150
  $r = array_merge( $r, _p2p_get_connections( $p2p_type, array(
151
- 'from' => $args[0],
152
- 'to' => $args[1],
153
- 'fields' => $field
154
  ) ) );
155
  }
156
 
157
- if ( 'count' == $fields )
158
  return array_sum( $r );
159
 
160
  return $r;
@@ -164,27 +167,25 @@ function p2p_get_connections( $p2p_type, $args = array() ) {
164
  function _p2p_get_connections( $p2p_type, $args = array() ) {
165
  global $wpdb;
166
 
167
- extract( $args, EXTR_SKIP );
168
-
169
  $where = $wpdb->prepare( 'WHERE p2p_type = %s', $p2p_type );
170
 
171
  foreach ( array( 'from', 'to' ) as $key ) {
172
- if ( 'any' == $$key )
173
  continue;
174
 
175
- if ( empty( $$key ) )
176
  return array();
177
 
178
- $value = scbUtil::array_to_sql( _p2p_normalize( $$key ) );
179
 
180
  $where .= " AND p2p_$key IN ($value)";
181
  }
182
 
183
- switch ( $fields ) {
184
  case 'p2p_id':
185
  case 'p2p_from':
186
  case 'p2p_to':
187
- $sql_field = $fields;
188
  break;
189
  case 'count':
190
  $sql_field = 'COUNT(*)';
@@ -225,34 +226,34 @@ function p2p_get_connection( $p2p_id ) {
225
  function p2p_create_connection( $p2p_type, $args ) {
226
  global $wpdb;
227
 
228
- extract( wp_parse_args( $args, array(
229
  'direction' => 'from',
230
  'from' => false,
231
  'to' => false,
232
  'meta' => array()
233
- ) ), EXTR_SKIP );
234
 
235
- list( $from ) = _p2p_normalize( $from );
236
- list( $to ) = _p2p_normalize( $to );
237
 
238
  if ( !$from || !$to )
239
  return false;
240
 
241
- $args = array( $from, $to );
242
 
243
- if ( 'to' == $direction ) {
244
- $args = array_reverse( $args );
245
  }
246
 
247
  $wpdb->insert( $wpdb->p2p, array(
248
  'p2p_type' => $p2p_type,
249
- 'p2p_from' => $args[0],
250
- 'p2p_to' => $args[1]
251
  ) );
252
 
253
  $p2p_id = $wpdb->insert_id;
254
 
255
- foreach ( $meta as $key => $value )
256
  p2p_add_meta( $p2p_id, $key, $value );
257
 
258
  do_action( 'p2p_created_connection', $p2p_id );
@@ -350,29 +351,10 @@ function p2p_distribute_connected( $items, $connected, $prop_name ) {
350
  $indexed_list[ $item->ID ] = $item;
351
  }
352
 
353
- $groups = p2p_triage_connected( $connected );
354
 
355
  foreach ( $groups as $outer_item_id => $connected_items ) {
356
  $indexed_list[ $outer_item_id ]->$prop_name = $connected_items;
357
  }
358
  }
359
 
360
- function p2p_triage_connected( $connected ) {
361
- $groups = array();
362
-
363
- foreach ( $connected as $inner_item ) {
364
- if ( $inner_item->ID == $inner_item->p2p_from ) {
365
- $outer_item_id = $inner_item->p2p_to;
366
- } elseif ( $inner_item->ID == $inner_item->p2p_to ) {
367
- $outer_item_id = $inner_item->p2p_from;
368
- } else {
369
- trigger_error( "Corrupted data for item $inner_item->ID", E_USER_WARNING );
370
- continue;
371
- }
372
-
373
- $groups[ $outer_item_id ][] = $inner_item;
374
- }
375
-
376
- return $groups;
377
- }
378
-
1
  <?php
2
 
3
  /**
4
+ * Register a connection type.
5
  *
6
+ * @param array $args Associative array:
 
 
7
  *
8
  * - 'name' - string A unique identifier for this connection type.
9
  *
10
+ * - 'from' - string|array The first end of the connection: post type name or 'user'
11
  *
12
  * - 'from_query_vars' - array Additional query vars to pass to WP_Query. Default: none.
13
  *
14
+ * - 'to' - string|array The second end of the connection: post type name or 'user'
15
  *
16
  * - 'to_query_vars' - array Additional query vars to pass to WP_Query. Default: none.
17
  *
21
  *
22
  * - 'duplicate_connections' - bool Whether to allow more than one connection between the same two posts. Default: false.
23
  *
24
+ * - 'self_connections' - bool Whether to allow a post/user to connect to itself. Default: false.
25
  *
26
  * - 'sortable' - bool|string Whether to allow connections to be ordered via drag-and-drop. Can be 'from', 'to', 'any' or false. Default: false.
27
  *
33
  *
34
  * - 'reciprocal' - bool For indeterminate connections: True means all connections are displayed in a single box. False means 'from' connections are shown in one box and 'to' connections are shown in another box. Default: false.
35
  *
36
+ * - 'admin_box' - bool|string|array Whether and where to show the admin connections box. Default: 'any'
37
  *
38
  * - 'can_create_post' - bool Whether to allow post creation via the connection box. Default: true.
39
  *
40
+ * - 'admin_column' - bool|string|array Whether to show connection columns on post/user list table. Default: false
41
+ *
42
+ * - 'admin_dropdown' - bool|string|array Whether to show connection dropdown on post/user list table. Default: false
43
  *
44
  * @return bool|object False on failure, P2P_Connection_Type instance on success.
45
  */
77
  $args['admin_box']['context'] = _p2p_pluck( $args, 'context' );
78
  }
79
 
80
+ if ( !isset( $args['admin_box'] ) )
81
+ $args['admin_box'] = 'any';
82
+
83
  $ctype = P2P_Connection_Type_Factory::register( $args );
84
 
85
  do_action( 'p2p_registered_connection_type', $ctype, $args );
129
  * @return array
130
  */
131
  function p2p_get_connections( $p2p_type, $args = array() ) {
132
+ $args = wp_parse_args( $args, array(
133
  'direction' => 'from',
134
  'from' => 'any',
135
  'to' => 'any',
136
  'fields' => 'all',
137
+ ) );
138
 
139
  $r = array();
140
 
141
+ foreach ( _p2p_expand_direction( $args['direction'] ) as $direction ) {
142
+ $dirs = array( $args['from'], $args['to'] );
143
 
144
  if ( 'to' == $direction ) {
145
+ $dirs = array_reverse( $dirs );
146
  }
147
 
148
+ if ( 'object_id' == $args['fields'] )
149
+ $fields = ( 'to' == $direction ) ? 'p2p_from' : 'p2p_to';
150
  else
151
+ $fields = $args['fields'];
152
 
153
  $r = array_merge( $r, _p2p_get_connections( $p2p_type, array(
154
+ 'from' => $dirs[0],
155
+ 'to' => $dirs[1],
156
+ 'fields' => $fields
157
  ) ) );
158
  }
159
 
160
+ if ( 'count' == $args['fields'] )
161
  return array_sum( $r );
162
 
163
  return $r;
167
  function _p2p_get_connections( $p2p_type, $args = array() ) {
168
  global $wpdb;
169
 
 
 
170
  $where = $wpdb->prepare( 'WHERE p2p_type = %s', $p2p_type );
171
 
172
  foreach ( array( 'from', 'to' ) as $key ) {
173
+ if ( 'any' == $args[ $key ] )
174
  continue;
175
 
176
+ if ( empty( $args[ $key ] ) )
177
  return array();
178
 
179
+ $value = scbUtil::array_to_sql( _p2p_normalize( $args[ $key ] ) );
180
 
181
  $where .= " AND p2p_$key IN ($value)";
182
  }
183
 
184
+ switch ( $args['fields'] ) {
185
  case 'p2p_id':
186
  case 'p2p_from':
187
  case 'p2p_to':
188
+ $sql_field = $args['fields'];
189
  break;
190
  case 'count':
191
  $sql_field = 'COUNT(*)';
226
  function p2p_create_connection( $p2p_type, $args ) {
227
  global $wpdb;
228
 
229
+ $args = wp_parse_args( $args, array(
230
  'direction' => 'from',
231
  'from' => false,
232
  'to' => false,
233
  'meta' => array()
234
+ ) );
235
 
236
+ list( $from ) = _p2p_normalize( $args['from'] );
237
+ list( $to ) = _p2p_normalize( $args['to'] );
238
 
239
  if ( !$from || !$to )
240
  return false;
241
 
242
+ $dirs = array( $from, $to );
243
 
244
+ if ( 'to' == $args['direction'] ) {
245
+ $dirs = array_reverse( $dirs );
246
  }
247
 
248
  $wpdb->insert( $wpdb->p2p, array(
249
  'p2p_type' => $p2p_type,
250
+ 'p2p_from' => $dirs[0],
251
+ 'p2p_to' => $dirs[1]
252
  ) );
253
 
254
  $p2p_id = $wpdb->insert_id;
255
 
256
+ foreach ( $args['meta'] as $key => $value )
257
  p2p_add_meta( $p2p_id, $key, $value );
258
 
259
  do_action( 'p2p_created_connection', $p2p_id );
351
  $indexed_list[ $item->ID ] = $item;
352
  }
353
 
354
+ $groups = p2p_list_cluster( $connected, '_p2p_get_other_id' );
355
 
356
  foreach ( $groups as $outer_item_id => $connected_items ) {
357
  $indexed_list[ $outer_item_id ]->$prop_name = $connected_items;
358
  }
359
  }
360
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/{type-factory.php → connection-type-factory.php} RENAMED
@@ -7,8 +7,6 @@ class P2P_Connection_Type_Factory {
7
  public static function register( $args ) {
8
  $defaults = array(
9
  'name' => false,
10
- 'from_object' => 'post',
11
- 'to_object' => 'post',
12
  'from' => 'post',
13
  'to' => 'post',
14
  'from_query_vars' => array(),
7
  public static function register( $args ) {
8
  $defaults = array(
9
  'name' => false,
 
 
10
  'from' => 'post',
11
  'to' => 'post',
12
  'from_query_vars' => array(),
core/{type.php → connection-type.php} RENAMED
@@ -98,11 +98,10 @@ class P2P_Connection_Type {
98
  }
99
 
100
  private function expand_labels( $additional_labels, $key ) {
101
- $additional_labels['create'] = __( 'Create connections', P2P_TEXTDOMAIN );
102
-
103
  $labels = clone $this->side[ $key ]->get_labels();
 
104
 
105
- foreach ( $additional_labels as $key => $var )
106
  $labels->$key = $var;
107
 
108
  return $labels;
@@ -380,11 +379,13 @@ class P2P_Connection_Type {
380
  }
381
 
382
  public function get_desc() {
 
 
383
  foreach ( array( 'from', 'to' ) as $key ) {
384
- $$key = $this->side[ $key ]->get_desc();
385
  }
386
 
387
- $label = "$from {$this->arrow} $to";
388
 
389
  $title = $this->get_field( 'title', 'from' );
390
 
@@ -395,36 +396,3 @@ class P2P_Connection_Type {
395
  }
396
  }
397
 
398
-
399
- class P2P_Indeterminate_Connection_Type extends P2P_Connection_Type {
400
-
401
- protected $directed_class = 'P2P_Indeterminate_Directed_Connection_Type';
402
-
403
- protected $arrow = '&harr;';
404
-
405
- protected function choose_direction( $direction ) {
406
- return 'from';
407
- }
408
-
409
- function _directions_for_admin( $direction, $show_ui ) {
410
- return parent::_directions_for_admin( 'any', $show_ui );
411
- }
412
- }
413
-
414
-
415
- class P2P_Reciprocal_Connection_Type extends P2P_Indeterminate_Connection_Type {
416
-
417
- protected function choose_direction( $direction ) {
418
- return 'any';
419
- }
420
-
421
- function _directions_for_admin( $direction, $show_ui ) {
422
- if ( $show_ui )
423
- $directions = array( 'any' );
424
- else
425
- $directions = array();
426
-
427
- return $directions;
428
- }
429
- }
430
-
98
  }
99
 
100
  private function expand_labels( $additional_labels, $key ) {
 
 
101
  $labels = clone $this->side[ $key ]->get_labels();
102
+ $labels->create = __( 'Create connections', P2P_TEXTDOMAIN );
103
 
104
+ foreach ( $additional_labels[ $key ] as $key => $var )
105
  $labels->$key = $var;
106
 
107
  return $labels;
379
  }
380
 
381
  public function get_desc() {
382
+ $desc = array();
383
+
384
  foreach ( array( 'from', 'to' ) as $key ) {
385
+ $desc[ $key ] = $this->side[ $key ]->get_desc();
386
  }
387
 
388
+ $label = sprintf( '%s %s %s', $desc['from'], $this->arrow, $desc['to'] );
389
 
390
  $title = $this->get_field( 'title', 'from' );
391
 
396
  }
397
  }
398
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/{directed-type.php → directed-connection-type.php} RENAMED
File without changes
core/indeterminate-connection-type.php ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Indeterminate_Connection_Type extends P2P_Connection_Type {
4
+
5
+ protected $directed_class = 'P2P_Indeterminate_Directed_Connection_Type';
6
+
7
+ protected $arrow = '&harr;';
8
+
9
+ protected function choose_direction( $direction ) {
10
+ return 'from';
11
+ }
12
+
13
+ function _directions_for_admin( $direction, $show_ui ) {
14
+ return parent::_directions_for_admin( 'any', $show_ui );
15
+ }
16
+ }
17
+
core/{indeterminate-type.php → indeterminate-directed-connection-type.php} RENAMED
File without changes
core/item-any.php ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Item_Any extends P2P_Item {
4
+
5
+ function __construct() {}
6
+
7
+ function get_permalink() {}
8
+
9
+ function get_title() {}
10
+
11
+ function get_object() {
12
+ return 'any';
13
+ }
14
+
15
+ function get_id() {
16
+ return false;
17
+ }
18
+ }
19
+
core/item-attachment.php ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Item_Attachment extends P2P_Item_Post {
4
+
5
+ function get_title() {
6
+ if( wp_attachment_is_image( $this->item->ID ) )
7
+ return wp_get_attachment_image( $this->item->ID, 'thumbnail', false );
8
+
9
+ return get_the_title( $this->item );
10
+ }
11
+ }
12
+
core/item-post.php ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Item_Post extends P2P_Item {
4
+
5
+ function get_title() {
6
+ return get_the_title( $this->item );
7
+ }
8
+
9
+ function get_permalink() {
10
+ return get_permalink( $this->item );
11
+ }
12
+
13
+ function get_editlink() {
14
+ return get_edit_post_link( $this->item );
15
+ }
16
+ }
17
+
core/item-user.php ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Item_User extends P2P_Item {
4
+
5
+ function get_title() {
6
+ return $this->item->display_name;
7
+ }
8
+
9
+ function get_permalink() {
10
+ return get_author_posts_url( $this->item->ID );
11
+ }
12
+
13
+ function get_editlink() {
14
+ return get_edit_user_link( $this->item->ID );
15
+ }
16
+ }
17
+
18
+
19
+ // WP < 3.5
20
+ if ( !function_exists( 'get_edit_user_link' ) ) :
21
+ function get_edit_user_link( $user_id = null ) {
22
+ if ( ! $user_id )
23
+ $user_id = get_current_user_id();
24
+
25
+ if ( empty( $user_id ) || ! current_user_can( 'edit_user', $user_id ) )
26
+ return '';
27
+
28
+ $user = new WP_User( $user_id );
29
+
30
+ if ( ! $user->exists() )
31
+ return '';
32
+
33
+ if ( get_current_user_id() == $user->ID )
34
+ $link = get_edit_profile_url( $user->ID );
35
+ else
36
+ $link = add_query_arg( 'user_id', $user->ID, self_admin_url( 'user-edit.php' ) );
37
+
38
+ return apply_filters( 'get_edit_user_link', $link, $user->ID );
39
+ }
40
+ endif;
41
+
core/item.php CHANGED
@@ -32,85 +32,3 @@ abstract class P2P_Item {
32
  abstract function get_title();
33
  }
34
 
35
-
36
- class P2P_Item_Any extends P2P_Item {
37
-
38
- function __construct() {}
39
-
40
- function get_permalink() {}
41
-
42
- function get_title() {}
43
-
44
- function get_object() {
45
- return 'any';
46
- }
47
-
48
- function get_id() {
49
- return false;
50
- }
51
- }
52
-
53
-
54
- class P2P_Item_Post extends P2P_Item {
55
-
56
- function get_title() {
57
- return get_the_title( $this->item );
58
- }
59
-
60
- function get_permalink() {
61
- return get_permalink( $this->item );
62
- }
63
-
64
- function get_editlink() {
65
- return get_edit_post_link( $this->item );
66
- }
67
- }
68
-
69
-
70
- class P2P_Item_Attachment extends P2P_Item_Post {
71
-
72
- function get_title() {
73
- return wp_get_attachment_image( $this->item->ID, 'thumbnail', false );
74
- }
75
- }
76
-
77
-
78
- class P2P_Item_User extends P2P_Item {
79
-
80
- function get_title() {
81
- return $this->item->display_name;
82
- }
83
-
84
- function get_permalink() {
85
- return get_author_posts_url( $this->item->ID );
86
- }
87
-
88
- function get_editlink() {
89
- return get_edit_user_link( $this->item->ID );
90
- }
91
- }
92
-
93
-
94
- // WP < 3.5
95
- if ( !function_exists( 'get_edit_user_link' ) ) :
96
- function get_edit_user_link( $user_id = null ) {
97
- if ( ! $user_id )
98
- $user_id = get_current_user_id();
99
-
100
- if ( empty( $user_id ) || ! current_user_can( 'edit_user', $user_id ) )
101
- return '';
102
-
103
- $user = new WP_User( $user_id );
104
-
105
- if ( ! $user->exists() )
106
- return '';
107
-
108
- if ( get_current_user_id() == $user->ID )
109
- $link = get_edit_profile_url( $user->ID );
110
- else
111
- $link = add_query_arg( 'user_id', $user->ID, self_admin_url( 'user-edit.php' ) );
112
-
113
- return apply_filters( 'get_edit_user_link', $link, $user->ID );
114
- }
115
- endif;
116
-
32
  abstract function get_title();
33
  }
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/list.php CHANGED
@@ -26,36 +26,34 @@ class P2P_List {
26
  'echo' => true
27
  ) );
28
 
29
- extract( $args, EXTR_SKIP );
 
 
30
 
31
- if ( $separator ) {
32
- if ( '<ul>' == $before_list )
33
- $before_list = '';
34
-
35
- if ( '</ul>' == $after_list )
36
- $after_list = '';
37
  }
38
 
39
- if ( !$echo )
40
  ob_start();
41
 
42
- echo $before_list;
43
 
44
- if ( $separator ) {
45
  $list = array();
46
  foreach ( $this->items as $item ) {
47
  $list[] = $this->render_item( $item );
48
  }
49
- echo implode( $separator, $list );
50
  } else {
51
  foreach ( $this->items as $item ) {
52
- echo $before_item . $this->render_item( $item ) . $after_item;
53
  }
54
  }
55
 
56
- echo $after_list;
57
 
58
- if ( !$echo )
59
  return ob_get_clean();
60
  }
61
 
26
  'echo' => true
27
  ) );
28
 
29
+ if ( $args['separator'] ) {
30
+ if ( '<ul>' == $args['before_list'] )
31
+ $args['before_list'] = '';
32
 
33
+ if ( '</ul>' == $args['after_list'] )
34
+ $args['after_list'] = '';
 
 
 
 
35
  }
36
 
37
+ if ( !$args['echo'] )
38
  ob_start();
39
 
40
+ echo $args['before_list'];
41
 
42
+ if ( $args['separator'] ) {
43
  $list = array();
44
  foreach ( $this->items as $item ) {
45
  $list[] = $this->render_item( $item );
46
  }
47
+ echo implode( $args['separator'], $list );
48
  } else {
49
  foreach ( $this->items as $item ) {
50
+ echo $args['before_item'] . $this->render_item( $item ) . $args['after_item'];
51
  }
52
  }
53
 
54
+ echo $args['after_list'];
55
 
56
+ if ( !$args['echo'] )
57
  return ob_get_clean();
58
  }
59
 
core/query-post.php CHANGED
@@ -1,6 +1,6 @@
1
  <?php
2
 
3
- class P2P_Post_Query {
4
 
5
  static function init() {
6
  add_action( 'parse_query', array( __CLASS__, 'parse_query' ), 20 );
@@ -59,5 +59,3 @@ class P2P_Post_Query {
59
  }
60
  }
61
 
62
- P2P_Post_Query::init();
63
-
1
  <?php
2
 
3
+ class P2P_Query_Post {
4
 
5
  static function init() {
6
  add_action( 'parse_query', array( __CLASS__, 'parse_query' ), 20 );
59
  }
60
  }
61
 
 
 
core/query-user.php CHANGED
@@ -1,6 +1,6 @@
1
  <?php
2
 
3
- class P2P_User_Query {
4
 
5
  static function init() {
6
  add_action( 'pre_user_query', array( __CLASS__, 'pre_user_query' ), 20 );
@@ -43,5 +43,3 @@ class P2P_User_Query {
43
  }
44
  }
45
 
46
- P2P_User_Query::init();
47
-
1
  <?php
2
 
3
+ class P2P_Query_User {
4
 
5
  static function init() {
6
  add_action( 'pre_user_query', array( __CLASS__, 'pre_user_query' ), 20 );
43
  }
44
  }
45
 
 
 
core/reciprocal-connection-type.php ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Reciprocal_Connection_Type extends P2P_Indeterminate_Connection_Type {
4
+
5
+ protected function choose_direction( $direction ) {
6
+ return 'any';
7
+ }
8
+
9
+ function _directions_for_admin( $direction, $show_ui ) {
10
+ if ( $show_ui )
11
+ $directions = array( 'any' );
12
+ else
13
+ $directions = array();
14
+
15
+ return $directions;
16
+ }
17
+ }
18
+
core/shortcodes.php ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Shortcodes {
4
+
5
+ static function init() {
6
+ add_shortcode( 'p2p_connected', array( __CLASS__, 'connected' ) );
7
+ add_shortcode( 'p2p_related', array( __CLASS__, 'related' ) );
8
+ }
9
+
10
+ static function connected( $attr ) {
11
+ return self::get_list( $attr, 'get_connected' );
12
+ }
13
+
14
+ static function related( $attr ) {
15
+ return self::get_list( $attr, 'get_related' );
16
+ }
17
+
18
+ private static function get_list( $attr, $method ) {
19
+ global $post;
20
+
21
+ $attr = shortcode_atts( array(
22
+ 'type' => '',
23
+ 'mode' => 'ul',
24
+ ), $attr );
25
+
26
+ return _p2p_get_list( array(
27
+ 'ctype' => $attr['type'],
28
+ 'method' => $method,
29
+ 'item' => $post,
30
+ 'mode' => $attr['mode'],
31
+ 'context' => 'shortcode'
32
+ ) );
33
+ }
34
+ }
35
+
core/side-attachment.php ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Side_Attachment extends P2P_Side_Post {
4
+
5
+ protected $item_type = 'P2P_Item_Attachment';
6
+
7
+ function __construct( $query_vars ) {
8
+ $this->query_vars = $query_vars;
9
+
10
+ $this->query_vars['post_type'] = array( 'attachment' );
11
+ }
12
+
13
+ function can_create_item() {
14
+ return false;
15
+ }
16
+
17
+ function get_base_qv( $q ) {
18
+ return array_merge( parent::get_base_qv( $q ), array(
19
+ 'post_status' => 'inherit'
20
+ ) );
21
+ }
22
+ }
23
+
core/side-post.php ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Side_Post extends P2P_Side {
4
+
5
+ protected $item_type = 'P2P_Item_Post';
6
+
7
+ function __construct( $query_vars ) {
8
+ $this->query_vars = $query_vars;
9
+ }
10
+
11
+ public function get_object_type() {
12
+ return 'post';
13
+ }
14
+
15
+ public function first_post_type() {
16
+ return $this->query_vars['post_type'][0];
17
+ }
18
+
19
+ private function get_ptype() {
20
+ return get_post_type_object( $this->first_post_type() );
21
+ }
22
+
23
+ function get_base_qv( $q ) {
24
+ if ( isset( $q['post_type'] ) && 'any' != $q['post_type'] ) {
25
+ $common = array_intersect( $this->query_vars['post_type'], (array) $q['post_type'] );
26
+
27
+ if ( !$common )
28
+ unset( $q['post_type'] );
29
+ }
30
+
31
+ return array_merge( $this->query_vars, $q, array(
32
+ 'suppress_filters' => false,
33
+ 'ignore_sticky_posts' => true,
34
+ ) );
35
+ }
36
+
37
+ function get_desc() {
38
+ return implode( ', ', array_map( array( $this, 'post_type_label' ), $this->query_vars['post_type'] ) );
39
+ }
40
+
41
+ private function post_type_label( $post_type ) {
42
+ $cpt = get_post_type_object( $post_type );
43
+ return $cpt ? $cpt->label : $post_type;
44
+ }
45
+
46
+ function get_title() {
47
+ return $this->get_ptype()->labels->name;
48
+ }
49
+
50
+ function get_labels() {
51
+ return $this->get_ptype()->labels;
52
+ }
53
+
54
+ function can_edit_connections() {
55
+ return current_user_can( $this->get_ptype()->cap->edit_posts );
56
+ }
57
+
58
+ function can_create_item() {
59
+ if ( count( $this->query_vars['post_type'] ) > 1 )
60
+ return false;
61
+
62
+ if ( count( $this->query_vars ) > 1 )
63
+ return false;
64
+
65
+ return true;
66
+ }
67
+
68
+ function translate_qv( $qv ) {
69
+ $map = array(
70
+ 'include' => 'post__in',
71
+ 'exclude' => 'post__not_in',
72
+ 'search' => 's',
73
+ 'page' => 'paged',
74
+ 'per_page' => 'posts_per_page'
75
+ );
76
+
77
+ foreach ( $map as $old => $new )
78
+ if ( isset( $qv["p2p:$old"] ) )
79
+ $qv[$new] = _p2p_pluck( $qv, "p2p:$old" );
80
+
81
+ return $qv;
82
+ }
83
+
84
+ function do_query( $args ) {
85
+ return new WP_Query( $args );
86
+ }
87
+
88
+ function capture_query( $args ) {
89
+ $q = new WP_Query;
90
+ $q->_p2p_capture = true;
91
+
92
+ $q->query( $args );
93
+
94
+ return $q->_p2p_sql;
95
+ }
96
+
97
+ function get_list( $wp_query ) {
98
+ $list = new P2P_List( $wp_query->posts, $this->item_type );
99
+
100
+ $list->current_page = max( 1, $wp_query->get('paged') );
101
+ $list->total_pages = $wp_query->max_num_pages;
102
+
103
+ return $list;
104
+ }
105
+
106
+ function is_indeterminate( $side ) {
107
+ $common = array_intersect(
108
+ $this->query_vars['post_type'],
109
+ $side->query_vars['post_type']
110
+ );
111
+
112
+ return !empty( $common );
113
+ }
114
+
115
+ protected function recognize( $arg ) {
116
+ if ( is_object( $arg ) && !isset( $arg->post_type ) )
117
+ return false;
118
+
119
+ $post = get_post( $arg );
120
+
121
+ if ( !is_object( $post ) )
122
+ return false;
123
+
124
+ if ( !$this->recognize_post_type( $post->post_type ) )
125
+ return false;
126
+
127
+ return $post;
128
+ }
129
+
130
+ public function recognize_post_type( $post_type ) {
131
+ if ( !post_type_exists( $post_type ) )
132
+ return false;
133
+
134
+ return in_array( $post_type, $this->query_vars['post_type'] );
135
+ }
136
+ }
137
+
138
+
core/side-user.php ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class P2P_Side_User extends P2P_Side {
4
+
5
+ protected $item_type = 'P2P_Item_User';
6
+
7
+ function __construct( $query_vars ) {
8
+ $this->query_vars = $query_vars;
9
+ }
10
+
11
+ function get_object_type() {
12
+ return 'user';
13
+ }
14
+
15
+ function get_desc() {
16
+ return __( 'Users', P2P_TEXTDOMAIN );
17
+ }
18
+
19
+ function get_title() {
20
+ return $this->get_desc();
21
+ }
22
+
23
+ function get_labels() {
24
+ return (object) array(
25
+ 'singular_name' => __( 'User', P2P_TEXTDOMAIN ),
26
+ 'search_items' => __( 'Search Users', P2P_TEXTDOMAIN ),
27
+ 'not_found' => __( 'No users found.', P2P_TEXTDOMAIN ),
28
+ );
29
+ }
30
+
31
+ function can_edit_connections() {
32
+ return current_user_can( 'list_users' );
33
+ }
34
+
35
+ function can_create_item() {
36
+ return false;
37
+ }
38
+
39
+ function translate_qv( $qv ) {
40
+ if ( isset( $qv['p2p:include'] ) )
41
+ $qv['include'] = _p2p_pluck( $qv, 'p2p:include' );
42
+
43
+ if ( isset( $qv['p2p:exclude'] ) )
44
+ $qv['exclude'] = _p2p_pluck( $qv, 'p2p:exclude' );
45
+
46
+ if ( isset( $qv['p2p:search'] ) && $qv['p2p:search'] )
47
+ $qv['search'] = '*' . _p2p_pluck( $qv, 'p2p:search' ) . '*';
48
+
49
+ if ( isset( $qv['p2p:page'] ) && $qv['p2p:page'] > 0 ) {
50
+ if ( isset( $qv['p2p:per_page'] ) && $qv['p2p:per_page'] > 0 ) {
51
+ $qv['number'] = $qv['p2p:per_page'];
52
+ $qv['offset'] = $qv['p2p:per_page'] * ( $qv['p2p:page'] - 1 );
53
+ }
54
+ }
55
+
56
+ return $qv;
57
+ }
58
+
59
+ function do_query( $args ) {
60
+ return new WP_User_Query( $args );
61
+ }
62
+
63
+ function capture_query( $args ) {
64
+ $args['count_total'] = false;
65
+
66
+ $uq = new WP_User_Query;
67
+ $uq->_p2p_capture = true; // needed by P2P_URL_Query
68
+
69
+ // see http://core.trac.wordpress.org/ticket/21119
70
+ $uq->query_vars = wp_parse_args( $args, array(
71
+ 'blog_id' => $GLOBALS['blog_id'],
72
+ 'role' => '',
73
+ 'meta_key' => '',
74
+ 'meta_value' => '',
75
+ 'meta_compare' => '',
76
+ 'include' => array(),
77
+ 'exclude' => array(),
78
+ 'search' => '',
79
+ 'search_columns' => array(),
80
+ 'orderby' => 'login',
81
+ 'order' => 'ASC',
82
+ 'offset' => '',
83
+ 'number' => '',
84
+ 'count_total' => true,
85
+ 'fields' => 'all',
86
+ 'who' => ''
87
+ ) );
88
+
89
+ $uq->prepare_query();
90
+
91
+ return "SELECT $uq->query_fields $uq->query_from $uq->query_where $uq->query_orderby $uq->query_limit";
92
+ }
93
+
94
+ function get_list( $query ) {
95
+ $list = new P2P_List( $query->get_results(), $this->item_type );
96
+
97
+ $qv = $query->query_vars;
98
+
99
+ if ( isset( $qv['p2p:page'] ) ) {
100
+ $list->current_page = $qv['p2p:page'];
101
+ $list->total_pages = ceil( $query->get_total() / $qv['p2p:per_page'] );
102
+ }
103
+
104
+ return $list;
105
+ }
106
+
107
+ function is_indeterminate( $side ) {
108
+ return true;
109
+ }
110
+
111
+ function get_base_qv( $q ) {
112
+ return array_merge( $this->query_vars, $q );
113
+ }
114
+
115
+ protected function recognize( $arg ) {
116
+ if ( is_a( $arg, 'WP_User' ) )
117
+ return $arg;
118
+
119
+ return get_user_by( 'id', $arg );
120
+ }
121
+ }
122
+
core/side.php CHANGED
@@ -52,280 +52,3 @@ abstract class P2P_Side {
52
  abstract protected function recognize( $arg );
53
  }
54
 
55
-
56
- class P2P_Side_Post extends P2P_Side {
57
-
58
- protected $item_type = 'P2P_Item_Post';
59
-
60
- function __construct( $query_vars ) {
61
- $this->query_vars = $query_vars;
62
- }
63
-
64
- public function get_object_type() {
65
- return 'post';
66
- }
67
-
68
- public function first_post_type() {
69
- return $this->query_vars['post_type'][0];
70
- }
71
-
72
- private function get_ptype() {
73
- return get_post_type_object( $this->first_post_type() );
74
- }
75
-
76
- function get_base_qv( $q ) {
77
- if ( isset( $q['post_type'] ) && 'any' != $q['post_type'] ) {
78
- $common = array_intersect( $this->query_vars['post_type'], (array) $q['post_type'] );
79
-
80
- if ( !$common )
81
- unset( $q['post_type'] );
82
- }
83
-
84
- return array_merge( $this->query_vars, $q, array(
85
- 'suppress_filters' => false,
86
- 'ignore_sticky_posts' => true,
87
- ) );
88
- }
89
-
90
- function get_desc() {
91
- return implode( ', ', array_map( array( $this, 'post_type_label' ), $this->query_vars['post_type'] ) );
92
- }
93
-
94
- private function post_type_label( $post_type ) {
95
- $cpt = get_post_type_object( $post_type );
96
- return $cpt ? $cpt->label : $post_type;
97
- }
98
-
99
- function get_title() {
100
- return $this->get_ptype()->labels->name;
101
- }
102
-
103
- function get_labels() {
104
- return $this->get_ptype()->labels;
105
- }
106
-
107
- function can_edit_connections() {
108
- return current_user_can( $this->get_ptype()->cap->edit_posts );
109
- }
110
-
111
- function can_create_item() {
112
- if ( count( $this->query_vars['post_type'] ) > 1 )
113
- return false;
114
-
115
- if ( count( $this->query_vars ) > 1 )
116
- return false;
117
-
118
- return true;
119
- }
120
-
121
- function translate_qv( $qv ) {
122
- $map = array(
123
- 'include' => 'post__in',
124
- 'exclude' => 'post__not_in',
125
- 'search' => 's',
126
- 'page' => 'paged',
127
- 'per_page' => 'posts_per_page'
128
- );
129
-
130
- foreach ( $map as $old => $new )
131
- if ( isset( $qv["p2p:$old"] ) )
132
- $qv[$new] = _p2p_pluck( $qv, "p2p:$old" );
133
-
134
- return $qv;
135
- }
136
-
137
- function do_query( $args ) {
138
- return new WP_Query( $args );
139
- }
140
-
141
- function capture_query( $args ) {
142
- $q = new WP_Query;
143
- $q->_p2p_capture = true;
144
-
145
- $q->query( $args );
146
-
147
- return $q->_p2p_sql;
148
- }
149
-
150
- function get_list( $wp_query ) {
151
- $list = new P2P_List( $wp_query->posts, $this->item_type );
152
-
153
- $list->current_page = max( 1, $wp_query->get('paged') );
154
- $list->total_pages = $wp_query->max_num_pages;
155
-
156
- return $list;
157
- }
158
-
159
- function is_indeterminate( $side ) {
160
- $common = array_intersect(
161
- $this->query_vars['post_type'],
162
- $side->query_vars['post_type']
163
- );
164
-
165
- return !empty( $common );
166
- }
167
-
168
- protected function recognize( $arg ) {
169
- if ( is_object( $arg ) && !isset( $arg->post_type ) )
170
- return false;
171
-
172
- $post = get_post( $arg );
173
-
174
- if ( !is_object( $post ) )
175
- return false;
176
-
177
- if ( !$this->recognize_post_type( $post->post_type ) )
178
- return false;
179
-
180
- return $post;
181
- }
182
-
183
- public function recognize_post_type( $post_type ) {
184
- if ( !post_type_exists( $post_type ) )
185
- return false;
186
-
187
- return in_array( $post_type, $this->query_vars['post_type'] );
188
- }
189
- }
190
-
191
-
192
- class P2P_Side_Attachment extends P2P_Side_Post {
193
-
194
- protected $item_type = 'P2P_Item_Attachment';
195
-
196
- function __construct( $query_vars ) {
197
- $this->query_vars = $query_vars;
198
-
199
- $this->query_vars['post_type'] = array( 'attachment' );
200
- }
201
-
202
- function can_create_item() {
203
- return false;
204
- }
205
-
206
- function get_base_qv( $q ) {
207
- return array_merge( parent::get_base_qv( $q ), array(
208
- 'post_status' => 'inherit'
209
- ) );
210
- }
211
- }
212
-
213
-
214
- class P2P_Side_User extends P2P_Side {
215
-
216
- protected $item_type = 'P2P_Item_User';
217
-
218
- function __construct( $query_vars ) {
219
- $this->query_vars = $query_vars;
220
- }
221
-
222
- function get_object_type() {
223
- return 'user';
224
- }
225
-
226
- function get_desc() {
227
- return __( 'Users', P2P_TEXTDOMAIN );
228
- }
229
-
230
- function get_title() {
231
- return $this->get_desc();
232
- }
233
-
234
- function get_labels() {
235
- return (object) array(
236
- 'singular_name' => __( 'User', P2P_TEXTDOMAIN ),
237
- 'search_items' => __( 'Search Users', P2P_TEXTDOMAIN ),
238
- 'not_found' => __( 'No users found.', P2P_TEXTDOMAIN ),
239
- );
240
- }
241
-
242
- function can_edit_connections() {
243
- return current_user_can( 'list_users' );
244
- }
245
-
246
- function can_create_item() {
247
- return false;
248
- }
249
-
250
- function translate_qv( $qv ) {
251
- if ( isset( $qv['p2p:include'] ) )
252
- $qv['include'] = _p2p_pluck( $qv, 'p2p:include' );
253
-
254
- if ( isset( $qv['p2p:exclude'] ) )
255
- $qv['exclude'] = _p2p_pluck( $qv, 'p2p:exclude' );
256
-
257
- if ( isset( $qv['p2p:search'] ) && $qv['p2p:search'] )
258
- $qv['search'] = '*' . _p2p_pluck( $qv, 'p2p:search' ) . '*';
259
-
260
- if ( isset( $qv['p2p:page'] ) && $qv['p2p:page'] > 0 ) {
261
- $qv['number'] = $qv['p2p:per_page'];
262
- $qv['offset'] = $qv['p2p:per_page'] * ( $qv['p2p:page'] - 1 );
263
- }
264
-
265
- return $qv;
266
- }
267
-
268
- function do_query( $args ) {
269
- return new WP_User_Query( $args );
270
- }
271
-
272
- function capture_query( $args ) {
273
- $args['count_total'] = false;
274
-
275
- $uq = new WP_User_Query;
276
- $uq->_p2p_capture = true; // needed by P2P_URL_Query
277
-
278
- // see http://core.trac.wordpress.org/ticket/21119
279
- $uq->query_vars = wp_parse_args( $args, array(
280
- 'blog_id' => $GLOBALS['blog_id'],
281
- 'role' => '',
282
- 'meta_key' => '',
283
- 'meta_value' => '',
284
- 'meta_compare' => '',
285
- 'include' => array(),
286
- 'exclude' => array(),
287
- 'search' => '',
288
- 'search_columns' => array(),
289
- 'orderby' => 'login',
290
- 'order' => 'ASC',
291
- 'offset' => '',
292
- 'number' => '',
293
- 'count_total' => true,
294
- 'fields' => 'all',
295
- 'who' => ''
296
- ) );
297
-
298
- $uq->prepare_query();
299
-
300
- return "SELECT $uq->query_fields $uq->query_from $uq->query_where $uq->query_orderby $uq->query_limit";
301
- }
302
-
303
- function get_list( $query ) {
304
- $list = new P2P_List( $query->get_results(), $this->item_type );
305
-
306
- $qv = $query->query_vars;
307
-
308
- if ( isset( $qv['p2p:page'] ) ) {
309
- $list->current_page = $qv['p2p:page'];
310
- $list->total_pages = ceil( $query->get_total() / $qv['p2p:per_page'] );
311
- }
312
-
313
- return $list;
314
- }
315
-
316
- function is_indeterminate( $side ) {
317
- return true;
318
- }
319
-
320
- function get_base_qv( $q ) {
321
- return array_merge( $this->query_vars, $q );
322
- }
323
-
324
- protected function recognize( $arg ) {
325
- if ( is_a( $arg, 'WP_User' ) )
326
- return $arg;
327
-
328
- return get_user_by( 'id', $arg );
329
- }
330
- }
331
-
52
  abstract protected function recognize( $arg );
53
  }
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/storage.php CHANGED
@@ -60,5 +60,3 @@ class P2P_Storage {
60
  }
61
  }
62
 
63
- P2P_Storage::init();
64
-
60
  }
61
  }
62
 
 
 
core/url-query.php CHANGED
@@ -8,36 +8,12 @@ class P2P_URL_Query {
8
 
9
  static function init() {
10
  add_filter( 'query_vars', array( __CLASS__, 'query_vars' ) );
11
-
12
- if ( is_admin() )
13
- add_action( 'pre_user_query', array( __CLASS__, 'user_query' ), 9 );
14
  }
15
 
16
  // Make the query vars public
17
  static function query_vars( $public_qv ) {
18
  return array_merge( $public_qv, self::get_custom_qv() );
19
  }
20
-
21
- // Add the query vars to the global user query (on the user admin screen)
22
- static function user_query( $query ) {
23
- if ( !function_exists( 'get_current_screen' ) )
24
- return;
25
-
26
- $current_screen = get_current_screen();
27
-
28
- if ( $current_screen && 'users' != $current_screen->id )
29
- return;
30
-
31
- if ( isset( $query->_p2p_capture ) )
32
- return;
33
-
34
- // Don't overwrite existing P2P query
35
- if ( isset( $query->query_vars['connected_type'] ) )
36
- return;
37
-
38
- _p2p_append( $query->query_vars, wp_array_slice_assoc( $_GET,
39
- self::get_custom_qv() ) );
40
- }
41
  }
42
 
43
  P2P_URL_Query::init();
8
 
9
  static function init() {
10
  add_filter( 'query_vars', array( __CLASS__, 'query_vars' ) );
 
 
 
11
  }
12
 
13
  // Make the query vars public
14
  static function query_vars( $public_qv ) {
15
  return array_merge( $public_qv, self::get_custom_qv() );
16
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  }
18
 
19
  P2P_URL_Query::init();
core/util.php CHANGED
@@ -1,5 +1,20 @@
1
  <?php
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  /** @internal */
4
  function _p2p_expand_direction( $direction ) {
5
  if ( !$direction )
@@ -107,3 +122,63 @@ function _p2p_first( $args ) {
107
  return reset( $args );
108
  }
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <?php
2
 
3
+ function p2p_list_cluster( $items, $callback ) {
4
+ $groups = array();
5
+
6
+ foreach ( $items as $item ) {
7
+ $key = $callback( $item );
8
+
9
+ if ( null === $key )
10
+ continue;
11
+
12
+ $groups[ $key ][] = $item;
13
+ }
14
+
15
+ return $groups;
16
+ }
17
+
18
  /** @internal */
19
  function _p2p_expand_direction( $direction ) {
20
  if ( !$direction )
122
  return reset( $args );
123
  }
124
 
125
+ /** @internal */
126
+ function _p2p_get_other_id( $item ) {
127
+ if ( $item->ID == $item->p2p_from )
128
+ return $item->p2p_to;
129
+
130
+ if ( $item->ID == $item->p2p_to )
131
+ return $item->p2p_from;
132
+
133
+ trigger_error( "Corrupted data for item $inner_item->ID", E_USER_WARNING );
134
+ }
135
+
136
+ /** @internal */
137
+ function _p2p_get_list( $args ) {
138
+ $ctype = p2p_type( $args['ctype'] );
139
+ if ( !$ctype ) {
140
+ trigger_error( sprintf( "Unregistered connection type '%s'.", $ctype ), E_USER_WARNING );
141
+ return '';
142
+ }
143
+
144
+ $directed = $ctype->find_direction( $args['item'] );
145
+ if ( !$directed )
146
+ return '';
147
+
148
+ $context = $args['context'];
149
+
150
+ $extra_qv = array(
151
+ 'p2p:per_page' => -1,
152
+ 'p2p:context' => $context
153
+ );
154
+
155
+ $connected = call_user_func( array( $directed, $args['method'] ), $args['item'], $extra_qv, 'abstract' );
156
+
157
+ switch ( $args['mode'] ) {
158
+ case 'inline':
159
+ $render_args = array(
160
+ 'separator' => ', '
161
+ );
162
+ break;
163
+
164
+ case 'ol':
165
+ $render_args = array(
166
+ 'before_list' => '<ol id="' . $ctype->name . '_list">',
167
+ 'after_list' => '</ol>',
168
+ );
169
+ break;
170
+
171
+ case 'ul':
172
+ default:
173
+ $render_args = array(
174
+ 'before_list' => '<ul id="' . $ctype->name . '_list">',
175
+ 'after_list' => '</ul>',
176
+ );
177
+ break;
178
+ }
179
+
180
+ $render_args['echo'] = false;
181
+
182
+ return apply_filters( "p2p_{$context}_html", $connected->render( $render_args ), $connected, $directed, $args['mode'] );
183
+ }
184
+
core/{extra.php → widget.php} RENAMED
@@ -85,87 +85,3 @@ class P2P_Widget extends scbWidget {
85
  }
86
  }
87
 
88
-
89
- class P2P_Shortcodes {
90
-
91
- static function init() {
92
- add_shortcode( 'p2p_connected', array( __CLASS__, 'connected' ) );
93
- add_shortcode( 'p2p_related', array( __CLASS__, 'related' ) );
94
- }
95
-
96
- static function connected( $attr ) {
97
- return self::get_list( $attr, 'get_connected' );
98
- }
99
-
100
- static function related( $attr ) {
101
- return self::get_list( $attr, 'get_related' );
102
- }
103
-
104
- private static function get_list( $attr, $method ) {
105
- global $post;
106
-
107
- $attr = shortcode_atts( array(
108
- 'type' => '',
109
- 'mode' => 'ul',
110
- ), $attr );
111
-
112
- return _p2p_get_list( array(
113
- 'ctype' => $attr['type'],
114
- 'method' => $method,
115
- 'item' => $post,
116
- 'mode' => $attr['mode'],
117
- 'context' => 'shortcode'
118
- ) );
119
- }
120
- }
121
-
122
-
123
- /** @internal */
124
- function _p2p_get_list( $args ) {
125
- extract( $args );
126
-
127
- $ctype = p2p_type( $ctype );
128
- if ( !$ctype ) {
129
- trigger_error( sprintf( "Unregistered connection type '%s'.", $ctype ), E_USER_WARNING );
130
- return '';
131
- }
132
-
133
- $directed = $ctype->find_direction( $item );
134
- if ( !$directed )
135
- return '';
136
-
137
- $extra_qv = array(
138
- 'p2p:per_page' => -1,
139
- 'p2p:context' => $context
140
- );
141
-
142
- $connected = $directed->$method( $item, $extra_qv, 'abstract' );
143
-
144
- switch ( $mode ) {
145
- case 'inline':
146
- $args = array(
147
- 'separator' => ', '
148
- );
149
- break;
150
-
151
- case 'ol':
152
- $args = array(
153
- 'before_list' => '<ol id="' . $ctype->name . '_list">',
154
- 'after_list' => '</ol>',
155
- );
156
- break;
157
-
158
- case 'ul':
159
- default:
160
- $args = array(
161
- 'before_list' => '<ul id="' . $ctype->name . '_list">',
162
- 'after_list' => '</ul>',
163
- );
164
- break;
165
- }
166
-
167
- $args['echo'] = false;
168
-
169
- return apply_filters( "p2p_{$context}_html", $connected->render( $args ), $connected, $directed, $mode );
170
- }
171
-
85
  }
86
  }
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
posts-to-posts.php CHANGED
@@ -2,7 +2,7 @@
2
  /*
3
  Plugin Name: Posts 2 Posts
4
  Description: Create many-to-many relationships between all types of posts.
5
- Version: 1.4.3
6
  Author: scribu
7
  Author URI: http://scribu.net/
8
  Plugin URI: http://scribu.net/wordpress/posts-to-posts
@@ -10,7 +10,7 @@ Text Domain: posts-to-posts
10
  Domain Path: /lang
11
  */
12
 
13
- define( 'P2P_PLUGIN_VERSION', '1.4.3' );
14
 
15
  define( 'P2P_TEXTDOMAIN', 'posts-to-posts' );
16
 
@@ -21,37 +21,40 @@ function _p2p_load() {
21
 
22
  load_plugin_textdomain( P2P_TEXTDOMAIN, '', basename( $base ) . '/lang' );
23
 
24
- _p2p_load_files( "$base/core", array(
25
- 'storage', 'query', 'query-post', 'query-user', 'url-query',
26
- 'util', 'item', 'list', 'side',
27
- 'type-factory', 'type', 'directed-type', 'indeterminate-type',
28
- 'api', 'extra'
29
- ) );
 
 
 
 
30
 
31
  P2P_Widget::init();
32
  P2P_Shortcodes::init();
33
 
34
- if ( is_admin() ) {
35
- _p2p_load_files( "$base/admin", array(
36
- 'mustache', 'factory',
37
- 'box-factory', 'box', 'fields',
38
- 'column-factory', 'column',
39
- 'tools'
40
- ) );
41
- }
42
 
43
  register_uninstall_hook( __FILE__, array( 'P2P_Storage', 'uninstall' ) );
44
  }
45
  scb_init( '_p2p_load' );
46
 
 
 
 
 
 
 
 
 
 
 
47
  function _p2p_init() {
48
  // Safe hook for calling p2p_register_connection_type()
49
  do_action( 'p2p_init' );
50
  }
51
  add_action( 'wp_loaded', '_p2p_init' );
52
 
53
- function _p2p_load_files( $dir, $files ) {
54
- foreach ( $files as $file )
55
- require_once "$dir/$file.php";
56
- }
57
-
2
  /*
3
  Plugin Name: Posts 2 Posts
4
  Description: Create many-to-many relationships between all types of posts.
5
+ Version: 1.5
6
  Author: scribu
7
  Author URI: http://scribu.net/
8
  Plugin URI: http://scribu.net/wordpress/posts-to-posts
10
  Domain Path: /lang
11
  */
12
 
13
+ define( 'P2P_PLUGIN_VERSION', '1.5' );
14
 
15
  define( 'P2P_TEXTDOMAIN', 'posts-to-posts' );
16
 
21
 
22
  load_plugin_textdomain( P2P_TEXTDOMAIN, '', basename( $base ) . '/lang' );
23
 
24
+ require $base . '/core/util.php';
25
+ require $base . '/core/api.php';
26
+ require $base . '/autoload.php';
27
+
28
+ P2P_Autoload::register( 'P2P_', $base . '/core' );
29
+
30
+ P2P_Storage::init();
31
+
32
+ P2P_Query_Post::init();
33
+ P2P_Query_User::init();
34
 
35
  P2P_Widget::init();
36
  P2P_Shortcodes::init();
37
 
38
+ if ( is_admin() )
39
+ _p2p_load_admin();
 
 
 
 
 
 
40
 
41
  register_uninstall_hook( __FILE__, array( 'P2P_Storage', 'uninstall' ) );
42
  }
43
  scb_init( '_p2p_load' );
44
 
45
+ function _p2p_load_admin() {
46
+ P2P_Autoload::register( 'P2P_', dirname( __FILE__ ) . '/admin' );
47
+
48
+ new P2P_Box_Factory;
49
+ new P2P_Column_Factory;
50
+ new P2P_Dropdown_Factory;
51
+
52
+ new P2P_Tools_Page;
53
+ }
54
+
55
  function _p2p_init() {
56
  // Safe hook for calling p2p_register_connection_type()
57
  do_action( 'p2p_init' );
58
  }
59
  add_action( 'wp_loaded', '_p2p_init' );
60
 
 
 
 
 
 
readme.txt CHANGED
@@ -1,9 +1,9 @@
1
  === Posts 2 Posts ===
2
  Contributors: scribu, ciobi
3
  Tags: connections, custom post types, relationships, many-to-many, users
4
- Requires at least: 3.4
5
  Tested up to: 3.5
6
- Stable tag: 1.4.3
7
  License: GPLv2 or later
8
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
 
@@ -53,6 +53,14 @@ Make sure your host is running PHP 5. The only foolproof way to do this is to ad
53
 
54
  == Changelog ==
55
 
 
 
 
 
 
 
 
 
56
  = 1.4.3 =
57
  * various bug fixes
58
  * added 'inline' mode for shortcodes
@@ -248,4 +256,3 @@ Make sure your host is running PHP 5. The only foolproof way to do this is to ad
248
  = 0.1 =
249
  * initial release
250
  * [more info](http://scribu.net/wordpress/posts-to-posts/p2p-0-1.html)
251
-
1
  === Posts 2 Posts ===
2
  Contributors: scribu, ciobi
3
  Tags: connections, custom post types, relationships, many-to-many, users
4
+ Requires at least: 3.5
5
  Tested up to: 3.5
6
+ Stable tag: 1.5
7
  License: GPLv2 or later
8
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
 
53
 
54
  == Changelog ==
55
 
56
+ = 1.5 =
57
+ * added [admin dropdowns](https://github.com/scribu/wp-posts-to-posts/wiki/Admin-dropdown-display)
58
+ * fixed SQL error related to user connections
59
+ * fixed 'labels' handling and added 'column_title' subkey
60
+ * refactor metabox JavaScript using Backbone.js
61
+ * lazy-load connection candidates, for faster page loads
62
+ * lazy-load PHP classes using `spl_register_autoload()`
63
+
64
  = 1.4.3 =
65
  * various bug fixes
66
  * added 'inline' mode for shortcodes
256
  = 0.1 =
257
  * initial release
258
  * [more info](http://scribu.net/wordpress/posts-to-posts/p2p-0-1.html)
 
scb/AdminPage.php CHANGED
@@ -1,13 +1,15 @@
1
  <?php
2
 
3
- // Administration page base class
4
-
 
5
  abstract class scbAdminPage {
6
  /** Page args
7
  * $page_title string (mandatory)
8
  * $parent (string) (default: options-general.php)
9
  * $capability (string) (default: 'manage_options')
10
  * $menu_title (string) (default: $page_title)
 
11
  * $page_slug (string) (default: sanitized $page_title)
12
  * $toplevel (string) If not empty, will create a new top level menu (for expected values see http://codex.wordpress.org/Administration_Menus#Using_add_submenu_page)
13
  * - $icon_url (string) URL to an icon for the top level menu
@@ -40,6 +42,13 @@ abstract class scbAdminPage {
40
 
41
  private static $registered = array();
42
 
 
 
 
 
 
 
 
43
  static function register( $class, $file, $options = null ) {
44
  if ( isset( self::$registered[$class] ) )
45
  return false;
@@ -51,6 +60,12 @@ abstract class scbAdminPage {
51
  return true;
52
  }
53
 
 
 
 
 
 
 
54
  static function replace( $old_class, $new_class ) {
55
  if ( ! isset( self::$registered[$old_class] ) )
56
  return false;
@@ -61,6 +76,11 @@ abstract class scbAdminPage {
61
  return true;
62
  }
63
 
 
 
 
 
 
64
  static function remove( $class ) {
65
  if ( ! isset( self::$registered[$class] ) )
66
  return false;
@@ -79,7 +99,12 @@ abstract class scbAdminPage {
79
  // ____________MAIN METHODS____________
80
 
81
 
82
- // Constructor
 
 
 
 
 
83
  function __construct( $file = false, $options = null ) {
84
  if ( is_a( $options, 'scbOptions' ) )
85
  $this->options = $options;
@@ -105,7 +130,9 @@ abstract class scbAdminPage {
105
  }
106
  }
107
 
108
- // This is where all the page args can be set
 
 
109
  function setup(){}
110
 
111
  /**
@@ -115,43 +142,64 @@ abstract class scbAdminPage {
115
  */
116
  function page_loaded() {}
117
 
118
- // This is where the css and js go
119
- // Both wp_enqueue_*() and inline code can be added
 
 
120
  function page_head(){}
121
 
122
- // This is where the contextual help goes
123
- // @return string
 
 
124
  function page_help(){}
125
 
126
- // A generic page header
 
 
127
  function page_header() {
128
  echo "<div class='wrap'>\n";
129
  screen_icon( $this->args['screen_icon'] );
130
- echo html( "h2", $this->args['page_title'] );
131
  }
132
 
133
- // This is where the page content goes
 
 
134
  abstract function page_content();
135
 
136
- // A generic page footer
 
 
137
  function page_footer() {
138
  echo "</div>\n";
139
  }
140
 
141
- // This is where the form data should be validated
 
 
 
 
 
 
 
142
  function validate( $new_data, $old_data ) {
143
  return $new_data;
144
  }
145
 
146
- // Manually handle option saving ( use Settings API instead )
 
 
 
 
147
  function form_handler() {
148
- if ( empty( $_POST['action'] ) )
149
  return false;
150
 
151
  check_admin_referer( $this->nonce );
152
 
153
  if ( !isset($this->options) ) {
154
- trigger_error('options handler not set', E_USER_WARNING);
155
  return false;
156
  }
157
 
@@ -164,10 +212,17 @@ abstract class scbAdminPage {
164
  $this->options->set( $new_data );
165
 
166
  $this->admin_msg();
 
 
167
  }
168
 
169
- // Manually generate a standard admin notice ( use Settings API instead )
170
- function admin_msg( $msg = '', $class = "updated" ) {
 
 
 
 
 
171
  if ( empty( $msg ) )
172
  $msg = __( 'Settings <strong>saved</strong>.', $this->textdomain );
173
 
@@ -178,52 +233,47 @@ abstract class scbAdminPage {
178
  // ____________UTILITIES____________
179
 
180
 
181
- // Generates a form submit button
182
- function submit_button( $value = '', $action = 'action', $class = "button" ) {
183
- if ( is_array( $value ) ) {
184
- extract( wp_parse_args( $value, array(
185
- 'value' => __( 'Save Changes', $this->textdomain ),
186
- 'action' => 'action',
187
- 'class' => 'button',
188
- 'ajax' => true
189
- ) ) );
190
-
191
- if ( ! $ajax )
192
- $class .= ' no-ajax';
193
- }
194
- else {
195
- if ( empty( $value ) )
196
- $value = __( 'Save Changes', $this->textdomain );
197
- }
198
-
199
- $input_args = array(
200
- 'type' => 'submit',
201
- 'name' => $action,
202
- 'value' => $value,
203
- 'extra' => '',
204
- 'desc' => false,
205
- 'wrap' => html( 'p class="submit"', scbForms::TOKEN )
206
- );
207
 
208
- if ( ! empty( $class ) )
209
- $input_args['extra'] = compact( 'class' );
 
 
 
 
210
 
211
- return scbForms::input( $input_args );
212
  }
213
 
214
- /*
215
- Mimics scbForms::form_wrap()
216
-
217
- $this->form_wrap( $content ); // generates a form with a default submit button
218
-
219
- $this->form_wrap( $content, false ); // generates a form with no submit button
220
-
221
- // the second argument is sent to submit_button()
222
- $this->form_wrap( $content, array( 'text' => 'Save changes',
223
- 'name' => 'action',
224
- 'ajax' => true,
225
- ) );
226
- */
 
 
 
 
 
 
 
227
  function form_wrap( $content, $submit_button = true ) {
228
  if ( is_array( $submit_button ) ) {
229
  $content .= $this->submit_button( $submit_button );
@@ -231,15 +281,24 @@ abstract class scbAdminPage {
231
  $content .= $this->submit_button();
232
  } elseif ( false !== strpos( $submit_button, '<input' ) ) {
233
  $content .= $submit_button;
 
 
234
  } elseif ( false !== $submit_button ) {
235
  $button_args = array_slice( func_get_args(), 1 );
236
- $content .= call_user_func_array( array( $this, 'submit_button' ), $button_args );
237
  }
238
 
239
  return scbForms::form_wrap( $content, $this->nonce );
240
  }
241
 
242
- // Generates a table wrapped in a form
 
 
 
 
 
 
 
243
  function form_table( $rows, $formdata = false ) {
244
  $output = '';
245
  foreach ( $rows as $row )
@@ -250,7 +309,13 @@ abstract class scbAdminPage {
250
  return $output;
251
  }
252
 
253
- // Wraps the given content in a <form><table>
 
 
 
 
 
 
254
  function form_table_wrap( $content ) {
255
  $output = $this->table_wrap( $content );
256
  $output = $this->form_wrap( $output );
@@ -258,7 +323,14 @@ abstract class scbAdminPage {
258
  return $output;
259
  }
260
 
261
- // Generates a form table
 
 
 
 
 
 
 
262
  function table( $rows, $formdata = false ) {
263
  $output = '';
264
  foreach ( $rows as $row )
@@ -269,12 +341,28 @@ abstract class scbAdminPage {
269
  return $output;
270
  }
271
 
272
- // Generates a table row
 
 
 
 
 
 
 
273
  function table_row( $args, $formdata = false ) {
274
  return $this->row_wrap( $args['title'], $this->input( $args, $formdata ) );
275
  }
276
 
277
- // Mimic scbForms inheritance
 
 
 
 
 
 
 
 
 
278
  function __call( $method, $args ) {
279
  if ( in_array( $method, array( 'input', 'form' ) ) ) {
280
  if ( empty( $args[1] ) && isset( $this->options ) )
@@ -287,12 +375,24 @@ abstract class scbAdminPage {
287
  return call_user_func_array( array( 'scbForms', $method ), $args );
288
  }
289
 
290
- // Wraps a string in a <script> tag
 
 
 
 
 
 
291
  function js_wrap( $string ) {
292
  return html( "script type='text/javascript'", $string );
293
  }
294
 
295
- // Wraps a string in a <style> tag
 
 
 
 
 
 
296
  function css_wrap( $string ) {
297
  return html( "style type='text/css'", $string );
298
  }
@@ -301,15 +401,40 @@ abstract class scbAdminPage {
301
  // ____________INTERNAL METHODS____________
302
 
303
 
304
- // Registers a page
 
 
305
  function page_init() {
306
- extract( $this->args );
307
 
308
- if ( ! $toplevel ) {
309
- $this->pagehook = add_submenu_page( $parent, $page_title, $menu_title, $capability, $page_slug, array( $this, '_page_content_hook' ) );
 
 
 
 
 
 
 
310
  } else {
311
- $func = 'add_' . $toplevel . '_page';
312
- $this->pagehook = $func( $page_title, $menu_title, $capability, $page_slug, array( $this, '_page_content_hook' ), $icon_url, $position );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  }
314
 
315
  if ( ! $this->pagehook )
@@ -317,11 +442,6 @@ abstract class scbAdminPage {
317
 
318
  add_action( 'load-' . $this->pagehook, array( $this, 'page_loaded' ) );
319
 
320
- if ( $ajax_submit ) {
321
- $this->ajax_response();
322
- add_action( 'admin_footer', array( $this, 'ajax_submit' ), 20 );
323
- }
324
-
325
  add_action( 'admin_print_styles-' . $this->pagehook, array( $this, 'page_head' ) );
326
  }
327
 
@@ -334,20 +454,22 @@ abstract class scbAdminPage {
334
  trigger_error( 'Page title cannot be empty', E_USER_WARNING );
335
 
336
  $this->args = wp_parse_args( $this->args, array(
337
- 'toplevel' => '',
338
- 'position' => null,
339
- 'icon_url' => '',
340
- 'screen_icon' => '',
341
- 'parent' => 'options-general.php',
342
- 'capability' => 'manage_options',
343
- 'menu_title' => $this->args['page_title'],
344
- 'page_slug' => '',
345
- 'nonce' => '',
346
- 'action_link' => __( 'Settings', $this->textdomain ),
347
- 'ajax_submit' => false,
348
  'admin_action_priority' => 10,
349
  ) );
350
 
 
 
 
351
  if ( empty( $this->args['page_slug'] ) )
352
  $this->args['page_slug'] = sanitize_title_with_dashes( $this->args['menu_title'] );
353
 
@@ -355,6 +477,12 @@ abstract class scbAdminPage {
355
  $this->nonce = $this->args['page_slug'];
356
  }
357
 
 
 
 
 
 
 
358
  function _contextual_help( $help, $screen ) {
359
  if ( is_object( $screen ) )
360
  $screen = $screen->id;
@@ -367,59 +495,6 @@ abstract class scbAdminPage {
367
  return $help;
368
  }
369
 
370
- function ajax_response() {
371
- if ( ! isset( $_POST['_ajax_submit'] ) || $_POST['_ajax_submit'] != $this->pagehook )
372
- return;
373
-
374
- $this->form_handler();
375
- die;
376
- }
377
-
378
- function ajax_submit() {
379
- global $page_hook;
380
-
381
- if ( $page_hook != $this->pagehook )
382
- return;
383
- ?>
384
- <script type="text/javascript">
385
- jQuery( document ).ready( function( $ ){
386
- var $spinner = $( new Image() ).attr( 'src', '<?php echo admin_url( "images/wpspin_light.gif" ); ?>' );
387
-
388
- $( ':submit' ).click( function( ev ){
389
- var $submit = $( this );
390
- var $form = $submit.parents( 'form' );
391
-
392
- if ( $submit.hasClass( 'no-ajax' ) || $form.attr( 'method' ).toLowerCase() != 'post' )
393
- return true;
394
-
395
- var $this_spinner = $spinner.clone();
396
-
397
- $submit.before( $this_spinner ).hide();
398
-
399
- var data = $form.serializeArray();
400
- data.push( {name: $submit.attr( 'name' ), value: $submit.val()} );
401
- data.push( {name: '_ajax_submit', value: '<?php echo $this->pagehook; ?>'} );
402
-
403
- $.post( location.href, data, function( response ){
404
- var $prev = $( '.wrap > .updated, .wrap > .error' );
405
- var $msg = $( response ).hide().insertAfter( $( '.wrap h2' ) );
406
- if ( $prev.length > 0 )
407
- $prev.fadeOut( 'slow', function(){ $msg.fadeIn( 'slow' ); } );
408
- else
409
- $msg.fadeIn( 'slow' );
410
-
411
- $this_spinner.hide();
412
- $submit.show();
413
- } );
414
-
415
- ev.stopPropagation();
416
- ev.preventDefault();
417
- } );
418
- } );
419
- </script>
420
- <?php
421
- }
422
-
423
  function _page_content_hook() {
424
  $this->form_handler();
425
 
@@ -428,6 +503,11 @@ jQuery( document ).ready( function( $ ){
428
  $this->page_footer();
429
  }
430
 
 
 
 
 
 
431
  function _action_link( $links ) {
432
  $url = add_query_arg( 'page', $this->args['page_slug'], admin_url( $this->args['parent'] ) );
433
 
1
  <?php
2
 
3
+ /**
4
+ * Administration page base class
5
+ */
6
  abstract class scbAdminPage {
7
  /** Page args
8
  * $page_title string (mandatory)
9
  * $parent (string) (default: options-general.php)
10
  * $capability (string) (default: 'manage_options')
11
  * $menu_title (string) (default: $page_title)
12
+ * $submenu_title (string) (default: $menu_title)
13
  * $page_slug (string) (default: sanitized $page_title)
14
  * $toplevel (string) If not empty, will create a new top level menu (for expected values see http://codex.wordpress.org/Administration_Menus#Using_add_submenu_page)
15
  * - $icon_url (string) URL to an icon for the top level menu
42
 
43
  private static $registered = array();
44
 
45
+ /**
46
+ * @param string $class
47
+ * @param string $file
48
+ * @param scbOptions $options
49
+ *
50
+ * @return bool
51
+ */
52
  static function register( $class, $file, $options = null ) {
53
  if ( isset( self::$registered[$class] ) )
54
  return false;
60
  return true;
61
  }
62
 
63
+ /**
64
+ * @param string $old_class
65
+ * @param string $new_class
66
+ *
67
+ * @return bool
68
+ */
69
  static function replace( $old_class, $new_class ) {
70
  if ( ! isset( self::$registered[$old_class] ) )
71
  return false;
76
  return true;
77
  }
78
 
79
+ /**
80
+ * @param string $class
81
+ *
82
+ * @return bool
83
+ */
84
  static function remove( $class ) {
85
  if ( ! isset( self::$registered[$class] ) )
86
  return false;
99
  // ____________MAIN METHODS____________
100
 
101
 
102
+ /**
103
+ * Constructor
104
+ *
105
+ * @param string|bool $file
106
+ * @param scbOptions $options
107
+ */
108
  function __construct( $file = false, $options = null ) {
109
  if ( is_a( $options, 'scbOptions' ) )
110
  $this->options = $options;
130
  }
131
  }
132
 
133
+ /**
134
+ * This is where all the page args can be set
135
+ */
136
  function setup(){}
137
 
138
  /**
142
  */
143
  function page_loaded() {}
144
 
145
+ /**
146
+ * This is where the css and js go
147
+ * Both wp_enqueue_*() and inline code can be added
148
+ */
149
  function page_head(){}
150
 
151
+ /**
152
+ * This is where the contextual help goes
153
+ * @return string
154
+ */
155
  function page_help(){}
156
 
157
+ /**
158
+ * A generic page header
159
+ */
160
  function page_header() {
161
  echo "<div class='wrap'>\n";
162
  screen_icon( $this->args['screen_icon'] );
163
+ echo html( 'h2', $this->args['page_title'] );
164
  }
165
 
166
+ /**
167
+ * This is where the page content goes
168
+ */
169
  abstract function page_content();
170
 
171
+ /**
172
+ * A generic page footer
173
+ */
174
  function page_footer() {
175
  echo "</div>\n";
176
  }
177
 
178
+ /**
179
+ * This is where the form data should be validated
180
+ *
181
+ * @param array $new_data
182
+ * @param array $old_data
183
+ *
184
+ * @return array
185
+ */
186
  function validate( $new_data, $old_data ) {
187
  return $new_data;
188
  }
189
 
190
+ /**
191
+ * Manually handle option saving ( use Settings API instead )
192
+ *
193
+ * @return bool
194
+ */
195
  function form_handler() {
196
+ if ( empty( $_POST['submit'] ) && empty( $_POST['action'] ) )
197
  return false;
198
 
199
  check_admin_referer( $this->nonce );
200
 
201
  if ( !isset($this->options) ) {
202
+ trigger_error( 'options handler not set', E_USER_WARNING );
203
  return false;
204
  }
205
 
212
  $this->options->set( $new_data );
213
 
214
  $this->admin_msg();
215
+
216
+ return true;
217
  }
218
 
219
+ /**
220
+ * Manually generate a standard admin notice ( use Settings API instead )
221
+ *
222
+ * @param string $msg
223
+ * @param string $class
224
+ */
225
+ function admin_msg( $msg = '', $class = 'updated' ) {
226
  if ( empty( $msg ) )
227
  $msg = __( 'Settings <strong>saved</strong>.', $this->textdomain );
228
 
233
  // ____________UTILITIES____________
234
 
235
 
236
+ /**
237
+ * Generates a form submit button
238
+ *
239
+ * @param string|array $value button text or array of arguments
240
+ * @param string $action
241
+ * @param string $class
242
+ *
243
+ * @return string
244
+ */
245
+ function submit_button( $value = '', $action = 'submit', $class = 'button' ) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
+ $args = is_array( $value ) ? $value : compact( 'value', 'action', 'class' );
248
+ $args = wp_parse_args( $args, array(
249
+ 'value' => null,
250
+ 'action' => $action,
251
+ 'class' => $class,
252
+ ) );
253
 
254
+ return get_submit_button( $args['value'], $args['class'], $args['action'] );
255
  }
256
 
257
+ /**
258
+ * Mimics scbForms::form_wrap()
259
+ *
260
+ * $this->form_wrap( $content ); // generates a form with a default submit button
261
+ *
262
+ * $this->form_wrap( $content, false ); // generates a form with no submit button
263
+ *
264
+ * // the second argument is sent to submit_button()
265
+ * $this->form_wrap( $content, array(
266
+ * 'text' => 'Save changes',
267
+ * 'name' => 'action',
268
+ * ) );
269
+ *
270
+ * @see scbForms::form_wrap()
271
+ *
272
+ * @param string $content
273
+ * @param boolean|string|array $submit_button
274
+ *
275
+ * @return string
276
+ */
277
  function form_wrap( $content, $submit_button = true ) {
278
  if ( is_array( $submit_button ) ) {
279
  $content .= $this->submit_button( $submit_button );
281
  $content .= $this->submit_button();
282
  } elseif ( false !== strpos( $submit_button, '<input' ) ) {
283
  $content .= $submit_button;
284
+ } elseif ( false !== strpos( $submit_button, '<button' ) ) {
285
+ $content .= $submit_button;
286
  } elseif ( false !== $submit_button ) {
287
  $button_args = array_slice( func_get_args(), 1 );
288
+ $content .= call_user_func_array( array( $this, 'submit_button' ), $button_args );
289
  }
290
 
291
  return scbForms::form_wrap( $content, $this->nonce );
292
  }
293
 
294
+ /**
295
+ * Generates a table wrapped in a form
296
+ *
297
+ * @param array $rows
298
+ * @param array|boolean $formdata
299
+ *
300
+ * @return string
301
+ */
302
  function form_table( $rows, $formdata = false ) {
303
  $output = '';
304
  foreach ( $rows as $row )
309
  return $output;
310
  }
311
 
312
+ /**
313
+ * Wraps the given content in a <form><table>
314
+ *
315
+ * @param string $content
316
+ *
317
+ * @return string
318
+ */
319
  function form_table_wrap( $content ) {
320
  $output = $this->table_wrap( $content );
321
  $output = $this->form_wrap( $output );
323
  return $output;
324
  }
325
 
326
+ /**
327
+ * Generates a form table
328
+ *
329
+ * @param array $rows
330
+ * @param array|boolean $formdata
331
+ *
332
+ * @return string
333
+ */
334
  function table( $rows, $formdata = false ) {
335
  $output = '';
336
  foreach ( $rows as $row )
341
  return $output;
342
  }
343
 
344
+ /**
345
+ * Generates a table row
346
+ *
347
+ * @param array $args
348
+ * @param array|boolean $formdata
349
+ *
350
+ * @return string
351
+ */
352
  function table_row( $args, $formdata = false ) {
353
  return $this->row_wrap( $args['title'], $this->input( $args, $formdata ) );
354
  }
355
 
356
+ /**
357
+ * Mimic scbForms inheritance
358
+ *
359
+ * @see scbForms
360
+ *
361
+ * @param string $method
362
+ * @param array $args
363
+ *
364
+ * @return mixed
365
+ */
366
  function __call( $method, $args ) {
367
  if ( in_array( $method, array( 'input', 'form' ) ) ) {
368
  if ( empty( $args[1] ) && isset( $this->options ) )
375
  return call_user_func_array( array( 'scbForms', $method ), $args );
376
  }
377
 
378
+ /**
379
+ * Wraps a string in a <script> tag
380
+ *
381
+ * @param string $string
382
+ *
383
+ * @return string
384
+ */
385
  function js_wrap( $string ) {
386
  return html( "script type='text/javascript'", $string );
387
  }
388
 
389
+ /**
390
+ * Wraps a string in a <style> tag
391
+ *
392
+ * @param string $string
393
+ *
394
+ * @return string
395
+ */
396
  function css_wrap( $string ) {
397
  return html( "style type='text/css'", $string );
398
  }
401
  // ____________INTERNAL METHODS____________
402
 
403
 
404
+ /**
405
+ * Registers a page
406
+ */
407
  function page_init() {
 
408
 
409
+ if ( ! $this->args['toplevel'] ) {
410
+ $this->pagehook = add_submenu_page(
411
+ $this->args['parent'],
412
+ $this->args['page_title'],
413
+ $this->args['menu_title'],
414
+ $this->args['capability'],
415
+ $this->args['page_slug'],
416
+ array( $this, '_page_content_hook' )
417
+ );
418
  } else {
419
+ $func = 'add_' . $this->args['toplevel'] . '_page';
420
+ $this->pagehook = $func(
421
+ $this->args['page_title'],
422
+ $this->args['menu_title'],
423
+ $this->args['capability'],
424
+ $this->args['page_slug'],
425
+ null,
426
+ $this->args['icon_url'],
427
+ $this->args['position']
428
+ );
429
+
430
+ add_submenu_page(
431
+ $this->args['page_slug'],
432
+ $this->args['page_title'],
433
+ $this->args['submenu_title'],
434
+ $this->args['capability'],
435
+ $this->args['page_slug'],
436
+ array( $this, '_page_content_hook' )
437
+ );
438
  }
439
 
440
  if ( ! $this->pagehook )
442
 
443
  add_action( 'load-' . $this->pagehook, array( $this, 'page_loaded' ) );
444
 
 
 
 
 
 
445
  add_action( 'admin_print_styles-' . $this->pagehook, array( $this, 'page_head' ) );
446
  }
447
 
454
  trigger_error( 'Page title cannot be empty', E_USER_WARNING );
455
 
456
  $this->args = wp_parse_args( $this->args, array(
457
+ 'toplevel' => '',
458
+ 'position' => null,
459
+ 'icon_url' => '',
460
+ 'screen_icon' => '',
461
+ 'parent' => 'options-general.php',
462
+ 'capability' => 'manage_options',
463
+ 'menu_title' => $this->args['page_title'],
464
+ 'page_slug' => '',
465
+ 'nonce' => '',
466
+ 'action_link' => __( 'Settings', $this->textdomain ),
 
467
  'admin_action_priority' => 10,
468
  ) );
469
 
470
+ if ( empty( $this->args['submenu_title'] ) )
471
+ $this->args['submenu_title'] = $this->args['menu_title'];
472
+
473
  if ( empty( $this->args['page_slug'] ) )
474
  $this->args['page_slug'] = sanitize_title_with_dashes( $this->args['menu_title'] );
475
 
477
  $this->nonce = $this->args['page_slug'];
478
  }
479
 
480
+ /**
481
+ * @param string $help
482
+ * @param string|object $screen
483
+ *
484
+ * @return string
485
+ */
486
  function _contextual_help( $help, $screen ) {
487
  if ( is_object( $screen ) )
488
  $screen = $screen->id;
495
  return $help;
496
  }
497
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
  function _page_content_hook() {
499
  $this->form_handler();
500
 
503
  $this->page_footer();
504
  }
505
 
506
+ /**
507
+ * @param array $links
508
+ *
509
+ * @return array
510
+ */
511
  function _action_link( $links ) {
512
  $url = add_query_arg( 'page', $this->args['page_slug'], admin_url( $this->args['parent'] ) );
513
 
scb/BoxesPage.php CHANGED
@@ -166,35 +166,55 @@ abstract class scbBoxesPage extends scbAdminPage {
166
  ) );
167
 
168
  $registered = array();
 
169
  foreach ( $this->boxes as $box_args ) {
170
- foreach ( array( 'name', 'title', 'context', 'priority', 'args' ) as $i => $arg ) {
171
- if ( isset( $box_args[$i] ) )
172
- $$arg = $box_args[$i];
173
- }
 
 
 
 
 
174
 
175
- if ( empty( $title ) )
176
- $title = ucfirst( $name );
177
- if ( empty( $context ) )
178
- $context = 'normal';
179
- if ( empty( $priority ) )
180
- $priority = 'default';
181
- if ( empty( $args ) )
182
- $args = array();
183
-
184
- if ( isset( $registered[$name] ) ) {
185
- if ( empty( $args ) )
186
  trigger_error( "Duplicate box name: $name", E_USER_NOTICE );
 
187
 
188
  $name = $this->_increment( $name );
189
  } else {
190
- $registered[$name] = true;
191
  }
192
 
193
- add_meta_box( $name, $title, array( $this, '_intermediate_callback' ), $this->pagehook, $context, $priority, $args );
 
 
 
 
 
 
 
 
194
  }
195
  }
196
 
197
- // Make it so that $args is actually what's passed to the callback
 
 
 
 
 
 
 
 
 
 
 
 
198
  function _intermediate_callback( $_, $box ) {
199
  list( $name ) = explode( '-', $box['id'] );
200
 
166
  ) );
167
 
168
  $registered = array();
169
+
170
  foreach ( $this->boxes as $box_args ) {
171
+ $box_args = self::numeric_to_assoc( $box_args, array( 'name', 'title', 'context', 'priority', 'args' ) );
172
+
173
+ $defaults = array(
174
+ 'title' => ucfirst( $box_args['name'] ),
175
+ 'context' => 'normal',
176
+ 'priority' => 'default',
177
+ 'args' => array()
178
+ );
179
+ $box_args = array_merge( $defaults, $box_args );
180
 
181
+ $name = $box_args['name'];
182
+
183
+ if ( isset( $registered[ $name ] ) ) {
184
+ if ( empty( $box_args['args'] ) ) {
 
 
 
 
 
 
 
185
  trigger_error( "Duplicate box name: $name", E_USER_NOTICE );
186
+ }
187
 
188
  $name = $this->_increment( $name );
189
  } else {
190
+ $registered[ $name ] = true;
191
  }
192
 
193
+ add_meta_box(
194
+ $name,
195
+ $box_args['title'],
196
+ array( $this, '_intermediate_callback' ),
197
+ $this->pagehook,
198
+ $box_args['context'],
199
+ $box_args['priority'],
200
+ $box_args['args']
201
+ );
202
  }
203
  }
204
 
205
+ private static function numeric_to_assoc( $argv, $keys ) {
206
+ $args = array();
207
+
208
+ foreach ( $keys as $i => $key ) {
209
+ if ( isset( $argv[ $i ] ) )
210
+ $args[ $key ] = $argv[ $i ];
211
+ }
212
+
213
+ return $args;
214
+ }
215
+
216
+ // Since we don't pass an object to do_meta_boxes(),
217
+ // pass $box['args'] directly to each method.
218
  function _intermediate_callback( $_, $box ) {
219
  list( $name ) = explode( '-', $box['id'] );
220
 
scb/Cron.php CHANGED
@@ -1,7 +1,8 @@
1
  <?php
2
 
3
- // wp-cron job container
4
-
 
5
  class scbCron {
6
  protected $schedule;
7
  protected $interval;
@@ -13,41 +14,40 @@ class scbCron {
13
  /**
14
  * Create a new cron job
15
  *
16
- * @param string Reference to main plugin file
17
- * @param array List of args:
18
- string $action OR callback $callback
19
- string $schedule OR number $interval
20
- array $callback_args (optional)
21
  */
22
  function __construct( $file = false, $args ) {
23
- extract( $args, EXTR_SKIP );
24
 
25
  // Set time & schedule
26
- if ( isset( $time ) )
27
- $this->time = $time;
28
-
29
- if ( isset( $interval ) ) {
30
- $this->schedule = $interval . 'secs';
31
- $this->interval = $interval;
32
- } elseif ( isset( $schedule ) ) {
33
- $this->schedule = $schedule;
34
  }
35
 
36
  // Set hook
37
- if ( isset( $action ) ) {
38
- $this->hook = $action;
39
- } elseif ( isset( $callback ) ) {
40
- $this->hook = self::_callback_to_string( $callback );
41
- add_action( $this->hook, $callback );
42
  } elseif ( method_exists( $this, 'callback' ) ) {
43
  $this->hook = self::_callback_to_string( array( $this, 'callback' ) );
44
- add_action( $this->hook, $callback );
45
  } else {
46
  trigger_error( '$action OR $callback not set', E_USER_WARNING );
47
  }
48
 
49
- if ( isset( $callback_args ) )
50
- $this->callback_args = (array) $callback_args;
51
 
52
  if ( $file && $this->schedule ) {
53
  scbUtil::add_activation_hook( $file, array( $this, 'reset' ) );
@@ -57,23 +57,23 @@ class scbCron {
57
  add_filter( 'cron_schedules', array( $this, '_add_timing' ) );
58
  }
59
 
60
- /* Change the interval of the cron job
 
61
  *
62
- * @param array List of args:
63
- string $schedule OR number $interval
64
- timestamp $time ( optional )
65
  */
66
  function reschedule( $args ) {
67
- extract( $args );
68
 
69
- if ( $schedule && $this->schedule != $schedule ) {
70
- $this->schedule = $schedule;
71
- } elseif ( $interval && $this->interval != $interval ) {
72
- $this->schedule = $interval . 'secs';
73
- $this->interval = $interval;
74
  }
75
 
76
- $this->time = $time;
77
 
78
  $this->reset();
79
  }
@@ -107,8 +107,8 @@ class scbCron {
107
 
108
  /**
109
  * Execute the job with a given delay
110
- * @param int $delay in seconds
111
- * @param array $args List of arguments to pass to the callback
112
  */
113
  function do_once( $delay = 0, $args = null ) {
114
  if ( is_null( $args ) )
@@ -121,13 +121,19 @@ class scbCron {
121
 
122
  //_____INTERNAL METHODS_____
123
 
124
-
 
 
 
 
125
  function _add_timing( $schedules ) {
126
  if ( isset( $schedules[$this->schedule] ) )
127
  return $schedules;
128
 
129
- $schedules[$this->schedule] = array( 'interval' => $this->interval,
130
- 'display' => $this->interval . ' seconds' );
 
 
131
 
132
  return $schedules;
133
  }
@@ -139,6 +145,9 @@ class scbCron {
139
  wp_schedule_event( $this->time, $this->schedule, $this->hook, $this->callback_args );
140
  }
141
 
 
 
 
142
  protected static function really_clear_scheduled_hook( $name ) {
143
  $crons = _get_cron_array();
144
 
@@ -147,13 +156,18 @@ class scbCron {
147
  if ( $hook == $name )
148
  unset( $crons[$timestamp][$hook] );
149
 
150
- if ( empty( $hooks ) )
151
  unset( $crons[$timestamp] );
152
  }
153
 
154
  _set_cron_array( $crons );
155
  }
156
 
 
 
 
 
 
157
  protected static function _callback_to_string( $callback ) {
158
  if ( ! is_array( $callback ) )
159
  $str = $callback;
1
  <?php
2
 
3
+ /**
4
+ * wp-cron job container
5
+ */
6
  class scbCron {
7
  protected $schedule;
8
  protected $interval;
14
  /**
15
  * Create a new cron job
16
  *
17
+ * @param string|bool $file Reference to main plugin file
18
+ * @param array $args List of args:
19
+ * string $action OR callback $callback
20
+ * string $schedule OR number $interval
21
+ * array $callback_args (optional)
22
  */
23
  function __construct( $file = false, $args ) {
 
24
 
25
  // Set time & schedule
26
+ if ( isset( $args['time'] ) )
27
+ $this->time = $args['time'];
28
+
29
+ if ( isset( $args['interval'] ) ) {
30
+ $this->schedule = $args['interval'] . 'secs';
31
+ $this->interval = $args['interval'];
32
+ } elseif ( isset( $args['schedule'] ) ) {
33
+ $this->schedule = $args['schedule'];
34
  }
35
 
36
  // Set hook
37
+ if ( isset( $args['action'] ) ) {
38
+ $this->hook = $args['action'];
39
+ } elseif ( isset( $args['callback'] ) ) {
40
+ $this->hook = self::_callback_to_string( $args['callback'] );
41
+ add_action( $this->hook, $args['callback'] );
42
  } elseif ( method_exists( $this, 'callback' ) ) {
43
  $this->hook = self::_callback_to_string( array( $this, 'callback' ) );
44
+ add_action( $this->hook, $args['callback'] );
45
  } else {
46
  trigger_error( '$action OR $callback not set', E_USER_WARNING );
47
  }
48
 
49
+ if ( isset( $args['callback_args'] ) )
50
+ $this->callback_args = (array) $args['callback_args'];
51
 
52
  if ( $file && $this->schedule ) {
53
  scbUtil::add_activation_hook( $file, array( $this, 'reset' ) );
57
  add_filter( 'cron_schedules', array( $this, '_add_timing' ) );
58
  }
59
 
60
+ /**
61
+ * Change the interval of the cron job
62
  *
63
+ * @param array $args List of args:
64
+ * string $schedule OR number $interval
65
+ * timestamp $time ( optional )
66
  */
67
  function reschedule( $args ) {
 
68
 
69
+ if ( $args['schedule'] && $this->schedule != $args['schedule'] ) {
70
+ $this->schedule = $args['schedule'];
71
+ } elseif ( $args['interval'] && $this->interval != $args['interval'] ) {
72
+ $this->schedule = $args['interval'] . 'secs';
73
+ $this->interval = $args['interval'];
74
  }
75
 
76
+ $this->time = $args['time'];
77
 
78
  $this->reset();
79
  }
107
 
108
  /**
109
  * Execute the job with a given delay
110
+ * @param int $delay in seconds
111
+ * @param array $args List of arguments to pass to the callback
112
  */
113
  function do_once( $delay = 0, $args = null ) {
114
  if ( is_null( $args ) )
121
 
122
  //_____INTERNAL METHODS_____
123
 
124
+ /**
125
+ * @param array $schedules
126
+ *
127
+ * @return array
128
+ */
129
  function _add_timing( $schedules ) {
130
  if ( isset( $schedules[$this->schedule] ) )
131
  return $schedules;
132
 
133
+ $schedules[$this->schedule] = array(
134
+ 'interval' => $this->interval,
135
+ 'display' => $this->interval . ' seconds',
136
+ );
137
 
138
  return $schedules;
139
  }
145
  wp_schedule_event( $this->time, $this->schedule, $this->hook, $this->callback_args );
146
  }
147
 
148
+ /**
149
+ * @param string $name
150
+ */
151
  protected static function really_clear_scheduled_hook( $name ) {
152
  $crons = _get_cron_array();
153
 
156
  if ( $hook == $name )
157
  unset( $crons[$timestamp][$hook] );
158
 
159
+ if ( empty( $crons[$timestamp] ) )
160
  unset( $crons[$timestamp] );
161
  }
162
 
163
  _set_cron_array( $crons );
164
  }
165
 
166
+ /**
167
+ * @param callback $callback
168
+ *
169
+ * @return string
170
+ */
171
  protected static function _callback_to_string( $callback ) {
172
  if ( ! is_array( $callback ) )
173
  $str = $callback;
scb/Forms.php CHANGED
@@ -1,24 +1,44 @@
1
  <?php
2
 
3
- // Data-aware form generator
4
-
 
5
  class scbForms {
6
 
7
  const TOKEN = '%input%';
8
 
 
 
 
 
 
 
9
  static function input_with_value( $args, $value ) {
10
  $field = scbFormField::create( $args );
11
 
12
  return $field->render( $value );
13
  }
14
 
 
 
 
 
 
 
15
  static function input( $args, $formdata = null ) {
16
  $field = scbFormField::create( $args );
17
 
18
  return $field->render( scbForms::get_value( $args['name'], $formdata ) );
19
  }
20
 
21
- // Generates a table wrapped in a form
 
 
 
 
 
 
 
22
  static function form_table( $rows, $formdata = null ) {
23
  $output = '';
24
  foreach ( $rows as $row )
@@ -29,7 +49,15 @@ class scbForms {
29
  return $output;
30
  }
31
 
32
- // Generates a form
 
 
 
 
 
 
 
 
33
  static function form( $inputs, $formdata = null, $nonce ) {
34
  $output = '';
35
  foreach ( $inputs as $input )
@@ -40,7 +68,14 @@ class scbForms {
40
  return $output;
41
  }
42
 
43
- // Generates a table
 
 
 
 
 
 
 
44
  static function table( $rows, $formdata = null ) {
45
  $output = '';
46
  foreach ( $rows as $row )
@@ -51,7 +86,14 @@ class scbForms {
51
  return $output;
52
  }
53
 
54
- // Generates a table row
 
 
 
 
 
 
 
55
  static function table_row( $args, $formdata = null ) {
56
  return self::row_wrap( $args['title'], self::input( $args, $formdata ) );
57
  }
@@ -59,11 +101,22 @@ class scbForms {
59
 
60
  // ____________WRAPPERS____________
61
 
62
-
 
 
 
 
 
63
  static function form_table_wrap( $content, $nonce = 'update_options' ) {
64
  return self::form_wrap( self::table_wrap( $content ), $nonce );
65
  }
66
 
 
 
 
 
 
 
67
  static function form_wrap( $content, $nonce = 'update_options' ) {
68
  return html( "form method='post' action=''",
69
  $content,
@@ -71,14 +124,25 @@ class scbForms {
71
  );
72
  }
73
 
 
 
 
 
 
74
  static function table_wrap( $content ) {
75
  return html( "table class='form-table'", $content );
76
  }
77
 
 
 
 
 
 
 
78
  static function row_wrap( $title, $content ) {
79
- return html( "tr",
80
  html( "th scope='row'", $title ),
81
- html( "td", $content )
82
  );
83
  }
84
 
@@ -111,9 +175,9 @@ class scbForms {
111
  /**
112
  * Traverses the formdata and retrieves the correct value.
113
  *
114
- * @param array|string $name The name of the value
115
- * @param array $value The data that will be traversed
116
- * @param mixed $fallback The value returned when the key is not found
117
  *
118
  * @return mixed
119
  */
@@ -131,8 +195,8 @@ class scbForms {
131
  /**
132
  * Given a list of fields, validate some data.
133
  *
134
- * @param array $fields List of args that would be sent to scbForms::input()
135
- * @param array $data The data to validate. Defaults to $_POST
136
  * @param array $to_update Existing data to populate. Necessary for nested values
137
  *
138
  * @return array
@@ -161,8 +225,8 @@ class scbForms {
161
  * For single-choice fields, we can't distinguish either, because of how self::update_meta() works.
162
  * Therefore, the 'default' parameter is always ignored.
163
  *
164
- * @param array $args Field arguments.
165
- * @param int $object_id The object ID the metadata is attached to
166
  * @param string $meta_type
167
  *
168
  * @return string
@@ -178,6 +242,12 @@ class scbForms {
178
  return self::input_with_value( $args, $value );
179
  }
180
 
 
 
 
 
 
 
181
  static function update_meta( $fields, $data, $object_id, $meta_type = 'post' ) {
182
  foreach ( $fields as $field_args ) {
183
  $key = $field_args['name'];
@@ -193,7 +263,7 @@ class scbForms {
193
  foreach ( array_diff( $old_values, $new_values ) as $value )
194
  delete_metadata( $meta_type, $object_id, $key, $value );
195
  } else {
196
- $value = $data[$key];
197
 
198
  if ( '' === $value )
199
  delete_metadata( $meta_type, $object_id, $key );
@@ -203,6 +273,11 @@ class scbForms {
203
  }
204
  }
205
 
 
 
 
 
 
206
  private static function set_value( &$arr, $name, $value ) {
207
  $name = (array) $name;
208
 
@@ -226,9 +301,13 @@ class scbForms {
226
  * A wrapper for scbForms, containing the formdata
227
  */
228
  class scbForm {
229
- protected $data = array();
230
  protected $prefix = array();
231
 
 
 
 
 
232
  function __construct( $data, $prefix = false ) {
233
  if ( is_array( $data ) )
234
  $this->data = $data;
@@ -237,6 +316,11 @@ class scbForm {
237
  $this->prefix = (array) $prefix;
238
  }
239
 
 
 
 
 
 
240
  function traverse_to( $path ) {
241
  $data = scbForms::get_value( $path, $this->data );
242
 
@@ -245,6 +329,11 @@ class scbForm {
245
  return new scbForm( $data, $prefix );
246
  }
247
 
 
 
 
 
 
248
  function input( $args ) {
249
  $value = scbForms::get_value( $args['name'], $this->data );
250
 
@@ -256,7 +345,9 @@ class scbForm {
256
  }
257
  }
258
 
259
-
 
 
260
  interface scbFormField_I {
261
 
262
  /**
@@ -278,11 +369,18 @@ interface scbFormField_I {
278
  function validate( $value );
279
  }
280
 
281
-
 
 
282
  abstract class scbFormField implements scbFormField_I {
283
 
284
  protected $args;
285
 
 
 
 
 
 
286
  public static function create( $args ) {
287
  if ( is_a( $args, 'scbFormField_I' ) )
288
  return $args;
@@ -305,9 +403,9 @@ abstract class scbFormField implements scbFormField_I {
305
  $args['extra'] = shortcode_parse_atts( $args['extra'] );
306
 
307
  $args = wp_parse_args( $args, array(
308
- 'desc' => '',
309
- 'desc_pos' => 'after',
310
- 'wrap' => scbForms::TOKEN,
311
  'wrap_each' => scbForms::TOKEN,
312
  ) );
313
 
@@ -332,18 +430,36 @@ abstract class scbFormField implements scbFormField_I {
332
  }
333
  }
334
 
 
 
 
335
  protected function __construct( $args ) {
336
  $this->args = $args;
337
  }
338
 
 
 
 
 
 
339
  public function __get( $key ) {
340
  return $this->args[ $key ];
341
  }
342
 
 
 
 
 
 
343
  public function __isset( $key ) {
344
  return isset( $this->args[ $key ] );
345
  }
346
 
 
 
 
 
 
347
  public function render( $value = null ) {
348
  if ( null === $value && isset( $this->default ) )
349
  $value = $this->default;
@@ -358,58 +474,89 @@ abstract class scbFormField implements scbFormField_I {
358
  return str_replace( scbForms::TOKEN, $this->_render( $args ), $this->wrap );
359
  }
360
 
361
- // Mutate the field arguments so that the value passed is rendered.
 
 
 
 
 
362
  abstract protected function _set_value( &$args, $value );
363
 
364
- // The actual rendering
 
 
 
 
365
  abstract protected function _render( $args );
366
 
367
- // Handle args for a single checkbox or radio input
 
 
 
 
 
 
368
  protected static function _checkbox( $args ) {
369
  $args = wp_parse_args( $args, array(
370
- 'value' => true,
371
- 'desc' => null,
372
  'checked' => false,
373
- 'extra' => array(),
374
  ) );
375
 
376
- foreach ( $args as $key => &$val )
377
- $$key = &$val;
378
- unset( $val );
379
 
380
- $extra['checked'] = $checked;
381
-
382
- if ( is_null( $desc ) && !is_bool( $value ) )
383
- $desc = str_replace( '[]', '', $value );
384
 
385
  return self::_input_gen( $args );
386
  }
387
 
388
- // Generate html with the final args
 
 
 
 
 
 
389
  protected static function _input_gen( $args ) {
390
- extract( wp_parse_args( $args, array(
391
  'value' => null,
392
- 'desc' => null,
393
- 'extra' => array()
394
- ) ) );
395
 
396
- $extra['name'] = $name;
397
 
398
- if ( 'textarea' == $type ) {
399
- $input = html( 'textarea', $extra, esc_textarea( $value ) );
400
  } else {
401
- $extra['value'] = $value;
402
- $extra['type'] = $type;
403
- $input = html( 'input', $extra );
404
  }
405
 
406
- return self::add_label( $input, $desc, $desc_pos );
407
  }
408
 
 
 
 
 
 
 
 
409
  protected static function add_label( $input, $desc, $desc_pos ) {
410
  return html( 'label', self::add_desc( $input, $desc, $desc_pos ) ) . "\n";
411
  }
412
 
 
 
 
 
 
 
 
413
  protected static function add_desc( $input, $desc, $desc_pos ) {
414
  if ( empty( $desc ) )
415
  return $input;
@@ -420,6 +567,9 @@ abstract class scbFormField implements scbFormField_I {
420
  return $input . ' ' . $desc;
421
  }
422
 
 
 
 
423
  private static function _expand_choices( &$args ) {
424
  $choices =& $args['choices'];
425
 
@@ -433,46 +583,70 @@ abstract class scbFormField implements scbFormField_I {
433
  }
434
  }
435
 
 
 
 
 
 
436
  private static function is_associative( $array ) {
437
  $keys = array_keys( $array );
438
  return array_keys( $keys ) !== $keys;
439
  }
440
  }
441
 
442
-
 
 
443
  class scbTextField extends scbFormField {
444
 
 
 
 
 
 
445
  public function validate( $value ) {
446
  $sanitize = isset( $this->sanitize ) ? $this->sanitize : 'wp_filter_kses';
447
 
448
  return call_user_func( $sanitize, $value, $this );
449
  }
450
 
 
 
 
 
 
451
  protected function _render( $args ) {
452
  $args = wp_parse_args( $args, array(
453
- 'value' => '',
454
  'desc_pos' => 'after',
455
- 'extra' => array( 'class' => 'regular-text' ),
456
  ) );
457
 
458
- foreach ( $args as $key => &$val )
459
- $$key = &$val;
460
- unset( $val );
461
-
462
- if ( !isset( $extra['id'] ) && !is_array( $name ) && false === strpos( $name, '[' ) )
463
- $extra['id'] = $name;
464
 
465
  return scbFormField::_input_gen( $args );
466
  }
467
 
 
 
 
 
468
  protected function _set_value( &$args, $value ) {
469
  $args['value'] = $value;
470
  }
471
  }
472
 
473
-
 
 
474
  abstract class scbSingleChoiceField extends scbFormField {
475
 
 
 
 
 
 
476
  public function validate( $value ) {
477
  if ( isset( $this->choices[ $value ] ) )
478
  return $value;
@@ -480,189 +654,265 @@ abstract class scbSingleChoiceField extends scbFormField {
480
  return null;
481
  }
482
 
 
 
 
 
 
483
  protected function _render( $args ) {
484
  $args = wp_parse_args( $args, array(
485
- 'numeric' => false, // use numeric array instead of associative
486
- 'selected' => array( 'foo' ), // hack to make default blank
487
  ) );
488
 
489
  return $this->_render_specific( $args );
490
  }
491
 
 
 
 
 
492
  protected function _set_value( &$args, $value ) {
493
  $args['selected'] = $value;
494
  }
495
 
 
 
 
 
 
496
  abstract protected function _render_specific( $args );
497
  }
498
 
499
-
 
 
500
  class scbSelectField extends scbSingleChoiceField {
501
 
 
 
 
 
 
502
  protected function _render_specific( $args ) {
503
- extract( wp_parse_args( $args, array(
504
- 'text' => false,
505
- 'extra' => array()
506
- ) ) );
507
 
508
  $options = array();
509
 
510
- if ( false !== $text ) {
511
  $options[] = array(
512
- 'value' => '',
513
- 'selected' => ( $selected == array( 'foo' ) ),
514
- 'title' => $text
515
  );
516
  }
517
 
518
- foreach ( $choices as $value => $title ) {
519
  $options[] = array(
520
- 'value' => $value,
521
- 'selected' => ( $value == $selected ),
522
- 'title' => $title
523
  );
524
  }
525
 
526
  $opts = '';
527
  foreach ( $options as $option ) {
528
- extract( $option );
529
-
530
- $opts .= html( 'option', compact( 'value', 'selected' ), $title );
531
  }
532
 
533
- $extra['name'] = $name;
534
 
535
- $input = html( 'select', $extra, $opts );
536
 
537
- return scbFormField::add_label( $input, $desc, $desc_pos );
538
  }
539
  }
540
 
541
-
 
 
542
  class scbRadiosField extends scbSelectField {
543
 
 
 
 
 
 
544
  protected function _render_specific( $args ) {
545
- extract( $args );
546
 
547
- if ( array( 'foo' ) == $selected ) {
548
  // radio buttons should always have one option selected
549
- $selected = key( $choices );
550
  }
551
 
552
  $opts = '';
553
- foreach ( $choices as $value => $title ) {
554
  $single_input = scbFormField::_checkbox( array(
555
- 'name' => $name,
556
- 'type' => 'radio',
557
- 'value' => $value,
558
- 'checked' => ( $value == $selected ),
559
- 'desc' => $title,
560
- 'desc_pos' => 'after'
561
  ) );
562
 
563
- $opts .= str_replace( scbForms::TOKEN, $single_input, $wrap_each );
564
  }
565
 
566
- return scbFormField::add_desc( $opts, $desc, $desc_pos );
567
  }
568
  }
569
 
570
-
 
 
571
  class scbMultipleChoiceField extends scbFormField {
572
 
 
 
 
 
 
573
  public function validate( $value ) {
574
  return array_intersect( array_keys( $this->choices ), (array) $value );
575
  }
576
 
 
 
 
 
 
577
  protected function _render( $args ) {
578
  $args = wp_parse_args( $args, array(
579
- 'numeric' => false, // use numeric array instead of associative
580
  'checked' => null,
581
  ) );
582
 
583
- extract( $args );
584
-
585
- if ( !is_array( $checked ) )
586
- $checked = array();
587
 
588
  $opts = '';
589
- foreach ( $choices as $value => $title ) {
590
  $single_input = scbFormField::_checkbox( array(
591
- 'name' => $name . '[]',
592
- 'type' => 'checkbox',
593
- 'value' => $value,
594
- 'checked' => in_array( $value, $checked ),
595
- 'desc' => $title,
596
- 'desc_pos' => 'after'
597
  ) );
598
 
599
- $opts .= str_replace( scbForms::TOKEN, $single_input, $wrap_each );
600
  }
601
 
602
- return scbFormField::add_desc( $opts, $desc, $desc_pos );
603
  }
604
 
 
 
 
 
605
  protected function _set_value( &$args, $value ) {
606
  $args['checked'] = (array) $value;
607
  }
608
  }
609
 
610
-
 
 
611
  class scbSingleCheckboxField extends scbFormField {
612
 
 
 
 
 
 
613
  public function validate( $value ) {
614
  return (bool) $value;
615
  }
616
 
 
 
 
 
 
617
  protected function _render( $args ) {
618
  $args = wp_parse_args( $args, array(
619
- 'value' => true,
620
- 'desc' => null,
621
  'checked' => false,
622
- 'extra' => array(),
623
  ) );
624
 
625
- foreach ( $args as $key => &$val )
626
- $$key = &$val;
627
- unset( $val );
628
-
629
- $extra['checked'] = $checked;
630
 
631
- if ( is_null( $desc ) && !is_bool( $value ) )
632
- $desc = str_replace( '[]', '', $value );
633
 
634
  return scbFormField::_input_gen( $args );
635
  }
636
 
 
 
 
 
637
  protected function _set_value( &$args, $value ) {
638
  $args['checked'] = ( $value || ( isset( $args['value'] ) && $value == $args['value'] ) );
639
  }
640
  }
641
 
642
-
 
 
643
  class scbCustomField implements scbFormField_I {
644
 
645
  protected $args;
646
 
 
 
 
647
  function __construct( $args ) {
648
  $this->args = wp_parse_args( $args, array(
649
- 'render' => 'var_dump',
650
  'sanitize' => 'wp_filter_kses',
651
  ) );
652
  }
653
 
 
 
 
 
 
654
  public function __get( $key ) {
655
  return $this->args[ $key ];
656
  }
657
 
 
 
 
 
 
658
  public function __isset( $key ) {
659
  return isset( $this->args[ $key ] );
660
  }
661
 
 
 
 
 
 
662
  public function render( $value = null ) {
663
  return call_user_func( $this->render, $value, $this );
664
  }
665
 
 
 
 
 
 
666
  public function validate( $value ) {
667
  return call_user_func( $this->sanitize, $value, $this );
668
  }
1
  <?php
2
 
3
+ /**
4
+ * Data-aware form generator
5
+ */
6
  class scbForms {
7
 
8
  const TOKEN = '%input%';
9
 
10
+ /**
11
+ * @param array|scbFormField_I $args
12
+ * @param mixed $value
13
+ *
14
+ * @return string
15
+ */
16
  static function input_with_value( $args, $value ) {
17
  $field = scbFormField::create( $args );
18
 
19
  return $field->render( $value );
20
  }
21
 
22
+ /**
23
+ * @param array|scbFormField_I $args
24
+ * @param array $formdata
25
+ *
26
+ * @return string
27
+ */
28
  static function input( $args, $formdata = null ) {
29
  $field = scbFormField::create( $args );
30
 
31
  return $field->render( scbForms::get_value( $args['name'], $formdata ) );
32
  }
33
 
34
+ /**
35
+ * Generates a table wrapped in a form
36
+ *
37
+ * @param array $rows
38
+ * @param array $formdata
39
+ *
40
+ * @return string
41
+ */
42
  static function form_table( $rows, $formdata = null ) {
43
  $output = '';
44
  foreach ( $rows as $row )
49
  return $output;
50
  }
51
 
52
+ /**
53
+ * Generates a form
54
+ *
55
+ * @param array $inputs
56
+ * @param array $formdata
57
+ * @param string $nonce
58
+ *
59
+ * @return string
60
+ */
61
  static function form( $inputs, $formdata = null, $nonce ) {
62
  $output = '';
63
  foreach ( $inputs as $input )
68
  return $output;
69
  }
70
 
71
+ /**
72
+ * Generates a table
73
+ *
74
+ * @param array $rows
75
+ * @param array $formdata
76
+ *
77
+ * @return string
78
+ */
79
  static function table( $rows, $formdata = null ) {
80
  $output = '';
81
  foreach ( $rows as $row )
86
  return $output;
87
  }
88
 
89
+ /**
90
+ * Generates a table row
91
+ *
92
+ * @param array $args
93
+ * @param array $formdata
94
+ *
95
+ * @return string
96
+ */
97
  static function table_row( $args, $formdata = null ) {
98
  return self::row_wrap( $args['title'], self::input( $args, $formdata ) );
99
  }
101
 
102
  // ____________WRAPPERS____________
103
 
104
+ /**
105
+ * @param string $content
106
+ * @param string $nonce
107
+ *
108
+ * @return string
109
+ */
110
  static function form_table_wrap( $content, $nonce = 'update_options' ) {
111
  return self::form_wrap( self::table_wrap( $content ), $nonce );
112
  }
113
 
114
+ /**
115
+ * @param string $content
116
+ * @param string $nonce
117
+ *
118
+ * @return string
119
+ */
120
  static function form_wrap( $content, $nonce = 'update_options' ) {
121
  return html( "form method='post' action=''",
122
  $content,
124
  );
125
  }
126
 
127
+ /**
128
+ * @param string $content
129
+ *
130
+ * @return string
131
+ */
132
  static function table_wrap( $content ) {
133
  return html( "table class='form-table'", $content );
134
  }
135
 
136
+ /**
137
+ * @param string $title
138
+ * @param string $content
139
+ *
140
+ * @return string
141
+ */
142
  static function row_wrap( $title, $content ) {
143
+ return html( 'tr',
144
  html( "th scope='row'", $title ),
145
+ html( 'td', $content )
146
  );
147
  }
148
 
175
  /**
176
  * Traverses the formdata and retrieves the correct value.
177
  *
178
+ * @param string $name The name of the value
179
+ * @param array $value The data that will be traversed
180
+ * @param mixed $fallback The value returned when the key is not found
181
  *
182
  * @return mixed
183
  */
195
  /**
196
  * Given a list of fields, validate some data.
197
  *
198
+ * @param array $fields List of args that would be sent to scbForms::input()
199
+ * @param array $data The data to validate. Defaults to $_POST
200
  * @param array $to_update Existing data to populate. Necessary for nested values
201
  *
202
  * @return array
225
  * For single-choice fields, we can't distinguish either, because of how self::update_meta() works.
226
  * Therefore, the 'default' parameter is always ignored.
227
  *
228
+ * @param array $args Field arguments.
229
+ * @param int $object_id The object ID the metadata is attached to
230
  * @param string $meta_type
231
  *
232
  * @return string
242
  return self::input_with_value( $args, $value );
243
  }
244
 
245
+ /**
246
+ * @param array $fields
247
+ * @param array $data
248
+ * @param int $object_id
249
+ * @param string $meta_type
250
+ */
251
  static function update_meta( $fields, $data, $object_id, $meta_type = 'post' ) {
252
  foreach ( $fields as $field_args ) {
253
  $key = $field_args['name'];
263
  foreach ( array_diff( $old_values, $new_values ) as $value )
264
  delete_metadata( $meta_type, $object_id, $key, $value );
265
  } else {
266
+ $value = isset( $data[$key] ) ? $data[$key] : '';
267
 
268
  if ( '' === $value )
269
  delete_metadata( $meta_type, $object_id, $key );
273
  }
274
  }
275
 
276
+ /**
277
+ * @param array $arr
278
+ * @param string $name
279
+ * @param mixed $value
280
+ */
281
  private static function set_value( &$arr, $name, $value ) {
282
  $name = (array) $name;
283
 
301
  * A wrapper for scbForms, containing the formdata
302
  */
303
  class scbForm {
304
+ protected $data = array();
305
  protected $prefix = array();
306
 
307
+ /**
308
+ * @param array $data
309
+ * @param string|boolean $prefix
310
+ */
311
  function __construct( $data, $prefix = false ) {
312
  if ( is_array( $data ) )
313
  $this->data = $data;
316
  $this->prefix = (array) $prefix;
317
  }
318
 
319
+ /**
320
+ * @param string $path
321
+ *
322
+ * @return scbForm
323
+ */
324
  function traverse_to( $path ) {
325
  $data = scbForms::get_value( $path, $this->data );
326
 
329
  return new scbForm( $data, $prefix );
330
  }
331
 
332
+ /**
333
+ * @param array $args
334
+ *
335
+ * @return string
336
+ */
337
  function input( $args ) {
338
  $value = scbForms::get_value( $args['name'], $this->data );
339
 
345
  }
346
  }
347
 
348
+ /**
349
+ * Interface for form fields.
350
+ */
351
  interface scbFormField_I {
352
 
353
  /**
369
  function validate( $value );
370
  }
371
 
372
+ /**
373
+ * Base class for form fields implementations.
374
+ */
375
  abstract class scbFormField implements scbFormField_I {
376
 
377
  protected $args;
378
 
379
+ /**
380
+ * @param array|scbFormField_I $args
381
+ *
382
+ * @return mixed false on failure or instance of form class
383
+ */
384
  public static function create( $args ) {
385
  if ( is_a( $args, 'scbFormField_I' ) )
386
  return $args;
403
  $args['extra'] = shortcode_parse_atts( $args['extra'] );
404
 
405
  $args = wp_parse_args( $args, array(
406
+ 'desc' => '',
407
+ 'desc_pos' => 'after',
408
+ 'wrap' => scbForms::TOKEN,
409
  'wrap_each' => scbForms::TOKEN,
410
  ) );
411
 
430
  }
431
  }
432
 
433
+ /**
434
+ * @param array $args
435
+ */
436
  protected function __construct( $args ) {
437
  $this->args = $args;
438
  }
439
 
440
+ /**
441
+ * @param string $key
442
+ *
443
+ * @return mixed
444
+ */
445
  public function __get( $key ) {
446
  return $this->args[ $key ];
447
  }
448
 
449
+ /**
450
+ * @param string $key
451
+ *
452
+ * @return bool
453
+ */
454
  public function __isset( $key ) {
455
  return isset( $this->args[ $key ] );
456
  }
457
 
458
+ /**
459
+ * @param mixed $value
460
+ *
461
+ * @return string
462
+ */
463
  public function render( $value = null ) {
464
  if ( null === $value && isset( $this->default ) )
465
  $value = $this->default;
474
  return str_replace( scbForms::TOKEN, $this->_render( $args ), $this->wrap );
475
  }
476
 
477
+ /**
478
+ * Mutate the field arguments so that the value passed is rendered.
479
+ *
480
+ * @param array $args
481
+ * @param mixed $value
482
+ */
483
  abstract protected function _set_value( &$args, $value );
484
 
485
+ /**
486
+ * The actual rendering
487
+ *
488
+ * @param array $args
489
+ */
490
  abstract protected function _render( $args );
491
 
492
+ /**
493
+ * Handle args for a single checkbox or radio input
494
+ *
495
+ * @param array $args
496
+ *
497
+ * @return string
498
+ */
499
  protected static function _checkbox( $args ) {
500
  $args = wp_parse_args( $args, array(
501
+ 'value' => true,
502
+ 'desc' => null,
503
  'checked' => false,
504
+ 'extra' => array(),
505
  ) );
506
 
507
+ $args['extra']['checked'] = $args['checked'];
 
 
508
 
509
+ if ( is_null( $args['desc'] ) && ! is_bool( $args['value'] ) )
510
+ $args['desc'] = str_replace( '[]', '', $args['value'] );
 
 
511
 
512
  return self::_input_gen( $args );
513
  }
514
 
515
+ /**
516
+ * Generate html with the final args
517
+ *
518
+ * @param array $args
519
+ *
520
+ * @return string
521
+ */
522
  protected static function _input_gen( $args ) {
523
+ $args = wp_parse_args( $args, array(
524
  'value' => null,
525
+ 'desc' => null,
526
+ 'extra' => array(),
527
+ ) );
528
 
529
+ $args['extra']['name'] = $args['name'];
530
 
531
+ if ( 'textarea' == $args['type'] ) {
532
+ $input = html( 'textarea', $args['extra'], esc_textarea( $args['value'] ) );
533
  } else {
534
+ $args['extra']['value'] = $args['value'];
535
+ $args['extra']['type'] = $args['type'];
536
+ $input = html( 'input', $args['extra'] );
537
  }
538
 
539
+ return self::add_label( $input, $args['desc'], $args['desc_pos'] );
540
  }
541
 
542
+ /**
543
+ * @param string $input
544
+ * @param string $desc
545
+ * @param string $desc_pos
546
+ *
547
+ * @return string
548
+ */
549
  protected static function add_label( $input, $desc, $desc_pos ) {
550
  return html( 'label', self::add_desc( $input, $desc, $desc_pos ) ) . "\n";
551
  }
552
 
553
+ /**
554
+ * @param string $input
555
+ * @param string $desc
556
+ * @param string $desc_pos
557
+ *
558
+ * @return string
559
+ */
560
  protected static function add_desc( $input, $desc, $desc_pos ) {
561
  if ( empty( $desc ) )
562
  return $input;
567
  return $input . ' ' . $desc;
568
  }
569
 
570
+ /**
571
+ * @param array $args
572
+ */
573
  private static function _expand_choices( &$args ) {
574
  $choices =& $args['choices'];
575
 
583
  }
584
  }
585
 
586
+ /**
587
+ * @param array $array
588
+ *
589
+ * @return bool
590
+ */
591
  private static function is_associative( $array ) {
592
  $keys = array_keys( $array );
593
  return array_keys( $keys ) !== $keys;
594
  }
595
  }
596
 
597
+ /**
598
+ * Text form field.
599
+ */
600
  class scbTextField extends scbFormField {
601
 
602
+ /**
603
+ * @param string $value
604
+ *
605
+ * @return string
606
+ */
607
  public function validate( $value ) {
608
  $sanitize = isset( $this->sanitize ) ? $this->sanitize : 'wp_filter_kses';
609
 
610
  return call_user_func( $sanitize, $value, $this );
611
  }
612
 
613
+ /**
614
+ * @param array $args
615
+ *
616
+ * @return string
617
+ */
618
  protected function _render( $args ) {
619
  $args = wp_parse_args( $args, array(
620
+ 'value' => '',
621
  'desc_pos' => 'after',
622
+ 'extra' => array( 'class' => 'regular-text' ),
623
  ) );
624
 
625
+ if ( ! isset( $args['extra']['id'] ) && ! is_array( $args['name'] ) && false === strpos( $args['name'], '[' ) )
626
+ $args['extra']['id'] = $args['name'];
 
 
 
 
627
 
628
  return scbFormField::_input_gen( $args );
629
  }
630
 
631
+ /**
632
+ * @param array $args
633
+ * @param string $value
634
+ */
635
  protected function _set_value( &$args, $value ) {
636
  $args['value'] = $value;
637
  }
638
  }
639
 
640
+ /**
641
+ * Base class for form fields with single choice.
642
+ */
643
  abstract class scbSingleChoiceField extends scbFormField {
644
 
645
+ /**
646
+ * @param mixed $value
647
+ *
648
+ * @return mixed|null
649
+ */
650
  public function validate( $value ) {
651
  if ( isset( $this->choices[ $value ] ) )
652
  return $value;
654
  return null;
655
  }
656
 
657
+ /**
658
+ * @param array $args
659
+ *
660
+ * @return string
661
+ */
662
  protected function _render( $args ) {
663
  $args = wp_parse_args( $args, array(
664
+ 'numeric' => false, // use numeric array instead of associative
665
+ 'selected' => array( 'foo' ), // hack to make default blank
666
  ) );
667
 
668
  return $this->_render_specific( $args );
669
  }
670
 
671
+ /**
672
+ * @param array $args
673
+ * @param mixed $value
674
+ */
675
  protected function _set_value( &$args, $value ) {
676
  $args['selected'] = $value;
677
  }
678
 
679
+ /**
680
+ * @param array $args
681
+ *
682
+ * @return string
683
+ */
684
  abstract protected function _render_specific( $args );
685
  }
686
 
687
+ /**
688
+ * Dropdown field.
689
+ */
690
  class scbSelectField extends scbSingleChoiceField {
691
 
692
+ /**
693
+ * @param array $args
694
+ *
695
+ * @return string
696
+ */
697
  protected function _render_specific( $args ) {
698
+ $args = wp_parse_args( $args, array(
699
+ 'text' => false,
700
+ 'extra' => array(),
701
+ ) );
702
 
703
  $options = array();
704
 
705
+ if ( false !== $args['text'] ) {
706
  $options[] = array(
707
+ 'value' => '',
708
+ 'selected' => ( $args['selected'] == array( 'foo' ) ),
709
+ 'title' => $args['text'],
710
  );
711
  }
712
 
713
+ foreach ( $args['choices'] as $value => $title ) {
714
  $options[] = array(
715
+ 'value' => $value,
716
+ 'selected' => ( $value == $args['selected'] ),
717
+ 'title' => $title,
718
  );
719
  }
720
 
721
  $opts = '';
722
  foreach ( $options as $option ) {
723
+ $opts .= html( 'option', array( 'value' => $option['value'], 'selected' => $option['selected'] ), $option['title'] );
 
 
724
  }
725
 
726
+ $args['extra']['name'] = $args['name'];
727
 
728
+ $input = html( 'select', $args['extra'], $opts );
729
 
730
+ return scbFormField::add_label( $input, $args['desc'], $args['desc_pos'] );
731
  }
732
  }
733
 
734
+ /**
735
+ * Radio field.
736
+ */
737
  class scbRadiosField extends scbSelectField {
738
 
739
+ /**
740
+ * @param array $args
741
+ *
742
+ * @return string
743
+ */
744
  protected function _render_specific( $args ) {
 
745
 
746
+ if ( array( 'foo' ) == $args['selected'] ) {
747
  // radio buttons should always have one option selected
748
+ $args['selected'] = key( $args['choices'] );
749
  }
750
 
751
  $opts = '';
752
+ foreach ( $args['choices'] as $value => $title ) {
753
  $single_input = scbFormField::_checkbox( array(
754
+ 'name' => $args['name'],
755
+ 'type' => 'radio',
756
+ 'value' => $value,
757
+ 'checked' => ( $value == $args['selected'] ),
758
+ 'desc' => $title,
759
+ 'desc_pos' => 'after',
760
  ) );
761
 
762
+ $opts .= str_replace( scbForms::TOKEN, $single_input, $args['wrap_each'] );
763
  }
764
 
765
+ return scbFormField::add_desc( $opts, $args['desc'], $args['desc_pos'] );
766
  }
767
  }
768
 
769
+ /**
770
+ * Checkbox field with multiple choices.
771
+ */
772
  class scbMultipleChoiceField extends scbFormField {
773
 
774
+ /**
775
+ * @param mixed $value
776
+ *
777
+ * @return array
778
+ */
779
  public function validate( $value ) {
780
  return array_intersect( array_keys( $this->choices ), (array) $value );
781
  }
782
 
783
+ /**
784
+ * @param array $args
785
+ *
786
+ * @return string
787
+ */
788
  protected function _render( $args ) {
789
  $args = wp_parse_args( $args, array(
790
+ 'numeric' => false, // use numeric array instead of associative
791
  'checked' => null,
792
  ) );
793
 
794
+ if ( ! is_array( $args['checked'] ) )
795
+ $args['checked'] = array();
 
 
796
 
797
  $opts = '';
798
+ foreach ( $args['choices'] as $value => $title ) {
799
  $single_input = scbFormField::_checkbox( array(
800
+ 'name' => $args['name'] . '[]',
801
+ 'type' => 'checkbox',
802
+ 'value' => $value,
803
+ 'checked' => in_array( $value, $args['checked'] ),
804
+ 'desc' => $title,
805
+ 'desc_pos' => 'after',
806
  ) );
807
 
808
+ $opts .= str_replace( scbForms::TOKEN, $single_input, $args['wrap_each'] );
809
  }
810
 
811
+ return scbFormField::add_desc( $opts, $args['desc'], $args['desc_pos'] );
812
  }
813
 
814
+ /**
815
+ * @param array $args
816
+ * @param mixed $value
817
+ */
818
  protected function _set_value( &$args, $value ) {
819
  $args['checked'] = (array) $value;
820
  }
821
  }
822
 
823
+ /**
824
+ * Checkbox field.
825
+ */
826
  class scbSingleCheckboxField extends scbFormField {
827
 
828
+ /**
829
+ * @param mixed $value
830
+ *
831
+ * @return boolean
832
+ */
833
  public function validate( $value ) {
834
  return (bool) $value;
835
  }
836
 
837
+ /**
838
+ * @param array $args
839
+ *
840
+ * @return string
841
+ */
842
  protected function _render( $args ) {
843
  $args = wp_parse_args( $args, array(
844
+ 'value' => true,
845
+ 'desc' => null,
846
  'checked' => false,
847
+ 'extra' => array(),
848
  ) );
849
 
850
+ $args['extra']['checked'] = $args['checked'];
 
 
 
 
851
 
852
+ if ( is_null( $args['desc'] ) && ! is_bool( $args['value'] ) )
853
+ $args['desc'] = str_replace( '[]', '', $args['value'] );
854
 
855
  return scbFormField::_input_gen( $args );
856
  }
857
 
858
+ /**
859
+ * @param array $args
860
+ * @param mixed $value
861
+ */
862
  protected function _set_value( &$args, $value ) {
863
  $args['checked'] = ( $value || ( isset( $args['value'] ) && $value == $args['value'] ) );
864
  }
865
  }
866
 
867
+ /**
868
+ * Wrapper field for custom callbacks.
869
+ */
870
  class scbCustomField implements scbFormField_I {
871
 
872
  protected $args;
873
 
874
+ /**
875
+ * @param array $args
876
+ */
877
  function __construct( $args ) {
878
  $this->args = wp_parse_args( $args, array(
879
+ 'render' => 'var_dump',
880
  'sanitize' => 'wp_filter_kses',
881
  ) );
882
  }
883
 
884
+ /**
885
+ * @param string $key
886
+ *
887
+ * @return mixed
888
+ */
889
  public function __get( $key ) {
890
  return $this->args[ $key ];
891
  }
892
 
893
+ /**
894
+ * @param string $key
895
+ *
896
+ * @return boolean
897
+ */
898
  public function __isset( $key ) {
899
  return isset( $this->args[ $key ] );
900
  }
901
 
902
+ /**
903
+ * @param mixed $value
904
+ *
905
+ * @return string
906
+ */
907
  public function render( $value = null ) {
908
  return call_user_func( $this->render, $value, $this );
909
  }
910
 
911
+ /**
912
+ * @param mixed $value
913
+ *
914
+ * @return mixed
915
+ */
916
  public function validate( $value ) {
917
  return call_user_func( $this->sanitize, $value, $this );
918
  }
scb/PostMetabox.php ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class scbPostMetabox {
4
+
5
+ private $id, $title;
6
+
7
+ private $post_types;
8
+
9
+ private $post_data = array();
10
+
11
+ protected $actions = array( 'admin_enqueue_scripts', 'post_updated_messages' );
12
+
13
+ public function __construct( $id, $title, $args = array() ) {
14
+ $this->id = $id;
15
+ $this->title = $title;
16
+
17
+ $args = wp_parse_args( $args, array(
18
+ 'post_type' => 'post',
19
+ 'context' => 'advanced',
20
+ 'priority' => 'default'
21
+ ) );
22
+
23
+ if ( is_string( $args['post_type'] ) )
24
+ $args['post_type'] = array( $args['post_type'] );
25
+ $this->post_types = $args['post_type'];
26
+
27
+ $this->context = $args['context'];
28
+ $this->priority = $args['priority'];
29
+
30
+ add_action( 'load-post.php', array( $this, 'pre_register' ) );
31
+ add_action( 'load-post-new.php', array( $this, 'pre_register' ) );
32
+ }
33
+
34
+ final public function pre_register() {
35
+ if ( ! in_array( get_current_screen()->post_type, $this->post_types ) )
36
+ return;
37
+
38
+ if ( ! $this->condition() )
39
+ return;
40
+
41
+ if ( isset( $_GET['post'] ) )
42
+ $this->post_data = $this->get_meta( intval( $_GET['post'] ) );
43
+
44
+ add_action( 'add_meta_boxes', array( $this, 'register' ) );
45
+ add_action( 'save_post', array( $this, '_save_post' ), 10, 2 );
46
+
47
+ foreach ( $this->actions as $action ) {
48
+ if ( method_exists( $this, $action ) )
49
+ add_action( $action, array( $this, $action ) );
50
+ }
51
+ }
52
+
53
+ // Additional checks before registering the metabox
54
+ protected function condition() {
55
+ return true;
56
+ }
57
+
58
+ final public function register() {
59
+ add_meta_box( $this->id, $this->title, array( $this, 'display' ), null, $this->context, $this->priority );
60
+ }
61
+
62
+ public function before_display( $form_data, $post ) {
63
+ return $form_data;
64
+ }
65
+
66
+ public function display( $post ) {
67
+ $form_fields = $this->form_fields();
68
+ if ( ! $form_fields )
69
+ return;
70
+
71
+ $form_data = $this->post_data;
72
+ $error_fields = array();
73
+
74
+ if ( isset( $form_data['_error_data_' . $this->id ] ) ) {
75
+ $data = unserialize( $form_data['_error_data_' . $this->id ] );
76
+
77
+ $error_fields = $data['fields'];
78
+ $form_data = $data['data'];
79
+ }
80
+
81
+ $form_data = $this->before_display( $form_data, $post );
82
+
83
+ $this->before_form( $post );
84
+ echo $this->table( $form_fields, $form_data, $error_fields );
85
+ $this->after_form( $post );
86
+
87
+ delete_post_meta( $post->ID, '_error_data_' . $this->id );
88
+ }
89
+
90
+ public function table( $rows, $formdata, $errors = array() ) {
91
+ $output = '';
92
+ foreach ( $rows as $row ) {
93
+ $output .= $this->table_row( $row, $formdata, $errors );
94
+ }
95
+
96
+ $output = scbForms::table_wrap( $output );
97
+
98
+ return $output;
99
+ }
100
+
101
+ public function table_row( $row, $formdata, $errors = array() ) {
102
+ $input = scbForms::input( $row, $formdata );
103
+
104
+ // If row has an error, highlight it
105
+ $style = ( in_array( $row['name'], $errors ) ) ? 'style= "background-color: #FFCCCC"' : '';
106
+
107
+ return html( 'tr',
108
+ html( "th $style scope='row'", $row['title'] ),
109
+ html( "td $style", $input )
110
+ );
111
+ }
112
+
113
+ // Display some extra HTML before the form
114
+ public function before_form( $post ) { }
115
+
116
+ // Return the list of form fields
117
+ public function form_fields() {
118
+ return array();
119
+ }
120
+
121
+ // Display some extra HTML after the form
122
+ public function after_form( $post ) { }
123
+
124
+ // Makes sure that the saving occurs only for the post being edited
125
+ final public function _save_post( $post_id, $post ) {
126
+ if ( ! isset( $_POST['action'] ) || $_POST['action'] != 'editpost' )
127
+ return;
128
+
129
+ if ( $post_id != $_POST['post_ID'] )
130
+ return;
131
+
132
+ if ( ! in_array( $post->post_type, $this->post_types ) )
133
+ return;
134
+
135
+ $this->save( $post->ID );
136
+ }
137
+
138
+ protected function save( $post_id ) {
139
+ $form_fields = $this->form_fields();
140
+
141
+ $to_update = scbForms::validate_post_data( $form_fields );
142
+
143
+ // Filter data
144
+ $to_update = $this->before_save( $to_update, $post_id );
145
+
146
+ // Validate dataset
147
+ $is_valid = $this->validate_post_data( $to_update, $post_id );
148
+ if ( $is_valid instanceof WP_Error && $is_valid->get_error_codes() ) {
149
+
150
+ $error_data = array(
151
+ 'fields' => $is_valid->get_error_codes(),
152
+ 'data' => $to_update
153
+ );
154
+ update_post_meta( $post_id, '_error_data_' . $this->id, $error_data );
155
+
156
+ $location = add_query_arg( 'message', 1, get_edit_post_link( $post_id, 'url' ) );
157
+ wp_redirect( apply_filters( 'redirect_post_location', $location, $post_id ) );
158
+ exit;
159
+ }
160
+
161
+ foreach ( $to_update as $key => $value ) {
162
+ update_post_meta( $post_id, $key, $value );
163
+ }
164
+ }
165
+
166
+ protected function before_save( $post_data, $post_id ) {
167
+ return $post_data;
168
+ }
169
+
170
+ protected function validate_post_data( $post_data ) {
171
+ return false;
172
+ }
173
+
174
+ private function get_meta( $post_id ) {
175
+ $meta = get_post_custom( $post_id );
176
+ foreach ( $meta as $key => $values )
177
+ $meta[$key] = $meta[$key][0];
178
+
179
+ return $meta;
180
+ }
181
+ }
182
+
scb/load.php CHANGED
@@ -1,8 +1,8 @@
1
  <?php
2
 
3
- $GLOBALS['_scb_data'] = array( 57, __FILE__, array(
4
  'scbUtil', 'scbOptions', 'scbForms', 'scbTable',
5
- 'scbWidget', 'scbAdminPage', 'scbBoxesPage',
6
  'scbCron', 'scbHooks',
7
  ) );
8
 
1
  <?php
2
 
3
+ $GLOBALS['_scb_data'] = array( 58, __FILE__, array(
4
  'scbUtil', 'scbOptions', 'scbForms', 'scbTable',
5
+ 'scbWidget', 'scbAdminPage', 'scbBoxesPage', 'scbPostMetabox',
6
  'scbCron', 'scbHooks',
7
  ) );
8