eth-timeline.php 14.9 KB
Newer Older
1
2
3
<?php
/*
Plugin Name: ETH Timeline
Erick Hitter's avatar
Erick Hitter committed
4
5
Plugin URI: https://ethitter.com/plugins/
Description: List whereabouts by year and month
6
Author: Erick Hitter
7
Version: 0.2
Erick Hitter's avatar
Erick Hitter committed
8
Author URI: https://ethitter.com/
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

class ETH_Timeline {
	/**
	 * Singleton
	 */
	private static $instance = null;

	/**
	 * Class variables
	 */
	private $post_type = 'eth_timeline';

36
37
	private $meta_start = '_eth_timeline_start';
	private $meta_end = '_eth_timeline_end';
38

39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
	/**
	 * Silence is golden!
	 */
	private function __construct() {}

	/**
	 * Instantiate singleton
	 */
	public static function get_instance() {
		if ( ! is_a( self::$instance, __CLASS__ ) ) {
			self::$instance = new self;

			self::$instance->setup();
		}

		return self::$instance;
	}

	/**
58
	 * Register actions and filters
59
	 *
60
61
62
	 * @uses add_action
	 * @uses add_filter
	 * @return null
63
64
65
	 */
	private function setup() {
		add_action( 'init', array( $this, 'action_init' ) );
66

67
68
		add_action( 'pre_get_posts', array( $this, 'action_pre_get_posts' ) );

69
70
		add_action( 'admin_enqueue_scripts', array( $this, 'action_admin_enqueue_scripts' ) );
		add_action( 'add_meta_boxes_' . $this->post_type, array( $this, 'action_add_meta_boxes' ) );
71
		add_action( 'save_post', array( $this, 'action_save_post' ) );
72

73
74
75
		add_filter( 'manage_' . $this->post_type . '_posts_columns', array( $this, 'filter_list_table_columns' ) );
		add_action( 'manage_' . $this->post_type . '_posts_custom_column', array( $this, 'do_list_table_columns' ), 10, 2 );

Erick Hitter's avatar
Erick Hitter committed
76
		add_filter( 'enter_title_here', array( $this, 'filter_editor_title_prompt' ), 10, 2 );
77
78
79
	}

	/**
80
	 * Register post type and shortcode
81
	 *
82
83
84
85
	 * @uses register_post_type
	 * @uses add_shortcode
	 * @action init
	 * @return null
86
87
88
	 */
	public function action_init() {
		register_post_type( $this->post_type, array(
89
			'label'               => __( 'Timeline', 'eth-timeline' ),
90
			'labels'              => array(
91
92
93
94
95
96
97
98
99
100
101
102
103
104
				'name'               => __( 'Timeline', 'eth-timeline' ),
				'singular_name'      => __( 'Timeline', 'eth-timeline' ),
				'menu_name'          => __( 'Timeline', 'eth-timeline' ),
				'all_items'          => __( 'All Entries', 'eth-timeline' ),
				'add_new'            => __( 'Add New', 'eth-timeline' ),
				'add_new_item'       => __( 'Add New', 'eth-timeline' ),
				'edit_item'          => __( 'Edit Entry', 'eth-timeline' ),
				'new_item'           => __( 'New Entry', 'eth-timeline' ),
				'view_item'          => __( 'View Entry', 'eth-timeline' ),
				'items_archive'      => __( 'Entries List', 'eth-timeline' ),
				'search_items'       => __( 'Search Timeline Entries', 'eth-timeline' ),
				'not_found'          => __( 'No entries found', 'eth-timeline' ),
				'not_found_in_trash' => __( 'No trashed entries', 'eth-timeline' ),
				'parent_item_colon'  => __( 'Entries:', 'eth-timeline' ),
105
106
			),
			'public'              => true,
107
			'has_archive'         => false,
108
109
110
			'exclude_from_search' => true,
			'show_in_nav_menus'   => false,
			'show_in_admin_bar'   => true,
111
			'rewrite'             => false,
112
113
114
115
116
117
			'supports'            => array(
				'title',
				'editor',
				'author',
			)
		) );
118
119

		add_shortcode( 'eth-timeline', array( $this, 'do_shortcode' ) );
120
121
	}

122
123
124
125
126
127
128
129
130
131
	/**
	 * Force all timeline queries to be sorted by start date.
	 * Doesn't interfere with admin list table sorting.
	 *
	 * @param object $query
	 * @uses is_admin
	 * @action pre_get_posts
	 * @return null
	 */
	public function action_pre_get_posts( $query ) {
132
		if ( $query->is_main_query() && $this->post_type == $query->get( 'post_type' ) ) {
133
			if ( is_admin() && isset( $_GET['orderby'] ) ) {
134
				return;
135
			}
136
137
138
139
140
141

			$query->set( 'orderby', 'meta_value_num' );
			$query->set( 'meta_key', $this->meta_start );
		}
	}

142
143
144
145
	/**
	 ** ADMINISTRATION
	 */

146
	/**
147
	 * Enqueue admin assets
148
	 *
149
150
151
152
153
154
155
	 * @uses get_current_screen
	 * @uses is_wp_error
	 * @uses wp_enqueue_script
	 * @uses plugins_url
	 * @uses wp_enqueue_style
	 * @action admin_enqueue_scripts
	 * @return null
156
	 */
157
	public function action_admin_enqueue_scripts() {
158
159
		$screen = get_current_screen();

160
		if ( is_object( $screen ) && ! is_wp_error( $screen ) && $this->post_type = $screen->post_type ) {
161
			wp_enqueue_script( 'eth-timeline-admin', plugins_url( 'js/admin.js', __FILE__ ), array( 'jquery', 'jquery-ui-datepicker' ), 20130721, false );
162
163

			wp_enqueue_style( 'eth-timeline-admin', plugins_url( 'css/smoothness.min.css', __FILE__ ), array(), 20130721, 'screen' );
164
165
166
167
		}
	}

	/**
168
	 * Register custom date metabox
169
	 *
170
171
172
	 * @uses add_meta_box
	 * @action add_meta_boxes
	 * @return null
173
174
175
176
177
178
	 */
	public function action_add_meta_boxes() {
		add_meta_box( 'eth-timeline-dates', __( 'Dates', 'eth-timeline' ), array( $this, 'meta_box_dates' ), $this->post_type, 'normal', 'high' );
	}

	/**
179
	 * Render dates metabox
180
	 *
181
182
183
184
185
186
187
188
	 * @param object $post
	 * @uses get_post_meta
	 * @uses _e
	 * @uses wp_nonce_field
	 * @uses ths::get_field_name
	 * @uses this::get_nonce_name
	 * @action add_meta_boxes_{$this->post_type}
	 * @return string
189
190
	 */
	public function meta_box_dates( $post ) {
191
		$times = $this->get_times( $post->ID );
192
193

		?>
194
195
		<p id="eth-timeline-startbox">
			<label for="eth-timeline-start"><?php _e( 'Start:', 'eth-timeline' ); ?></label>
196
			<input type="text" name="eth-timeline[start]" id="eth-timeline-start" class="regular-text" style="width: 11em;" value="<?php echo date( 'F j, Y', $times['start'] ); ?>" />
197
198
199
200
		</p>

		<p id="eth-timeline-endbox">
			<label for="eth-timeline-end"><?php _e( 'End:', 'eth-timeline' ); ?></label>
201
			<input type="text" name="eth-timeline[end]" id="eth-timeline-end" class="regular-text" style="width: 11em;" value="<?php echo date( 'F j, Y', $times['end'] ); ?>" />
202
		</p>
203
		<?php
204
205

		wp_nonce_field( $this->get_field_name( 'date' ), $this->get_nonce_name( 'date' ), false );
206
207
	}

208
	/**
209
	 * Save custom dates
210
	 *
211
212
213
214
215
216
217
218
	 * @param int $post_id
	 * @uses get_post_type
	 * @uses this::get_nonce_name
	 * @uses this::get_field_name
	 * @uses update_post_meta
	 * @uses delete_post_meta
	 * @action save_post
	 * @return null
219
220
	 */
	public function action_save_post( $post_id ) {
221
		if ( $this->post_type != get_post_type( $post_id ) ) {
222
			return;
223
		}
224

225
		if ( isset( $_POST[ $this->get_nonce_name( 'date' ) ] ) && wp_verify_nonce( $_POST[ $this->get_nonce_name( 'date' ) ], $this->get_field_name( 'date' ) ) ) {
226
			$dates = isset( $_POST['eth-timeline'] ) ? $_POST['eth-timeline'] : array();
227

228
			if ( empty( $dates ) ) {
229
				return;
230
			}
231

232
			foreach ( $dates as $key => $date ) {
233
				if ( ! in_array( $key, array( 'start', 'end' ) ) ) {
234
					continue;
235
				}
236

237
				// Timestamp comes from JS
238
				if ( empty( $date ) ) {
239
					$timestamp = false;
240
				} else {
241
					$timestamp = strtotime( $date );
242
				}
243

244
				if ( $timestamp ) {
245
					update_post_meta( $post_id, $this->{'meta_' . $key}, $timestamp );
246
				} else {
247
					delete_post_meta( $post_id, $this->{'meta_' . $key} );
248
				}
249
			}
250
251
252
		}
	}

253
254
255
256
257
258
259
260
261
262
263
264
265
	/**
	 * Add new date columns to list table
	 *
	 * @param array $columns
	 * @uses __
	 * @filter manage_{$this->post_type}_posts_columns
	 * @return array
	 */
	public function filter_list_table_columns( $columns ) {
		$after = array_splice( $columns, 2 );

		$new_columns = array(
			'eth_timeline_start' => __( 'Start Date', 'eth-timeline' ),
266
			'eth_timeline_end'   => __( 'End Date (Optional)', 'eth-timeline' ),
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
		);

		$columns = $columns + $new_columns + $after;

		return $columns;
	}

	/**
	 * Display start and end dates in the post list table
	 *
	 * @param string $column
	 * @param int $post_id
	 * @uses get_post_meta
	 * @uses get_option
	 * @action manage_{$this->post_type}_posts_custom_column
	 * @return string or null
	 */
	public function do_list_table_columns( $column, $post_id ) {
		if ( in_array( $column, array( 'eth_timeline_start', 'eth_timeline_end' ) ) ) {
			$key = str_replace( 'eth_timeline_', '', $column );
			$date = get_post_meta( $post_id, $this->{'meta_' . $key}, true );

289
			if ( is_numeric( $date ) ) {
290
				echo date( get_option( 'date_format', 'F j, Y' ), $date );
291
			}
292
293
294
		}
	}

295
296
297
298
299
	/**
	 ** PRESENTATION
	 */

	/**
300
	 * Render list of timeline entries
301
	 *
302
303
304
305
306
307
308
	 * @global $post
	 * @param mixed $atts
	 * @uses shortcode_atts
	 * @uses WP_Query
	 * @uses this::get_times
	 * @uses the_ID
	 * @uses this::format_date
309
	 * @uses get_the_ID
310
311
312
313
314
315
316
	 * @uses the_title
	 * @uses get_the_content
	 * @uses remove_filter
	 * @uses the_content
	 * @uses add_filter
	 * @uses wp_reset_query
	 * @return string or null
317
318
319
	 */
	public function do_shortcode( $atts ) {
		// Parse and sanitize atts
320
		$atts = shortcode_atts( array(
321
322
323
			'posts_per_page' => 100,
			'order'          => 'DESC',
			'year'           => null,
324
		), $atts );
325

326
		$atts['posts_per_page'] = min( 200, max( (int) $atts['posts_per_page'], -1 ) );
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
		$atts['order']          = 'ASC' == $atts['order'] ? 'ASC' : 'DESC';
		$atts['year']           = is_numeric( $atts['year'] ) ? (int) $atts['year'] : null;

		// Build query
		$query = array(
			'post_type'      => $this->post_type,
			'posts_per_page' => $atts['posts_per_page'],
			'post_status'    => 'publish',
			'order'          => $atts['order'],
			'orderby'        => 'meta_value_num',
			'meta_key'       => $this->meta_start
		);

		if ( $atts['year'] ) {
			$query['meta_query'] = array(
				array(
					'key'     => $this->meta_start,
					'value'   => array( strtotime( $atts['year'] . '-01-01 00:00:00' ), strtotime( $atts['year'] . '-12-31 23:59:59' ) ),
					'type'    => 'numeric',
					'compare' => 'BETWEEN'
				)
			);
		}

		// Run query and build output, if possible
		$query = new WP_Query( $query );

		if ( $query->have_posts() ) {
			ob_start();

			global $post;

359
360
			echo '<div class="eth-timeline">';

361
362
			$year = $month = null;

363
364
365
			while ( $query->have_posts() ) {
				$query->the_post();

366
367
368
369
370
				$times = $this->get_times( $post->ID );

				// Deal with grouping by year
				if ( $year != date( 'Y', $times['start'] ) ) {
					if ( null !== $year ) {
371
						echo '</ul><!-- ' . $year . '-' . $month . ' --></ul><!-- ' . $year . ' -->' . "\n";
372
373
374
375
376
377
						$month = null;
					}

					$year = (int) date( 'Y', $times['start'] );

					echo '<div class="eth-timline-year-label">' . $year . '</div>' . "\n";
378
					echo '<ul class="eth-timeline-year eth-timeline-' . $year . '">' . "\n";
379
380
381
382
				}

				// Deal with grouping by month
				if ( $month != date( 'n', $times['start'] ) ) {
383
					if ( null !== $month ) {
384
						echo '</ul><!-- ' . $year . '-' . $month . ' --></li>' . "\n";
385
					}
386
387
388

					$month = (int) date( 'n', $times['start'] );

389
					echo '<li class="eth-timeline-month eth-timeline-month-' . $month . '">';
390
					echo '<div class="eth-timeline-month-label">' . date( 'F', $times['start'] ) . '</div>' . "\n";
391
					echo '<ul class="eth-timeline-month-items eth-timeline-' . $year . '-' . $month . '">' . "\n";
392
393
394
395
				}

				// Info about the item
				?>
396
				<li class="eth-timeline-item" id="eth-timeline-<?php the_ID(); ?>">
397
					<span class="eth-timeline-date"><?php echo $this->format_date( get_the_ID(), $year, $month ); ?>:</span>
Erick Hitter's avatar
Erick Hitter committed
398
					<span class="eth-timeline-location"><?php the_title(); ?></span>
399
400
401
402
403
404
405

					<?php
						$content = get_the_content();

						if ( ! empty( $content ) ) {
							$removed = remove_filter( 'the_content', 'wpautop' );

Erick Hitter's avatar
Erick Hitter committed
406
							echo ' <span class="eth-timeline-sep">&mdash;</span> <span class="eth-timeline-body">';
407
							the_content();
Erick Hitter's avatar
Erick Hitter committed
408
							echo '</span>';
409

410
							if ( $removed ) {
411
								add_filter( 'the_content', 'wpautop' );
412
							}
413
414
						}
					?>
415
				</li><!-- .eth-timeline-item#eth-timeline-<?php the_ID(); ?> -->
416
				<?php
417
418
			}

419
			// Ensure our tags are balanced!
420
421
422
423
			echo '</ul><!-- ' . $year . '-' . $month . ' -->';
			echo '</ul><!-- ' . $year . ' -->';

			echo '</div><!-- .eth-timeline -->';
424

425
426
427
428
429
430
431
432
433
			wp_reset_query();
			return ob_get_clean();
		}
	}

	/**
	 ** HELPERS
	 */

434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
	/**
	 * Retrieve timestamps for a given entry
	 *
	 * @param int $post_id
	 * @uses get_post_meta
	 * @return array
	 */
	private function get_times( $post_id ) {
		$post_id = (int) $post_id;

		$start = get_post_meta( $post_id, $this->meta_start, true );
		$start = is_numeric( $start ) ? (int) $start : '';

		$end = get_post_meta( $post_id, $this->meta_end, true );
		$end = is_numeric( $end ) ? (int) $end : '';

		return compact( 'start', 'end' );
	}

453
454
455
456
457
458
459
460
461
462
463
464
465
466
	/**
	 * Format entry dates for display
	 *
	 * @param int $post_id
	 * @param int $loop_year
	 * @param int $loop_month
	 * @uses this::get_times
	 * @uses this::format_single_date
	 * @return string
	 */
	private function format_date( $post_id, $loop_year, $loop_month ) {
		$times = $this->get_times( $post_id );

		if ( empty( $times['end'] ) || $times['end'] <= $times['start'] ) {
467
			return $this->format_single_date( $times['start'], false, true );
468
		} else {
469
470
471
472
473
474
475
476
477
			$start_year  = date( 'Y', $times['start'] );
			$end_year    = date( 'Y', $times['end'] );
			$end_month   = date( 'n', $times['end'] );

			$show_start_year  = ( $start_year != $end_year ) || ( $start_year != $loop_year );
			$show_end_year    = ( $start_year != $end_year ) || ( $end_year != $loop_year );
			$show_end_month   = $show_end_year || ( $end_month != $loop_month );

			return $this->format_single_date( $times['start'], $show_start_year, true ) . '&ndash;' . $this->format_single_date( $times['end'], $show_end_year, $show_end_month );
478
479
480
		}
	}

481
	/**
482
	 * Format timestamp into display date.
483
	 *
484
	 * @param int $timestamp
485
486
487
	 * @param bool $show_year
	 * @param bool $show_month
	 * @uses apply_filters
488
	 * @return string
489
	 */
490
	private function format_single_date( $timestamp, $show_year = false, $show_month = false ) {
491
492
		$format = 'j';

493
		if ( $show_year ) {
494
			$format .= ', Y';
495
		}
496

497
		if ( $show_month ) {
498
			$format = 'F ' . $format;
499
		}
500

501
502
		$format = apply_filters( 'eth_timeline_date_format', $format, $show_year, $show_month, $timestamp );

503
504
505
		return date( $format, $timestamp );
	}

Erick Hitter's avatar
Erick Hitter committed
506
507
508
509
510
511
512
513
514
515
516
	/**
	 * Provide better prompt text for the editor title field
	 *
	 * @param string $text
	 * @param object $post
	 * @uses get_post_type
	 * @uses __
	 * @filter enter_title_here
	 * @return string
	 */
	public function filter_editor_title_prompt( $text, $post ) {
517
		if ( $this->post_type == get_post_type( $post ) ) {
Erick Hitter's avatar
Erick Hitter committed
518
			$text = __( 'Enter destination here', 'eth-timeline' );
519
		}
Erick Hitter's avatar
Erick Hitter committed
520
521
522
523

		return $text;
	}

524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
	/**
	 * Return formatted field name
	 *
	 * @param string $field
	 * @return string
	 */
	private function get_field_name( $field ) {
		return $this->post_type . '_' . $field;
	}

	/**
	 * Return formatted nonce name
	 *
	 * @param string $field
	 * @uses this::get_field_name
	 * @return string
	 */
	private function get_nonce_name( $field ) {
		return $this->get_field_name( $field ) . '_nonce';
	}
}

ETH_Timeline::get_instance();