├── .gitignore ├── assets ├── images │ ├── screenshot-1.png │ ├── screenshot-2.png │ ├── screenshot-3.png │ └── screenshot-4.png ├── styles │ ├── as-speaking-frontend.css │ └── as-speaking-backend.css └── scripts │ ├── php-date-formatter.min.js │ ├── as-speaking-backend.js │ └── php-date-formatter.js ├── composer.json ├── src ├── Service.php ├── Model │ ├── Entity.php │ ├── CustomPostTypeRepository.php │ ├── TalkMeta.php │ ├── TalkRepository.php │ ├── CustomPostTypeEntity.php │ └── Talk.php ├── Exception │ ├── SpeakingPageException.php │ ├── InvalidPostID.php │ ├── InvalidAssetHandle.php │ ├── InvalidURI.php │ ├── InvalidService.php │ └── FailedToLoadView.php ├── Registerable.php ├── Assets │ ├── AssetsAware.php │ ├── Asset.php │ ├── AssetsAwareness.php │ ├── AssetsHandler.php │ ├── StyleAsset.php │ ├── ScriptAsset.php │ └── BaseAsset.php ├── Renderable.php ├── PluginFactory.php ├── CustomPostType │ ├── BaseCustomPostType.php │ └── Talk.php ├── View │ ├── View.php │ ├── TemplatedView.php │ ├── PostEscapedView.php │ ├── FormEscapedView.php │ └── BaseView.php ├── Shortcode │ ├── SpeakingPage.php │ └── BaseShortcode.php ├── Plugin.php ├── Metabox │ ├── Talk.php │ └── BaseMetabox.php ├── Widget │ ├── Talks.php │ └── BaseWidget.php └── Autoloader.php ├── views ├── talks-widget-form.php ├── talk-metabox.php ├── speaking-page.php ├── talks-widget.php ├── talks-widget-talk.php ├── talk-metabox-video.php ├── speaking-page-talk.php ├── talk-metabox-slides.php ├── talk-metabox-event.php ├── talk-metabox-image-link.php └── talk-metabox-session.php ├── LICENSE ├── as-speaking.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | -------------------------------------------------------------------------------- /assets/images/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schlessera/as-speaking/HEAD/assets/images/screenshot-1.png -------------------------------------------------------------------------------- /assets/images/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schlessera/as-speaking/HEAD/assets/images/screenshot-2.png -------------------------------------------------------------------------------- /assets/images/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schlessera/as-speaking/HEAD/assets/images/screenshot-3.png -------------------------------------------------------------------------------- /assets/images/screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schlessera/as-speaking/HEAD/assets/images/screenshot-4.png -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "schlessera/as-speaking", 3 | "description": "WordPress plugin to manage an archive of your talks", 4 | "type": "wordpress-plugin", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Alain Schlesser", 9 | "email": "alain.schlesser@gmail.com" 10 | } 11 | ], 12 | "minimum-stability": "dev", 13 | "require": {} 14 | } 15 | -------------------------------------------------------------------------------- /src/Service.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | /** 15 | * Interface Service. 16 | * 17 | * A generic service. 18 | * 19 | * @since 0.1.0 20 | * 21 | * @package AlainSchlesser\Speaking 22 | * @author Alain Schlesser 23 | */ 24 | interface Service extends Registerable { 25 | 26 | } 27 | -------------------------------------------------------------------------------- /assets/styles/as-speaking-frontend.css: -------------------------------------------------------------------------------- 1 | /* 2 | * AlainSchlesser.com Speaking Page Plugin. 3 | * 4 | * @package AlainSchlesser\Speaking 5 | * @author Alain Schlesser 6 | * @license MIT 7 | * @link https://www.alainschlesser.com/ 8 | * @copyright 2017 Alain Schlesser 9 | */ 10 | 11 | ul.talk__meta-container { 12 | overflow: auto; 13 | list-style-type: none; 14 | padding-left: 0; 15 | } 16 | 17 | li.talk__meta { 18 | float: left; 19 | border-right: 1px solid rgba(0, 0, 0, 0.3); 20 | padding: 0 20px; 21 | } 22 | 23 | li.talk__meta:last-child { 24 | border-right: none; 25 | } 26 | -------------------------------------------------------------------------------- /views/talks-widget-form.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | ?>

15 | 16 | 17 |

18 | -------------------------------------------------------------------------------- /src/Model/Entity.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Model; 13 | 14 | /** 15 | * Interface Entity. 16 | * 17 | * @since 0.2.1 18 | * 19 | * @package AlainSchlesser\Speaking 20 | * @author Alain Schlesser 21 | */ 22 | interface Entity { 23 | 24 | /** 25 | * Return the entity ID. 26 | * 27 | * @since 0.2.1 28 | * 29 | * @return int Entity ID. 30 | */ 31 | public function get_ID(); 32 | } 33 | -------------------------------------------------------------------------------- /src/Exception/SpeakingPageException.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Exception; 13 | 14 | /** 15 | * Interface SpeakingPageException. 16 | * 17 | * This interface is implemented by all speaking page exceptions, so that 18 | * we can catch "internal" exceptions only. 19 | * 20 | * @since 0.1.0 21 | * 22 | * @package AlainSchlesser\Speaking 23 | * @author Alain Schlesser 24 | */ 25 | interface SpeakingPageException { 26 | 27 | } 28 | -------------------------------------------------------------------------------- /views/talk-metabox.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | ?> 15 |
16 | nonce_field ?> 17 | render_partial( 'views/talk-metabox-event' ) ?> 18 | render_partial( 'views/talk-metabox-session' ) ?> 19 | render_partial( 'views/talk-metabox-video' ) ?> 20 | render_partial( 'views/talk-metabox-slides' ) ?> 21 | render_partial( 'views/talk-metabox-image-link' ) ?> 22 |
23 | -------------------------------------------------------------------------------- /src/Registerable.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | /** 15 | * Interface Registerable. 16 | * 17 | * An object that can be `register()`ed. 18 | * 19 | * @since 0.1.0 20 | * 21 | * @package AlainSchlesser\Speaking 22 | * @author Alain Schlesser 23 | */ 24 | interface Registerable { 25 | 26 | /** 27 | * Register the current Registerable. 28 | * 29 | * @since 0.1.0 30 | * 31 | * @return void 32 | */ 33 | public function register(); 34 | } 35 | -------------------------------------------------------------------------------- /src/Assets/AssetsAware.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Assets; 13 | 14 | /** 15 | * Interface AssetsAware. 16 | * 17 | * @since 0.1.0 18 | * 19 | * @package AlainSchlesser\Speaking 20 | * @author Alain Schlesser 21 | */ 22 | interface AssetsAware { 23 | 24 | /** 25 | * Set the assets handler to use within this object. 26 | * 27 | * @since 0.1.0 28 | * 29 | * @param AssetsHandler $assets Assets handler to use. 30 | */ 31 | public function with_assets_handler( AssetsHandler $assets ); 32 | } 33 | -------------------------------------------------------------------------------- /src/Renderable.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | /** 15 | * Interface Renderable. 16 | * 17 | * An object that can be `render()`ed. 18 | * 19 | * @since 0.1.0 20 | * 21 | * @package AlainSchlesser\Speaking 22 | * @author Alain Schlesser 23 | */ 24 | interface Renderable { 25 | 26 | /** 27 | * Render the current Renderable. 28 | * 29 | * @since 0.1.0 30 | * 31 | * @param array $context Context in which to render. 32 | * 33 | * @return string Rendered HTML. 34 | */ 35 | public function render( array $context = [] ); 36 | } 37 | -------------------------------------------------------------------------------- /src/PluginFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | /** 15 | * Class PluginFactory 16 | * 17 | * @since 0.2.7 18 | * 19 | * @package AlainSchlesser\Speaking 20 | * @author Alain Schlesser 21 | */ 22 | final class PluginFactory { 23 | 24 | /** 25 | * Create and return an instance of the plugin. 26 | * 27 | * This always returns a shared instance. 28 | * 29 | * @since 0.2.7 30 | * 31 | * @return Plugin Plugin instance. 32 | */ 33 | public static function create() { 34 | static $plugin = null; 35 | 36 | if ( null === $plugin ) { 37 | $plugin = new Plugin(); 38 | } 39 | 40 | return $plugin; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Model/CustomPostTypeRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Model; 13 | 14 | /** 15 | * Abstract class CustomPostTypeRepository. 16 | * 17 | * @since 0.1.0 18 | * 19 | * @package AlainSchlesser\Speaking 20 | * @author Alain Schlesser 21 | */ 22 | abstract class CustomPostTypeRepository { 23 | 24 | /** 25 | * Persist a modified entity to the storage. 26 | * 27 | * @since 0.1.0 28 | * 29 | * @param CustomPostTypeEntity $entity Entity instance to persist. 30 | */ 31 | public function persist( CustomPostTypeEntity $entity ) { 32 | wp_insert_post( $entity->get_post_object()->to_array() ); 33 | $entity->persist_properties(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Alain Schlesser 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /src/Exception/InvalidPostID.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Exception; 13 | 14 | /** 15 | * Class InvalidPostID. 16 | * 17 | * @since 0.1.0 18 | * 19 | * @package AlainSchlesser\Speaking\Exception 20 | * @author Alain Schlesser 21 | */ 22 | class InvalidPostID extends \InvalidArgumentException implements SpeakingPageException { 23 | 24 | /** 25 | * Create a new instance of the exception for a post ID that is not valid. 26 | * 27 | * @since 0.1.0 28 | * 29 | * @param int $id Post ID that is not valid. 30 | * 31 | * @return static 32 | */ 33 | public static function from_id( $id ) { 34 | $message = sprintf( 35 | 'The post ID "%d" is not valid.', 36 | $id 37 | ); 38 | 39 | return new static( $message ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Assets/Asset.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Assets; 13 | 14 | use AlainSchlesser\Speaking\Registerable; 15 | 16 | /** 17 | * Interface Asset. 18 | * 19 | * @since 0.1.0 20 | * 21 | * @package AlainSchlesser\Speaking\Assets 22 | * @author Alain Schlesser 23 | */ 24 | interface Asset extends Registerable { 25 | 26 | /** 27 | * Enqueue the asset. 28 | * 29 | * @since 0.1.0 30 | * 31 | * @return void 32 | */ 33 | public function enqueue(); 34 | 35 | /** 36 | * Dequeue the asset. 37 | * 38 | * @since 0.2.7 39 | * 40 | * @return void 41 | */ 42 | public function dequeue(); 43 | 44 | /** 45 | * Get the handle of the asset. 46 | * 47 | * @since 0.1.0 48 | * 49 | * @return string 50 | */ 51 | public function get_handle(); 52 | } 53 | -------------------------------------------------------------------------------- /src/Exception/InvalidAssetHandle.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Exception; 13 | 14 | /** 15 | * Class InvalidAssetHandle. 16 | * 17 | * @since 0.1.0 18 | * 19 | * @package AlainSchlesser\Speaking\Exception 20 | * @author Alain Schlesser 21 | */ 22 | class InvalidAssetHandle extends \InvalidArgumentException implements SpeakingPageException { 23 | 24 | /** 25 | * Create a new instance of the exception for a asset handle that is not 26 | * valid. 27 | * 28 | * @since 0.1.0 29 | * 30 | * @param int $handle Asset handle that is not valid. 31 | * 32 | * @return static 33 | */ 34 | public static function from_handle( $handle ) { 35 | $message = sprintf( 36 | 'The asset handle "%s" is not valid.', 37 | $handle 38 | ); 39 | 40 | return new static( $message ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Exception/InvalidURI.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Exception; 13 | 14 | /** 15 | * Class InvalidURI. 16 | * 17 | * @since 0.1.0 18 | * 19 | * @package AlainSchlesser\Speaking\Exception 20 | * @author Alain Schlesser 21 | */ 22 | class InvalidURI extends \InvalidArgumentException implements SpeakingPageException { 23 | 24 | /** 25 | * Create a new instance of the exception for a file that is not accessible 26 | * or not readable. 27 | * 28 | * @since 0.1.0 29 | * 30 | * @param string $uri URI of the file that is not accessible or not 31 | * readable. 32 | * 33 | * @return static 34 | */ 35 | public static function from_uri( $uri ) { 36 | $message = sprintf( 37 | 'The View URI "%s" is not accessible or readable.', 38 | $uri 39 | ); 40 | 41 | return new static( $message ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /views/speaking-page.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | use AlainSchlesser\Speaking\Model\Talk; 15 | 16 | $upcoming_header = false; 17 | $past_header = false; 18 | $timestamp = time(); 19 | 20 | ?>
21 | talks as $talk ) : /** @var Talk $talk */ ?> 22 | get_session_date() >= $timestamp ) { ?> 23 |

24 | 25 | get_session_date() < $timestamp ) { ?> 26 |

27 | 28 | 29 | render_partial( 'views/speaking-page-talk', [ 'talk' => $talk ] ) ?> 30 | 31 |
32 | -------------------------------------------------------------------------------- /src/Exception/InvalidService.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Exception; 13 | 14 | /** 15 | * Class InvalidService. 16 | * 17 | * @since 0.1.0 18 | * 19 | * @package AlainSchlesser\Speaking\Exception 20 | * @author Alain Schlesser 21 | */ 22 | class InvalidService extends \InvalidArgumentException implements SpeakingPageException { 23 | 24 | /** 25 | * Create a new instance of the exception for a service class name that is 26 | * not recognized. 27 | * 28 | * @since 0.1.0 29 | * 30 | * @param string $service Class name of the service that was not recognized. 31 | * 32 | * @return static 33 | */ 34 | public static function from_service( $service ) { 35 | $message = sprintf( 36 | 'The service "%s" is not recognized and cannot be registered.', 37 | is_object( $service ) 38 | ? get_class( $service ) 39 | : (string) $service 40 | ); 41 | 42 | return new static( $message ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /views/talks-widget.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | use AlainSchlesser\Speaking\Model\Talk; 15 | 16 | $upcoming_header = false; 17 | $past_header = false; 18 | $timestamp = time(); 19 | 20 | ?>

title ?>

21 |
22 | talks as $talk ) : /** @var Talk $talk */ ?> 23 | get_session_date() >= $timestamp ) { ?> 24 |
25 | 26 | get_session_date() < $timestamp ) { ?> 27 |
28 | 29 | 30 | render_partial( 'views/talks-widget-talk', [ 'talk' => $talk ] ) ?> 31 | 32 |
33 | -------------------------------------------------------------------------------- /as-speaking.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | * 11 | * @wordpress-plugin 12 | * Plugin Name: AlainSchlesser.com Speaking Page Plugin. 13 | * Plugin URI: https://www.alainschlesser.com/ 14 | * Description: Custom post type and presentation tools for a page to promote your speaking gigs. 15 | * Version: 0.2.13 16 | * Requires PHP: 5.6 17 | * Author: Alain Schlesser 18 | * Author URI: https://www.alainschlesser.com/ 19 | * Text Domain: as-speaking 20 | * Domain Path: /languages 21 | * License: MIT 22 | * License URI: https://opensource.org/licenses/MIT 23 | */ 24 | 25 | namespace AlainSchlesser\Speaking; 26 | 27 | // Make sure this file is only run from within WordPress. 28 | defined( 'ABSPATH' ) or die(); 29 | 30 | // Load Autoloader class and register plugin namespace. 31 | require_once __DIR__ . '/src/Autoloader.php'; 32 | ( new Autoloader() ) 33 | ->add_namespace( __NAMESPACE__, __DIR__ . '/src' ) 34 | ->register(); 35 | 36 | // Hook plugin into WordPress request lifecycle. 37 | PluginFactory::create() 38 | ->register(); 39 | -------------------------------------------------------------------------------- /src/Exception/FailedToLoadView.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Exception; 13 | 14 | /** 15 | * Class FailedToLoadView. 16 | * 17 | * @since 0.1.0 18 | * 19 | * @package AlainSchlesser\Speaking\Exception 20 | * @author Alain Schlesser 21 | */ 22 | class FailedToLoadView extends \RuntimeException implements SpeakingPageException { 23 | 24 | /** 25 | * Create a new instance of the exception if the view file itself created 26 | * an exception. 27 | * 28 | * @since 0.1.0 29 | * 30 | * @param string $uri URI of the file that is not accessible or 31 | * not readable. 32 | * @param \Exception $exception Exception that was thrown by the view file. 33 | * 34 | * @return static 35 | */ 36 | public static function view_exception( $uri, $exception ) { 37 | $message = sprintf( 38 | 'Could not load the View URI "%1$s". Reason: "%2$s".', 39 | $uri, 40 | $exception->getMessage() 41 | ); 42 | 43 | return new static( $message, $exception->getCode(), $exception ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/CustomPostType/BaseCustomPostType.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\CustomPostType; 13 | 14 | use AlainSchlesser\Speaking\Service; 15 | 16 | /** 17 | * Abstract class BaseCustomPostType. 18 | * 19 | * @since 0.1.0 20 | * 21 | * @package AlainSchlesser\Speaking 22 | * @author Alain Schlesser 23 | */ 24 | abstract class BaseCustomPostType implements Service { 25 | 26 | /** 27 | * Register the custom post type. 28 | * 29 | * @since 0.1.0 30 | */ 31 | public function register() { 32 | add_action( 'init', function () { 33 | register_post_type( $this->get_slug(), $this->get_arguments() ); 34 | } ); 35 | } 36 | 37 | /** 38 | * Get the slug to use for the custom post type. 39 | * 40 | * @since 0.1.0 41 | * 42 | * @return string Custom post type slug. 43 | */ 44 | abstract protected function get_slug(); 45 | 46 | /** 47 | * Get the arguments that configure the custom post type. 48 | * 49 | * @since 0.1.0 50 | * 51 | * @return array Array of arguments. 52 | */ 53 | abstract protected function get_arguments(); 54 | } 55 | -------------------------------------------------------------------------------- /views/talks-widget-talk.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | ?>
15 |
talk->get_title() ?>
16 | 30 |
31 | -------------------------------------------------------------------------------- /src/Model/TalkMeta.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Model; 13 | 14 | /** 15 | * Class TalkMeta. 16 | * 17 | * @since 0.2.0 18 | * 19 | * @package AlainSchlesser\Speaking 20 | * @author Alain Schlesser 21 | */ 22 | interface TalkMeta { 23 | 24 | const EVENT_NAME = 'event_name'; 25 | const EVENT_LINK = 'event_link'; 26 | const SESSION_DATE = 'session_date'; 27 | const SESSION_LINK = 'session_link'; 28 | const VIDEO = 'video'; 29 | const SLIDES = 'slides'; 30 | const IMAGE_LINK = 'image_link'; 31 | 32 | const PROPERTIES = [ 33 | self::EVENT_NAME, 34 | self::EVENT_LINK, 35 | self::SESSION_DATE, 36 | self::SESSION_LINK, 37 | self::VIDEO, 38 | self::SLIDES, 39 | self::IMAGE_LINK, 40 | ]; 41 | 42 | const IMAGE_LINK_NOTHING = 'nothing'; 43 | const IMAGE_LINK_EVENT = 'event'; 44 | const IMAGE_LINK_SESSION = 'session'; 45 | const IMAGE_LINK_VIDEO = 'video'; 46 | const IMAGE_LINK_SLIDES = 'slides'; 47 | 48 | const FORM_FIELD_PREFIX = 'talk_cpt_'; 49 | const FORM_FIELD_SESSION_AA = self::FORM_FIELD_PREFIX . 'session_aa'; 50 | const FORM_FIELD_SESSION_MM = self::FORM_FIELD_PREFIX . 'session_mm'; 51 | const FORM_FIELD_SESSION_JJ = self::FORM_FIELD_PREFIX . 'session_jj'; 52 | const META_PREFIX = 'talk_cpt_meta_'; 53 | } 54 | -------------------------------------------------------------------------------- /views/talk-metabox-video.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | $video_display = empty( $this->talk->get_video() ) 15 | ? '' 16 | : sprintf( 17 | '%s', 18 | $this->talk->get_video(), 19 | __( 'Link', 'as-speaking' ) 20 | ); 21 | 22 | ?>
23 | 24 | 25 | 32 |
33 | -------------------------------------------------------------------------------- /views/speaking-page-talk.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | ?>
15 | 16 |

talk->get_title() ?>

17 | 31 |
talk->get_content() ) ?>
32 |
33 | -------------------------------------------------------------------------------- /views/talk-metabox-slides.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | $slides_display = empty( $this->talk->get_slides() ) 15 | ? '' 16 | : sprintf( 17 | '%s', 18 | $this->talk->get_slides(), 19 | __( 'Link', 'as-speaking' ) 20 | ); 21 | 22 | ?>
23 | 24 | 25 | 32 |
33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Speaking Page Plugin 2 | 3 | > Custom post type and presentation tools for a page to promote your speaking gigs. 4 | 5 | The main reason why I developed this plugin is because I needed something to talk about at [WordCamp Nijmegen 2017](https://2017.nijmegen.wordcamp.org/session/oop-plugin-development-basics/). 6 | 7 | This plugin is currently active on my personal blog, and you can see the output on my [Speaking Page](https://www.alainschlesser.com/speaking/). 8 | 9 | ## Basic usage 10 | 11 | Adding and editing talks should be pretty straight-forward. 12 | 13 | To display them on the frontpage, you can use the `[speaking_page]` shortcode. 14 | 15 | The talks will automatically be ordered by date, and showed as either "Upcoming Talks" or "Past Talks". 16 | 17 | The plugin also provides an additional widget called "Latest Talks", that you just need to drag into a sidebar to use. 18 | 19 | ## Customizing 20 | 21 | If you want to override the markup fom within your theme, just create a `views` folder in your (parent or child) theme and copy the file from the plugin's `views` folder in there and copy the markup file(s) you need from the plugin's `views` folder. You can then change the copied file(s) as needed. 22 | 23 | ## Screenshots 24 | 25 | ### List view of the added talks 26 | 27 | ![List view of the added talks](/assets/images/screenshot-1.png) 28 | 29 | ### Metabox in talk edit screen 30 | 31 | ![Metabox in talk edit screen](/assets/images/screenshot-2.png) 32 | 33 | ### Expanded metabox showing all properties 34 | 35 | ![Expanded metabox showing all properties](/assets/images/screenshot-3.png) 36 | 37 | ### Frontend rendering 38 | 39 | ![Frontend rendering](/assets/images/screenshot-4.png) 40 | -------------------------------------------------------------------------------- /src/View/View.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\View; 13 | 14 | use AlainSchlesser\Speaking\Exception\FailedToLoadView; 15 | use AlainSchlesser\Speaking\Exception\InvalidURI; 16 | use AlainSchlesser\Speaking\Renderable; 17 | 18 | /** 19 | * Interface View. 20 | * 21 | * @since 0.2.4 22 | * 23 | * @package AlainSchlesser\Speaking 24 | * @author Alain Schlesser 25 | */ 26 | interface View extends Renderable { 27 | 28 | /** 29 | * Render a given URI. 30 | * 31 | * @since 0.2.4 32 | * 33 | * @param array $context Context in which to render. 34 | * 35 | * @return string Rendered HTML. 36 | * @throws FailedToLoadView If the View URI could not be loaded. 37 | */ 38 | public function render( array $context = [] ); 39 | 40 | /** 41 | * Render a partial view. 42 | * 43 | * This can be used from within a currently rendered view, to include 44 | * nested partials. 45 | * 46 | * The passed-in context is optional, and will fall back to the parent's 47 | * context if omitted. 48 | * 49 | * @since 0.2.4 50 | * 51 | * @param string $uri URI of the partial to render. 52 | * @param array|null $context Context in which to render the partial. 53 | * 54 | * @return string Rendered HTML. 55 | * @throws InvalidURI If the provided URI was not valid. 56 | * @throws FailedToLoadView If the view could not be loaded. 57 | */ 58 | public function render_partial( $uri, array $context = null ); 59 | } 60 | -------------------------------------------------------------------------------- /src/View/TemplatedView.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\View; 13 | 14 | use AlainSchlesser\Speaking\Exception\InvalidURI; 15 | 16 | /** 17 | * Class TemplatedView. 18 | * 19 | * Looks within the child theme and parent theme folders first for a view, 20 | * before defaulting to the plugin folder. 21 | * 22 | * @since 0.1.0 23 | * 24 | * @package AlainSchlesser\Speaking 25 | * @author Alain Schlesser 26 | */ 27 | final class TemplatedView extends BaseView { 28 | 29 | /** 30 | * Validate an URI. 31 | * 32 | * @since 0.1.0 33 | * 34 | * @param string $uri URI to validate. 35 | * 36 | * @return string Validated URI. 37 | * @throws InvalidURI If an invalid URI was passed into the View. 38 | */ 39 | protected function validate( $uri ) { 40 | $uri = $this->check_extension( $uri, static::VIEW_EXTENSION ); 41 | 42 | foreach ( $this->get_locations( $uri ) as $location ) { 43 | if ( is_readable( $location ) ) { 44 | return $location; 45 | } 46 | } 47 | 48 | if ( ! is_readable( $uri ) ) { 49 | throw InvalidURI::from_uri( $uri ); 50 | } 51 | 52 | return $uri; 53 | } 54 | 55 | /** 56 | * Get the possible locations for the view. 57 | * 58 | * @since 0.1.0 59 | * 60 | * @param string $uri URI of the view to get the locations for. 61 | * 62 | * @return array Array of possible locations. 63 | */ 64 | protected function get_locations( $uri ) { 65 | return [ 66 | trailingslashit( STYLESHEETPATH ) . $uri, 67 | trailingslashit( TEMPLATEPATH ) . $uri, 68 | trailingslashit( dirname( __DIR__, 2 ) ) . $uri, 69 | ]; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /views/talk-metabox-event.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | $event_display = empty( $this->talk->get_event_link() ) 15 | ? $this->talk->get_event_name() 16 | : "talk->get_event_link()}\">{$this->talk->get_event_name()}"; 17 | 18 | ?>
19 | 20 | 21 | 31 |
32 | -------------------------------------------------------------------------------- /src/Model/TalkRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Model; 13 | 14 | use AlainSchlesser\Speaking\CustomPostType\Talk as TalkCPT; 15 | use AlainSchlesser\Speaking\Exception\InvalidPostID; 16 | use WP_Query; 17 | 18 | /** 19 | * Class TalkRepository. 20 | * 21 | * @since 0.1.0 22 | * 23 | * @package AlainSchlesser\Speaking 24 | * @author Alain Schlesser 25 | */ 26 | final class TalkRepository extends CustomPostTypeRepository { 27 | 28 | /** 29 | * Find the Talk with a given post ID. 30 | * 31 | * @since 0.1.0 32 | * 33 | * @param int $id Post ID to retrieve. 34 | * 35 | * @return Talk 36 | * @throws InvalidPostID If the post for the requested ID was not found. 37 | */ 38 | public function find( $id ) { 39 | $post = get_post( $id ); 40 | if ( null === $post ) { 41 | throw InvalidPostID::from_id( $id ); 42 | } 43 | 44 | return new Talk( $post ); 45 | } 46 | 47 | /** 48 | * Find all the published Talks. 49 | * 50 | * @since 0.1.0 51 | * 52 | * @return array 53 | */ 54 | public function find_all() { 55 | return $this->find_latest( -1 ); 56 | } 57 | 58 | /** 59 | * Find the latest published Talks. 60 | * 61 | * @since 0.2.4 62 | 63 | * @param int $limit Maximum number of results to fetch. Defaults to 3. 64 | * 65 | * @return array 66 | */ 67 | public function find_latest( $limit = 3 ) { 68 | $args = [ 69 | 'post_type' => TalkCPT::SLUG, 70 | 'post_status' => 'publish', 71 | 'posts_per_page' => $limit, 72 | 'meta_key' => TalkMeta::META_PREFIX . 'session_date', 73 | 'orderby' => 'meta_value_num', 74 | 'order' => 'DESC', 75 | ]; 76 | $query = new WP_Query( $args ); 77 | 78 | $talks = []; 79 | foreach ( $query->posts as $post ) { 80 | $talks[ $post->ID ] = new Talk( $post ); 81 | } 82 | 83 | return $talks; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Assets/AssetsAwareness.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Assets; 13 | 14 | use AlainSchlesser\Speaking\Exception\InvalidAssetHandle; 15 | 16 | /** 17 | * Trait AssetsAwareness 18 | * 19 | * @since 0.1.0 20 | * 21 | * @package AlainSchlesser\Speaking 22 | * @author Alain Schlesser 23 | */ 24 | trait AssetsAwareness { 25 | 26 | /** 27 | * Assets handler instance to use. 28 | * 29 | * @since 0.1.0 30 | * 31 | * @var AssetsHandler 32 | */ 33 | protected $assets_handler; 34 | 35 | /** 36 | * Get the array of known assets. 37 | * 38 | * @since 0.1.0 39 | * 40 | * @return array 41 | */ 42 | protected function get_assets() { 43 | return []; 44 | } 45 | 46 | /** 47 | * Register the known assets. 48 | * 49 | * @since 0.1.0 50 | */ 51 | protected function register_assets() { 52 | foreach ( $this->get_assets() as $asset ) { 53 | $this->assets_handler->add( $asset ); 54 | } 55 | } 56 | 57 | /** 58 | * Enqueue the known assets. 59 | * 60 | * @since 0.1.0 61 | * 62 | * @throws InvalidAssetHandle If the passed-in asset handle is not valid. 63 | */ 64 | protected function enqueue_assets() { 65 | foreach ( $this->get_assets() as $asset ) { 66 | $this->assets_handler->enqueue( $asset ); 67 | } 68 | } 69 | 70 | /** 71 | * Enqueue a single asset. 72 | * 73 | * @since 0.1.0 74 | * 75 | * @param string $handle Handle of the asset to enqueue. 76 | * 77 | * @throws InvalidAssetHandle If the passed-in asset handle is not valid. 78 | */ 79 | protected function enqueue_asset( $handle ) { 80 | $this->assets_handler->enqueue_handle( $handle ); 81 | } 82 | 83 | /** 84 | * Set the assets handler to use within this object. 85 | * 86 | * @since 0.1.0 87 | * 88 | * @param AssetsHandler $assets Assets handler to use. 89 | */ 90 | public function with_assets_handler( AssetsHandler $assets ) { 91 | $this->assets_handler = $assets; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /assets/styles/as-speaking-backend.css: -------------------------------------------------------------------------------- 1 | /* 2 | * AlainSchlesser.com Speaking Page Plugin. 3 | * 4 | * @package AlainSchlesser\Speaking 5 | * @author Alain Schlesser 6 | * @license MIT 7 | * @link https://www.alainschlesser.com/ 8 | * @copyright 2017 Alain Schlesser 9 | */ 10 | 11 | .talk-cpt-metabox-section { 12 | padding: 6px 0 8px; 13 | } 14 | 15 | .talk-cpt-metabox-section input[type="text"] { 16 | width: 100%; 17 | } 18 | 19 | .talk-cpt-metabox-section .button-edit:before, 20 | .talk-cpt-metabox-section-event:before, 21 | .talk-cpt-metabox-section-session:before, 22 | .talk-cpt-metabox-section-video:before, 23 | .talk-cpt-metabox-section-slides:before, 24 | .talk-cpt-metabox-section-featured-image:before { 25 | color: #82878c; 26 | font: normal 20px/1 dashicons; 27 | speak: none; 28 | display: inline-block; 29 | margin-left: -1px; 30 | padding-right: 3px; 31 | vertical-align: top; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-osx-font-smoothing: grayscale; 34 | } 35 | 36 | .talk-cpt-metabox-section .button-edit { 37 | font-size: 0; 38 | } 39 | 40 | .talk-cpt-metabox-section .button-edit:before { 41 | color: inherit; 42 | content: "\f464"; 43 | } 44 | 45 | .talk-cpt-metabox-section-event:before { 46 | content: "\f319"; 47 | } 48 | 49 | .talk-cpt-metabox-section-session:before { 50 | content: "\f145"; 51 | } 52 | 53 | .talk-cpt-metabox-section-video:before { 54 | content: "\f490"; 55 | } 56 | 57 | .talk-cpt-metabox-section-slides:before { 58 | content: "\f496"; 59 | } 60 | 61 | .talk-cpt-metabox-section-featured-image:before { 62 | content: "\f128"; 63 | } 64 | #talk-cpt-event-display, 65 | #talk-cpt-session-display, 66 | #talk-cpt-video-display, 67 | #talk-cpt-slides-display, 68 | #talk-cpt-image-link-display { 69 | font-weight: 600; 70 | } 71 | 72 | #talk_cpt_session_aa, 73 | #talk_cpt_session_jj { 74 | padding: 1px; 75 | font-size: 12px; 76 | } 77 | 78 | #talk_cpt_session_jj { 79 | width: 2em; 80 | } 81 | 82 | #talk_cpt_session_aa { 83 | width: 3.4em; 84 | } 85 | 86 | #talk_cpt_session_mm { 87 | height: 21px; 88 | line-height: 14px; 89 | padding: 0; 90 | vertical-align: top; 91 | font-size: 12px; 92 | } 93 | -------------------------------------------------------------------------------- /views/talk-metabox-image-link.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | use AlainSchlesser\Speaking\Model\TalkMeta; 15 | 16 | $image_link_captions = [ 17 | TalkMeta::IMAGE_LINK_NOTHING => __( 'Nothing', 'as-speaking' ), 18 | TalkMeta::IMAGE_LINK_EVENT => __( 'Event', 'as-speaking' ), 19 | TalkMeta::IMAGE_LINK_SESSION => __( 'Session', 'as-speaking' ), 20 | TalkMeta::IMAGE_LINK_VIDEO => __( 'Video', 'as-speaking' ), 21 | TalkMeta::IMAGE_LINK_SLIDES => __( 'Slides', 'as-speaking' ), 22 | ]; 23 | 24 | ?> 39 | 40 | -------------------------------------------------------------------------------- /src/View/PostEscapedView.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\View; 13 | 14 | use AlainSchlesser\Speaking\Exception\FailedToLoadView; 15 | use AlainSchlesser\Speaking\Exception\InvalidURI; 16 | 17 | /** 18 | * Class PostEscapedView. 19 | * 20 | * This is a Decorator that decorates a given View with escaping meant for 21 | * standard HTML post output. 22 | * 23 | * @since 0.2.4 24 | * 25 | * @package AlainSchlesser\Speaking\View 26 | * @author Alain Schlesser 27 | */ 28 | final class PostEscapedView implements View { 29 | 30 | /** 31 | * View instance to decorate. 32 | * 33 | * @since 0.2.4 34 | * 35 | * @var View 36 | */ 37 | private $view; 38 | 39 | /** 40 | * Instantiate a PostEscapedView object. 41 | * 42 | * @since 0.2.4 43 | * 44 | * @param View $view View instance to decorate. 45 | */ 46 | public function __construct( View $view ) { 47 | $this->view = $view; 48 | } 49 | 50 | /** 51 | * Render a given URI. 52 | * 53 | * @since 0.2.4 54 | * 55 | * @param array $context Context in which to render. 56 | * 57 | * @return string Rendered HTML. 58 | * @throws FailedToLoadView If the View URI could not be loaded. 59 | */ 60 | public function render( array $context = [] ) { 61 | return wp_kses_post( $this->view->render( $context ) ); 62 | } 63 | 64 | /** 65 | * Render a partial view. 66 | * 67 | * This can be used from within a currently rendered view, to include 68 | * nested partials. 69 | * 70 | * The passed-in context is optional, and will fall back to the parent's 71 | * context if omitted. 72 | * 73 | * @since 0.2.4 74 | * 75 | * @param string $uri URI of the partial to render. 76 | * @param array|null $context Context in which to render the partial. 77 | * 78 | * @return string Rendered HTML. 79 | * @throws InvalidURI If the provided URI was not valid. 80 | * @throws FailedToLoadView If the view could not be loaded. 81 | */ 82 | public function render_partial( $uri, array $context = null ) { 83 | return wp_kses_post( $this->view->render_partial( $uri, $context ) ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Assets/AssetsHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Assets; 13 | 14 | use AlainSchlesser\Speaking\Exception\InvalidAssetHandle; 15 | use AlainSchlesser\Speaking\Registerable; 16 | 17 | /** 18 | * Class AssetsHandler. 19 | * 20 | * @since 0.1.0 21 | * 22 | * @package AlainSchlesser\Speaking 23 | * @author Alain Schlesser 24 | */ 25 | final class AssetsHandler implements Registerable { 26 | 27 | /** 28 | * Assets known to this asset handler. 29 | * 30 | * @since 0.1.0 31 | * 32 | * @var array 33 | */ 34 | private $assets; 35 | 36 | /** 37 | * Add a single asset to the asset handler. 38 | * 39 | * @since 0.1.0 40 | * 41 | * @param Asset $asset Asset to add. 42 | */ 43 | public function add( Asset $asset ) { 44 | $this->assets[ $asset->get_handle() ] = $asset; 45 | } 46 | 47 | /** 48 | * Register the current Registerable. 49 | * 50 | * @since 0.1.0 51 | * 52 | * @return void 53 | */ 54 | public function register() { 55 | foreach ( $this->assets as $asset ) { 56 | $asset->register(); 57 | } 58 | } 59 | 60 | /** 61 | * Enqueue a single asset based on its handle. 62 | * 63 | * @since 0.1.0 64 | * 65 | * @param string $handle Handle of the asset to enqueue. 66 | * 67 | * @throws InvalidAssetHandle If the passed-in asset handle is not valid. 68 | */ 69 | public function enqueue_handle( $handle ) { 70 | if ( ! array_key_exists( $handle, $this->assets ) ) { 71 | throw InvalidAssetHandle::from_handle( $handle ); 72 | } 73 | $this->assets[ $handle ]->enqueue(); 74 | } 75 | 76 | /** 77 | * Dequeue a single asset based on its handle. 78 | * 79 | * @since 0.2.7 80 | * 81 | * @param string $handle Handle of the asset to enqueue. 82 | * 83 | * @throws InvalidAssetHandle If the passed-in asset handle is not valid. 84 | */ 85 | public function dequeue_handle( $handle ) { 86 | if ( ! array_key_exists( $handle, $this->assets ) ) { 87 | throw InvalidAssetHandle::from_handle( $handle ); 88 | } 89 | $this->assets[ $handle ]->dequeue(); 90 | } 91 | 92 | /** 93 | * Enqueue all assets known to this asset handler. 94 | * 95 | * @since 0.1.0 96 | * 97 | * @param Asset|null $asset Optional. Asset to enqueue. If omitted, all 98 | * known assets are enqueued. 99 | */ 100 | public function enqueue( Asset $asset = null ) { 101 | $assets = $asset ? [ $asset ] : $this->assets; 102 | foreach ( $assets as $asset_object ) { 103 | $asset_object->enqueue(); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Shortcode/SpeakingPage.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Shortcode; 13 | 14 | use AlainSchlesser\Speaking\Assets\StyleAsset; 15 | use AlainSchlesser\Speaking\Model\Talk; 16 | use AlainSchlesser\Speaking\Model\TalkRepository; 17 | use DateTime; 18 | 19 | /** 20 | * Class SpeakingPage. 21 | * 22 | * @since 0.1.0 23 | * 24 | * @package AlainSchlesser\Speaking 25 | * @author Alain Schlesser 26 | */ 27 | final class SpeakingPage extends BaseShortcode { 28 | 29 | const TAG = 'speaking_page'; 30 | const VIEW_URI = 'views/speaking-page'; 31 | 32 | const CSS_HANDLE = 'as-speaking-frontend-css'; 33 | const CSS_URI = 'assets/styles/as-speaking-frontend'; 34 | 35 | /** 36 | * Get the tag to use for the shortcode. 37 | * 38 | * @since 0.1.0 39 | */ 40 | protected function get_tag() { 41 | return self::TAG; 42 | } 43 | 44 | /** 45 | * Get the View URI to use for rendering the shortcode. 46 | * 47 | * @since 0.1.0 48 | */ 49 | protected function get_view_uri() { 50 | return self::VIEW_URI; 51 | } 52 | 53 | /** 54 | * Get the context to pass onto the view. 55 | * 56 | * Override to provide data to the view that is not part of the shortcode 57 | * attributes. 58 | * 59 | * @since 0.1.0 60 | * 61 | * @param array $atts Array of shortcode attributes. 62 | * 63 | * @return array Context to pass onto view. 64 | */ 65 | protected function get_context( $atts ) { 66 | $talk_repository = new TalkRepository(); 67 | 68 | $talks = $talk_repository->find_all(); 69 | 70 | return [ 71 | 'talks' => $this->sort_talks( $talks ), 72 | ]; 73 | } 74 | 75 | /** 76 | * Get the array of known assets. 77 | * 78 | * @since 0.1.0 79 | * 80 | * @return array 81 | */ 82 | protected function get_assets() { 83 | return [ 84 | new StyleAsset( 85 | self::CSS_HANDLE, 86 | self::CSS_URI 87 | ), 88 | ]; 89 | } 90 | 91 | /** 92 | * Sort the array of talks for display. 93 | * 94 | * @since 0.2.12 95 | * 96 | * @param array $talks Array of talks to sort. 97 | * 98 | * @return array Sorted array of talks. 99 | */ 100 | protected function sort_talks( $talks ) { 101 | $now = ( new DateTime() )->getTimestamp(); 102 | 103 | // We need a custom sort function to reverse the sort order for 104 | // upcoming talks (soonest first) as opposed to past talks. 105 | usort( 106 | $talks, 107 | function ( Talk $talk_a, Talk $talk_b ) use ( $now ) { 108 | $timestamp_a = $talk_a->get_session_date(); 109 | $timestamp_b = $talk_b->get_session_date(); 110 | 111 | if ( $timestamp_a >= $now ) { 112 | // A is upcoming. 113 | return $timestamp_b >= $now 114 | // B is also upcoming, ascend natural number. 115 | ? strnatcmp( $timestamp_a, $timestamp_b ) 116 | // B is past. 117 | : - 1; 118 | } 119 | 120 | // A is past. 121 | return $timestamp_b >= $now 122 | // B is upcoming. 123 | ? 1 124 | // B is also past, descend on natural number. 125 | : strnatcmp( $timestamp_b, $timestamp_a ); 126 | } 127 | ); 128 | 129 | return $talks; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Assets/StyleAsset.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Assets; 13 | 14 | use Closure; 15 | 16 | /** 17 | * Class StyleAsset. 18 | * 19 | * @since 0.1.0 20 | * 21 | * @package AlainSchlesser\Speaking\Assets 22 | * @author Alain Schlesser 23 | */ 24 | class StyleAsset extends BaseAsset { 25 | 26 | const MEDIA_ALL = 'all'; 27 | const MEDIA_PRINT = 'print'; 28 | const MEDIA_SCREEN = 'screen'; 29 | 30 | const DEFAULT_EXTENSION = 'css'; 31 | 32 | /** 33 | * Source location of the asset. 34 | * 35 | * @since 0.1.0 36 | * 37 | * @var string 38 | */ 39 | protected $source; 40 | 41 | /** 42 | * Dependencies of the asset. 43 | * 44 | * @since 0.1.0 45 | * 46 | * @var array 47 | */ 48 | protected $dependencies; 49 | 50 | /** 51 | * Version of the asset. 52 | * 53 | * @since 0.1.0 54 | * 55 | * @var string|bool|null 56 | */ 57 | protected $version; 58 | 59 | /** 60 | * Media for which the asset is defined. 61 | * 62 | * @since 0.1.0 63 | * 64 | * @var string 65 | */ 66 | protected $media; 67 | 68 | /** 69 | * Instantiate a StyleAsset object. 70 | * 71 | * @since 0.1.0 72 | * 73 | * @param string $handle Handle of the asset. 74 | * @param string $source Source location of the asset. 75 | * @param array $dependencies Optional. Dependencies of the 76 | * asset. 77 | * @param string|bool|null $version Optional. Version of the asset. 78 | * @param string $media Media for which the asset is 79 | * defined. 80 | */ 81 | public function __construct( 82 | $handle, 83 | $source, 84 | $dependencies = [], 85 | $version = false, 86 | $media = self::MEDIA_ALL 87 | ) { 88 | $this->handle = $handle; 89 | $this->source = $this->normalize_source( 90 | $source, 91 | static::DEFAULT_EXTENSION 92 | ); 93 | $this->dependencies = (array) $dependencies; 94 | $this->version = $version; 95 | $this->media = $media; 96 | } 97 | 98 | /** 99 | * Get the enqueue closure to use. 100 | * 101 | * @since 0.1.0 102 | * 103 | * @return Closure 104 | */ 105 | protected function get_register_closure() { 106 | return function () { 107 | if ( wp_script_is( $this->handle, 'registered' ) ) { 108 | return; 109 | } 110 | 111 | wp_register_style( 112 | $this->handle, 113 | $this->source, 114 | $this->dependencies, 115 | $this->version, 116 | $this->media 117 | ); 118 | }; 119 | } 120 | 121 | /** 122 | * Get the enqueue closure to use. 123 | * 124 | * @since 0.1.0 125 | * 126 | * @return Closure 127 | */ 128 | protected function get_enqueue_closure() { 129 | return function () { 130 | wp_enqueue_style( $this->handle ); 131 | }; 132 | } 133 | 134 | /** 135 | * Get the dequeue closure to use. 136 | * 137 | * @since 0.2.7 138 | * 139 | * @return Closure 140 | */ 141 | protected function get_dequeue_closure() { 142 | return function () { 143 | wp_dequeue_style( $this->handle ); 144 | }; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | use AlainSchlesser\Speaking\Assets\AssetsAware; 15 | use AlainSchlesser\Speaking\Assets\AssetsHandler; 16 | 17 | /** 18 | * Class Plugin. 19 | * 20 | * Main plugin controller class that hooks the plugin's functionality into the 21 | * WordPress request lifecycle. 22 | * 23 | * @since 0.1.0 24 | * 25 | * @package AlainSchlesser\Speaking 26 | * @author Alain Schlesser 27 | */ 28 | final class Plugin implements Registerable { 29 | 30 | /** 31 | * Assets handler instance. 32 | * 33 | * @since 0.1.0 34 | * 35 | * @var AssetsHandler 36 | */ 37 | private $assets_handler; 38 | 39 | /** 40 | * Instantiate a Plugin object. 41 | * 42 | * @since 0.2.7 43 | * 44 | * @param AssetsHandler|null $assets_handler Optional. Instance of the 45 | * assets handler to use. 46 | */ 47 | public function __construct( AssetsHandler $assets_handler = null ) { 48 | $this->assets_handler = $assets_handler ?: new AssetsHandler(); 49 | } 50 | 51 | /** 52 | * Register the plugin with the WordPress system. 53 | * 54 | * @since 0.1.0 55 | * 56 | * @throws Exception\InvalidService If a service is not valid. 57 | */ 58 | public function register() { 59 | add_action( 'plugins_loaded', [ $this, 'register_services' ] ); 60 | add_action( 'init', [ $this, 'register_assets_handler' ] ); 61 | } 62 | 63 | /** 64 | * Register the individual services of this plugin. 65 | * 66 | * @since 0.1.0 67 | * 68 | * @throws Exception\InvalidService If a service is not valid. 69 | */ 70 | public function register_services() { 71 | $services = $this->get_services(); 72 | $services = array_map( [ $this, 'instantiate_service' ], $services ); 73 | array_walk( $services, function ( Service $service ) { 74 | $service->register(); 75 | } ); 76 | } 77 | 78 | /** 79 | * Register the assets handler. 80 | * 81 | * @since 0.1.0 82 | */ 83 | public function register_assets_handler() { 84 | $this->assets_handler->register(); 85 | } 86 | 87 | /** 88 | * Return the instance of the assets handler in use. 89 | * 90 | * @since 0.2.7 91 | * 92 | * @return AssetsHandler 93 | */ 94 | public function get_assets_handler() { 95 | return $this->assets_handler; 96 | } 97 | 98 | /** 99 | * Instantiate a single service. 100 | * 101 | * @since 0.1.0 102 | * 103 | * @param string $class Service class to instantiate. 104 | * 105 | * @return Service 106 | * @throws Exception\InvalidService If the service is not valid. 107 | */ 108 | private function instantiate_service( $class ) { 109 | if ( ! class_exists( $class ) ) { 110 | throw Exception\InvalidService::from_service( $class ); 111 | } 112 | 113 | $service = new $class(); 114 | 115 | if ( ! $service instanceof Service ) { 116 | throw Exception\InvalidService::from_service( $service ); 117 | } 118 | 119 | if ( $service instanceof AssetsAware ) { 120 | $service->with_assets_handler( $this->assets_handler ); 121 | } 122 | 123 | return $service; 124 | } 125 | 126 | /** 127 | * Get the list of services to register. 128 | * 129 | * @since 0.1.0 130 | * 131 | * @return array Array of fully qualified class names. 132 | */ 133 | private function get_services() { 134 | return [ 135 | Shortcode\SpeakingPage::class, 136 | CustomPostType\Talk::class, 137 | Metabox\Talk::class, 138 | Widget\Talks::class, 139 | ]; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Shortcode/BaseShortcode.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Shortcode; 13 | 14 | use AlainSchlesser\Speaking\Assets\AssetsAware; 15 | use AlainSchlesser\Speaking\Assets\AssetsAwareness; 16 | use AlainSchlesser\Speaking\Renderable; 17 | use AlainSchlesser\Speaking\Service; 18 | use AlainSchlesser\Speaking\View\PostEscapedView; 19 | use AlainSchlesser\Speaking\View\TemplatedView; 20 | 21 | /** 22 | * Abstract class BaseShortcode. 23 | * 24 | * @since 0.1.0 25 | * 26 | * @package AlainSchlesser\Speaking 27 | * @author Alain Schlesser 28 | */ 29 | abstract class BaseShortcode implements Renderable, AssetsAware, Service { 30 | 31 | use AssetsAwareness; 32 | 33 | /** 34 | * Register the Shortcode. 35 | * 36 | * @since 0.1.0 37 | */ 38 | public function register() { 39 | $this->register_assets(); 40 | 41 | add_action( 'init', function () { 42 | add_shortcode( $this->get_tag(), [ $this, 'process_shortcode' ] ); 43 | } ); 44 | } 45 | 46 | /** 47 | * Process the shortcode attributes and prepare rendering. 48 | * 49 | * @since 0.1.0 50 | * 51 | * @param array|string $atts Attributes as passed to the shortcode. 52 | * 53 | * @return string Rendered HTML of the shortcode. 54 | */ 55 | public function process_shortcode( $atts ) { 56 | $atts = $this->process_attributes( $atts ); 57 | $context = $this->get_context( $atts ); 58 | 59 | return $this->render( array_merge( $atts, $context ) ); 60 | } 61 | 62 | /** 63 | * Render the current Renderable. 64 | * 65 | * @since 0.1.0 66 | * 67 | * @param array $context Context in which to render. 68 | * 69 | * @return string Rendered HTML. 70 | */ 71 | public function render( array $context = [] ) { 72 | try { 73 | $this->enqueue_assets(); 74 | 75 | $view = new PostEscapedView( 76 | new TemplatedView( $this->get_view_uri() ) 77 | ); 78 | 79 | return $view->render( $context ); 80 | } catch ( \Exception $exception ) { 81 | // Don't let exceptions bubble up. Just render an empty shortcode 82 | // instead. 83 | return ''; 84 | } 85 | } 86 | 87 | /** 88 | * Process the shortcode attributes. 89 | * 90 | * Override to add accepted attributes and their default values. 91 | * 92 | * @since 0.1.0 93 | * 94 | * @param array|string $atts Raw shortcode attributes passed into the 95 | * shortcode function. 96 | * 97 | * @return array Processed shortcode attributes. 98 | */ 99 | protected function process_attributes( $atts ) { 100 | return shortcode_atts( 101 | [ 102 | // Shortcode attributes' default values. 103 | ], 104 | $atts, 105 | $this->get_tag() 106 | ); 107 | } 108 | 109 | /** 110 | * Get the context to pass onto the view. 111 | * 112 | * Override to provide data to the view that is not part of the shortcode 113 | * attributes. 114 | * 115 | * @since 0.1.0 116 | * 117 | * @param array $atts Array of shortcode attributes. 118 | * 119 | * @return array Context to pass onto view. 120 | */ 121 | protected function get_context( $atts ) { 122 | return []; 123 | } 124 | 125 | /** 126 | * Get the tag to use for the shortcode. 127 | * 128 | * @since 0.1.0 129 | * 130 | * @return string Tag of the shortcode. 131 | */ 132 | abstract protected function get_tag(); 133 | 134 | /** 135 | * Get the View URI to use for rendering the shortcode. 136 | * 137 | * @since 0.1.0 138 | * 139 | * @return string View URI. 140 | */ 141 | abstract protected function get_view_uri(); 142 | } 143 | -------------------------------------------------------------------------------- /views/talk-metabox-session.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | use DateTime; 15 | 16 | global $wp_locale; 17 | 18 | $session_date = $this->talk->get_session_date(); 19 | $session_date_time = new DateTime(); 20 | $session_date_time->setTimestamp( $session_date ); 21 | $session_date_string = date_i18n( get_option( 'date_format' ), $session_date ); 22 | 23 | $session_display = empty( $this->talk->get_session_link() ) 24 | ? $session_date_string 25 | : "talk->get_session_link()}\">{$session_date_string}"; 26 | 27 | $jj = $session_date_time->format( 'd' ); 28 | $mm = $session_date_time->format( 'm' ); 29 | $aa = $session_date_time->format( 'Y' ); 30 | 31 | $month = ''; 40 | 41 | $day = ''; 42 | 43 | $year = ''; 44 | 45 | $sessions_date_fields = '
'; 46 | $sessions_date_fields .= sprintf( 47 | /* translators: 1: month, 2: day, 3: year */ 48 | __( '%1$s %2$s, %3$s' ), 49 | $month, 50 | $day, 51 | $year 52 | ); 53 | 54 | $map = array( 55 | 'talk_cpt_session_mm' => $mm, 56 | 'talk_cpt_session_jj' => $jj, 57 | 'talk_cpt_session_aa' => $aa, 58 | ); 59 | 60 | foreach ( $map as $timeunit => $value ) { 61 | $sessions_date_fields .= ''; 62 | } 63 | $sessions_date_fields .= '
'; 64 | 65 | ?>
66 | Edit session date and link 67 | 77 |
78 | -------------------------------------------------------------------------------- /src/Model/CustomPostTypeEntity.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Model; 13 | 14 | use WP_Post; 15 | 16 | /** 17 | * Abstract class CustomPostTypeEntity. 18 | * 19 | * @since 0.2.1 20 | * 21 | * @package AlainSchlesser\Speaking 22 | * @author Alain Schlesser 23 | */ 24 | abstract class CustomPostTypeEntity implements Entity { 25 | 26 | /** 27 | * WordPress post data representing the post. 28 | * 29 | * @since 0.2.1 30 | * 31 | * @var WP_Post 32 | */ 33 | protected $post; 34 | 35 | /** 36 | * Instantiate a CustomPostTypeEntity object. 37 | * 38 | * @since 0.2.1 39 | * 40 | * @param WP_Post $post Post object to instantiate a CustomPostTypeEntity model from. 41 | */ 42 | public function __construct( WP_Post $post ) { 43 | $this->post = $post; 44 | } 45 | 46 | /** 47 | * Return the entity ID. 48 | * 49 | * @since 0.2.1 50 | * 51 | * @return int Entity ID. 52 | */ 53 | public function get_ID() { 54 | return $this->post->ID; 55 | } 56 | 57 | /** 58 | * Return the WP_Post object that represents this model. 59 | * 60 | * @since 0.2.1 61 | * 62 | * @return WP_Post WP_Post object representing this model. 63 | */ 64 | public function get_post_object() { 65 | return $this->post; 66 | } 67 | 68 | /** 69 | * Get the post's title. 70 | * 71 | * @since 0.2.1 72 | * 73 | * @return string Title of the post. 74 | */ 75 | public function get_title() { 76 | return $this->post->post_title; 77 | } 78 | 79 | /** 80 | * Set the post's title. 81 | * 82 | * @since 0.2.1 83 | * 84 | * @param string $title New title of the post. 85 | */ 86 | public function set_title( $title ) { 87 | $this->post->post_title = $title; 88 | } 89 | 90 | /** 91 | * Get the post's content. 92 | * 93 | * @since 0.2.1 94 | * 95 | * @return string Content of the post. 96 | */ 97 | public function get_content() { 98 | return $this->post->post_content; 99 | } 100 | 101 | /** 102 | * Set the post's content. 103 | * 104 | * @since 0.2.1 105 | * 106 | * @param string $content New content of the post. 107 | */ 108 | public function set_content( $content ) { 109 | $this->post->post_content = $content; 110 | } 111 | 112 | /** 113 | * Magic getter method to fetch meta properties only when requested. 114 | * 115 | * @since 0.2.1 116 | * 117 | * @param string $property Property that was requested. 118 | * 119 | * @return mixed 120 | */ 121 | public function __get( $property ) { 122 | if ( array_key_exists( $property, $this->get_lazy_properties() ) ) { 123 | $this->load_lazy_property( $property ); 124 | 125 | return $this->$property; 126 | } 127 | 128 | $message = sprintf( 129 | 'Undefined property: %s::$%s', 130 | static::class, 131 | $property 132 | ); 133 | 134 | trigger_error( $message, E_USER_NOTICE ); 135 | 136 | return null; 137 | } 138 | 139 | /** 140 | * Persist the additional properties of the entity. 141 | * 142 | * @since 0.2.1 143 | * 144 | * @return void 145 | */ 146 | abstract public function persist_properties(); 147 | 148 | /** 149 | * Return the list of lazily-loaded properties and their default values. 150 | * 151 | * @since 0.2.1 152 | * 153 | * @return array 154 | */ 155 | abstract protected function get_lazy_properties(); 156 | 157 | /** 158 | * Load a lazily-loaded property. 159 | * 160 | * After this process, the loaded property should be set within the 161 | * object's state, otherwise the load procedure might be triggered multiple 162 | * times. 163 | * 164 | * @since 0.2.1 165 | * 166 | * @param string $property Name of the property to load. 167 | * 168 | * @return void 169 | */ 170 | abstract protected function load_lazy_property( $property ); 171 | } 172 | -------------------------------------------------------------------------------- /src/Assets/ScriptAsset.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Assets; 13 | 14 | use Closure; 15 | 16 | /** 17 | * Class ScriptAsset. 18 | * 19 | * @since 0.1.0 20 | * 21 | * @package AlainSchlesser\Speaking\Assets 22 | * @author Alain Schlesser 23 | */ 24 | class ScriptAsset extends BaseAsset { 25 | 26 | const ENQUEUE_HEADER = false; 27 | const ENQUEUE_FOOTER = true; 28 | 29 | const DEFAULT_EXTENSION = 'js'; 30 | 31 | /** 32 | * Source location of the asset. 33 | * 34 | * @since 0.1.0 35 | * 36 | * @var string 37 | */ 38 | protected $source; 39 | 40 | /** 41 | * Dependencies of the asset. 42 | * 43 | * @since 0.1.0 44 | * 45 | * @var array 46 | */ 47 | protected $dependencies; 48 | 49 | /** 50 | * Version of the asset. 51 | * 52 | * @since 0.1.0 53 | * 54 | * @var string|bool|null 55 | */ 56 | protected $version; 57 | 58 | /** 59 | * Whether to enqueue the script in the footer. 60 | * 61 | * @since 0.1.0 62 | * 63 | * @var bool 64 | */ 65 | protected $in_footer; 66 | 67 | /** 68 | * Localization data that is added to the JS space. 69 | * 70 | * @since 0.1.0 71 | * 72 | * @var array 73 | */ 74 | protected $localizations = []; 75 | 76 | /** 77 | * Instantiate a ScriptAsset object. 78 | * 79 | * @since 0.1.0 80 | * 81 | * @param string $handle Handle of the asset. 82 | * @param string $source Source location of the asset. 83 | * @param array $dependencies Optional. Dependencies of the 84 | * asset. 85 | * @param string|bool|null $version Optional. Version of the asset. 86 | * @param bool $in_footer Whether to enqueue the asset in 87 | * the footer. 88 | */ 89 | public function __construct( 90 | $handle, 91 | $source, 92 | $dependencies = [], 93 | $version = false, 94 | $in_footer = self::ENQUEUE_HEADER 95 | ) { 96 | $this->handle = $handle; 97 | $this->source = $this->normalize_source( 98 | $source, 99 | static::DEFAULT_EXTENSION 100 | ); 101 | $this->dependencies = (array) $dependencies; 102 | $this->version = $version; 103 | $this->in_footer = $in_footer; 104 | } 105 | 106 | /** 107 | * Add a localization to the script. 108 | * 109 | * @since 0.1.0 110 | * 111 | * @param string $object_name Name of the object to create in JS space. 112 | * @param array $data_array Array of data to attach to the object. 113 | * 114 | * @return static 115 | */ 116 | public function add_localization( $object_name, $data_array ) { 117 | $this->localizations[ $object_name ] = $data_array; 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * Get the enqueue closure to use. 124 | * 125 | * @since 0.1.0 126 | * 127 | * @return Closure 128 | */ 129 | protected function get_register_closure() { 130 | return function () { 131 | if ( wp_script_is( $this->handle, 'registered' ) ) { 132 | return; 133 | } 134 | 135 | wp_register_script( 136 | $this->handle, 137 | $this->source, 138 | $this->dependencies, 139 | $this->version, 140 | $this->in_footer 141 | ); 142 | 143 | foreach ( $this->localizations as $object_name => $data_array ) { 144 | wp_localize_script( $this->handle, $object_name, $data_array ); 145 | } 146 | }; 147 | } 148 | 149 | /** 150 | * Get the enqueue closure to use. 151 | * 152 | * @since 0.1.0 153 | * 154 | * @return Closure 155 | */ 156 | protected function get_enqueue_closure() { 157 | return function () { 158 | wp_enqueue_script( $this->handle ); 159 | }; 160 | } 161 | 162 | /** 163 | * Get the dequeue closure to use. 164 | * 165 | * @since 0.2.7 166 | * 167 | * @return Closure 168 | */ 169 | protected function get_dequeue_closure() { 170 | return function () { 171 | wp_dequeue_script( $this->handle ); 172 | }; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/CustomPostType/Talk.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\CustomPostType; 13 | 14 | /** 15 | * Class Talk. 16 | * 17 | * @since 0.1.0 18 | * 19 | * @package AlainSchlesser\Speaking 20 | * @author Alain Schlesser 21 | */ 22 | final class Talk extends BaseCustomPostType { 23 | 24 | const SLUG = 'talk'; 25 | 26 | /** 27 | * Get the slug to use for the custom post type. 28 | * 29 | * @since 0.1.0 30 | * 31 | * @return string Custom post type slug. 32 | */ 33 | protected function get_slug() { 34 | return self::SLUG; 35 | } 36 | 37 | /** 38 | * Get the arguments that configure the custom post type. 39 | * 40 | * @since 0.1.0 41 | * 42 | * @return array Array of arguments. 43 | */ 44 | protected function get_arguments() { 45 | return [ 46 | 'label' => __( 'Talk', 'as-speaking' ), 47 | 'description' => __( 'Talks for the Speaking page', 'as-speaking' ), 48 | 'labels' => $this->get_labels(), 49 | 'supports' => array( 50 | 'title', 51 | 'editor', 52 | 'author', 53 | 'thumbnail', 54 | 'custom-fields', 55 | ), 56 | 'taxonomies' => array( 'category', 'post_tag' ), 57 | 'hierarchical' => false, 58 | 'public' => true, 59 | 'show_ui' => true, 60 | 'show_in_menu' => true, 61 | 'menu_position' => 5, 62 | 'menu_icon' => 'dashicons-media-interactive', 63 | 'show_in_admin_bar' => true, 64 | 'show_in_nav_menus' => false, 65 | 'can_export' => true, 66 | 'has_archive' => false, 67 | 'exclude_from_search' => false, 68 | 'publicly_queryable' => true, 69 | 'capability_type' => 'page', 70 | 'show_in_rest' => true, 71 | ]; 72 | } 73 | 74 | /** 75 | * Get the localized labels for the custom post type UI. 76 | * 77 | * @since 0.1.0 78 | * 79 | * @return array Associative array of localized strings. 80 | */ 81 | private function get_labels() { 82 | return [ 83 | 'name' => _x( 'Talks', 'Post Type General Name', 'as-speaking' ), 84 | 'singular_name' => _x( 'Talk', 'Post Type Singular Name', 'as-speaking' ), 85 | 'menu_name' => __( 'Talks', 'as-speaking' ), 86 | 'name_admin_bar' => __( 'Talk', 'as-speaking' ), 87 | 'archives' => __( 'Item Archives', 'as-speaking' ), 88 | 'attributes' => __( 'Item Attributes', 'as-speaking' ), 89 | 'parent_item_colon' => __( 'Parent Item:', 'as-speaking' ), 90 | 'all_items' => __( 'All Items', 'as-speaking' ), 91 | 'add_new_item' => __( 'Add New Item', 'as-speaking' ), 92 | 'add_new' => __( 'Add New', 'as-speaking' ), 93 | 'new_item' => __( 'New Item', 'as-speaking' ), 94 | 'edit_item' => __( 'Edit Item', 'as-speaking' ), 95 | 'update_item' => __( 'Update Item', 'as-speaking' ), 96 | 'view_item' => __( 'View Item', 'as-speaking' ), 97 | 'view_items' => __( 'View Items', 'as-speaking' ), 98 | 'search_items' => __( 'Search Item', 'as-speaking' ), 99 | 'not_found' => __( 'Not found', 'as-speaking' ), 100 | 'not_found_in_trash' => __( 'Not found in Trash', 'as-speaking' ), 101 | 'featured_image' => __( 'Featured Image', 'as-speaking' ), 102 | 'set_featured_image' => __( 'Set featured image', 'as-speaking' ), 103 | 'remove_featured_image' => __( 'Remove featured image', 'as-speaking' ), 104 | 'use_featured_image' => __( 'Use as featured image', 'as-speaking' ), 105 | 'insert_into_item' => __( 'Insert into item', 'as-speaking' ), 106 | 'uploaded_to_this_item' => __( 'Uploaded to this item', 'as-speaking' ), 107 | 'items_list' => __( 'Items list', 'as-speaking' ), 108 | 'items_list_navigation' => __( 'Items list navigation', 'as-speaking' ), 109 | 'filter_items_list' => __( 'Filter items list', 'as-speaking' ), 110 | ]; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/View/FormEscapedView.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\View; 13 | 14 | use AlainSchlesser\Speaking\Exception\FailedToLoadView; 15 | use AlainSchlesser\Speaking\Exception\InvalidURI; 16 | 17 | /** 18 | * Class FormEscapedView. 19 | * 20 | * This is a Decorator that decorates a given View with escaping meant for 21 | * HTML form output. 22 | * 23 | * @since 0.2.4 24 | * 25 | * @package AlainSchlesser\Speaking\View 26 | * @author Alain Schlesser 27 | */ 28 | final class FormEscapedView implements View { 29 | 30 | /** 31 | * Form tags that are allowed to be rendered. 32 | */ 33 | const FORM_TAGS = [ 34 | 'form' => [ 35 | 'id' => true, 36 | 'class' => true, 37 | 'action' => true, 38 | 'method' => true, 39 | ], 40 | 'input' => [ 41 | 'id' => true, 42 | 'class' => true, 43 | 'type' => true, 44 | 'name' => true, 45 | 'value' => true, 46 | ], 47 | 'select' => [ 48 | 'id' => true, 49 | 'class' => true, 50 | 'type' => true, 51 | 'name' => true, 52 | 'value' => true, 53 | ], 54 | 'option' => [ 55 | 'id' => true, 56 | 'class' => true, 57 | 'type' => true, 58 | 'name' => true, 59 | 'value' => true, 60 | 'selected' => true, 61 | ], 62 | 'label' => [ 63 | 'for' => true, 64 | ], 65 | ]; 66 | 67 | /** 68 | * View instance to decorate. 69 | * 70 | * @since 0.2.4 71 | * 72 | * @var View 73 | */ 74 | private $view; 75 | 76 | /** 77 | * Tags that are allowed to pass through the escaping function. 78 | * 79 | * @since 0.2.3 80 | * 81 | * @var array 82 | */ 83 | private $allowed_tags = []; 84 | 85 | /** 86 | * Instantiate a FormEscapedView object. 87 | * 88 | * @since 0.2.4 89 | * 90 | * @param View $view View instance to decorate. 91 | * @param array|null $allowed_tags Optional. Array of allowed tags to let 92 | * through escaping functions. Set to sane 93 | * defaults if none provided. 94 | */ 95 | public function __construct( View $view, $allowed_tags = null ) { 96 | $this->view = $view; 97 | $this->allowed_tags = null === $allowed_tags 98 | ? $this->prepare_allowed_tags( wp_kses_allowed_html( 'post' ) ) 99 | : $allowed_tags; 100 | } 101 | 102 | /** 103 | * Render a given URI. 104 | * 105 | * @since 0.2.4 106 | * 107 | * @param array $context Context in which to render. 108 | * 109 | * @return string Rendered HTML. 110 | * @throws FailedToLoadView If the View URI could not be loaded. 111 | */ 112 | public function render( array $context = [] ) { 113 | return wp_kses( $this->view->render( $context ), $this->allowed_tags ); 114 | } 115 | 116 | /** 117 | * Render a partial view. 118 | * 119 | * This can be used from within a currently rendered view, to include 120 | * nested partials. 121 | * 122 | * The passed-in context is optional, and will fall back to the parent's 123 | * context if omitted. 124 | * 125 | * @since 0.2.4 126 | * 127 | * @param string $uri URI of the partial to render. 128 | * @param array|null $context Context in which to render the partial. 129 | * 130 | * @return string Rendered HTML. 131 | * @throws InvalidURI If the provided URI was not valid. 132 | * @throws FailedToLoadView If the view could not be loaded. 133 | */ 134 | public function render_partial( $uri, array $context = null ) { 135 | return wp_kses( 136 | $this->view->render_partial( $uri, $context ), 137 | $this->allowed_tags 138 | ); 139 | } 140 | 141 | /** 142 | * Prepare an array of allowed tags by adding form elements to the existing 143 | * array. 144 | * 145 | * This makes sure that the basic form elements always pass through the 146 | * escaping functions. 147 | * 148 | * @since 0.2.4 149 | * 150 | * @param array $allowed_tags Allowed tags as fetched from the WordPress 151 | * defaults. 152 | * 153 | * @return array Modified tags array. 154 | */ 155 | private function prepare_allowed_tags( $allowed_tags ) { 156 | return array_replace_recursive( $allowed_tags, self::FORM_TAGS ); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Metabox/Talk.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Metabox; 13 | 14 | use AlainSchlesser\Speaking\Assets\ScriptAsset; 15 | use AlainSchlesser\Speaking\Assets\StyleAsset; 16 | use AlainSchlesser\Speaking\CustomPostType\Talk as TalkCPT; 17 | use AlainSchlesser\Speaking\Model\TalkRepository; 18 | 19 | /** 20 | * Abstract class BaseMetabox. 21 | * 22 | * @since 0.1.0 23 | * 24 | * @package AlainSchlesser\Speaking 25 | * @author Alain Schlesser 26 | */ 27 | final class Talk extends BaseMetabox { 28 | 29 | const ID = 'talk_cpt_metabox'; 30 | const VIEW_URI = 'views/talk-metabox'; 31 | 32 | const CSS_HANDLE = 'as-speaking-backend-css'; 33 | const CSS_URI = 'assets/styles/as-speaking-backend'; 34 | 35 | const JS_HANDLE = 'as-speaking-backend-js'; 36 | const JS_URI = 'assets/scripts/as-speaking-backend'; 37 | 38 | const DATE_FORMATTER_JS_HANDLE = 'php-date-formatter-js'; 39 | const DATE_FORMATTER_JS_URI = 'assets/scripts/php-date-formatter'; 40 | 41 | /** 42 | * Get the ID to use for the metabox. 43 | * 44 | * @since 0.1.0 45 | * 46 | * @return string ID to use for the metabox. 47 | */ 48 | protected function get_id() { 49 | return self::ID; 50 | } 51 | 52 | /** 53 | * Get the title to use for the metabox. 54 | * 55 | * @since 0.1.0 56 | * 57 | * @return string Title to use for the metabox. 58 | */ 59 | protected function get_title() { 60 | return __( 'Talk properties', 'as-speaking' ); 61 | } 62 | 63 | /** 64 | * Get the array of known assets. 65 | * 66 | * @since 0.1.0 67 | * 68 | * @return array 69 | */ 70 | protected function get_assets() { 71 | 72 | $date_formatter_script = new ScriptAsset( 73 | self::DATE_FORMATTER_JS_HANDLE, 74 | self::DATE_FORMATTER_JS_URI, 75 | [], 76 | false, 77 | ScriptAsset::ENQUEUE_FOOTER 78 | ); 79 | 80 | $metabox_style = new StyleAsset( 81 | self::CSS_HANDLE, 82 | self::CSS_URI 83 | ); 84 | 85 | $metabox_script = new ScriptAsset( 86 | self::JS_HANDLE, 87 | self::JS_URI, 88 | [ 'jquery', self::DATE_FORMATTER_JS_HANDLE ], 89 | false, 90 | ScriptAsset::ENQUEUE_FOOTER 91 | ); 92 | 93 | $metabox_script->add_localization( 94 | 'speakingPageMetabox', 95 | [ 96 | 'dateFormat' => get_option( 'date_format' ), 97 | ] 98 | ); 99 | 100 | return [ 101 | $date_formatter_script, 102 | $metabox_script, 103 | $metabox_style, 104 | ]; 105 | } 106 | 107 | /** 108 | * Get the screen on which to show the metabox. 109 | * 110 | * @since 0.1.0 111 | * 112 | * @return string|array|\WP_Screen Screen on which to show the metabox. 113 | */ 114 | protected function get_screen() { 115 | return TalkCPT::SLUG; 116 | } 117 | 118 | /** 119 | * Get the context in which to show the metabox. 120 | * 121 | * @since 0.1.0 122 | * 123 | * @return string Context to use. 124 | */ 125 | protected function get_context() { 126 | return static::CONTEXT_SIDE; 127 | } 128 | 129 | /** 130 | * Get the priority within the context where the boxes should show. 131 | * 132 | * @since 0.1.0 133 | * 134 | * @return string Priority within context. 135 | */ 136 | protected function get_priority() { 137 | return static::PRIORITY_HIGH; 138 | } 139 | 140 | /** 141 | * Get the View URI to use for rendering the metabox. 142 | * 143 | * @since 0.1.0 144 | * 145 | * @return string View URI. 146 | */ 147 | protected function get_view_uri() { 148 | return self::VIEW_URI; 149 | } 150 | 151 | /** 152 | * Process the metabox attributes. 153 | * 154 | * @since 0.1.0 155 | * 156 | * @param array|string $atts Raw metabox attributes passed into the 157 | * metabox function. 158 | * 159 | * @return array Processed metabox attributes. 160 | */ 161 | protected function process_attributes( $atts ) { 162 | $talk_repository = new TalkRepository(); 163 | $atts = (array) $atts; 164 | $atts['talk'] = $talk_repository->find( get_the_ID() ); 165 | 166 | return $atts; 167 | } 168 | 169 | /** 170 | * Do the actual persistence of the changed data. 171 | * 172 | * @since 0.1.0 173 | * 174 | * @param int $post_id ID of the post to persist. 175 | * 176 | * @return void 177 | */ 178 | protected function persist( $post_id ) { 179 | $talk_repository = new TalkRepository(); 180 | $talk = $talk_repository->find( $post_id ); 181 | $talk->parse_post_data( $_POST ); 182 | $talk->persist_properties(); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/Widget/Talks.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Widget; 13 | 14 | use AlainSchlesser\Speaking\Assets\StyleAsset; 15 | use AlainSchlesser\Speaking\Model\TalkRepository; 16 | 17 | /** 18 | * Class Talks. 19 | * 20 | * @since 0.2.4 21 | * 22 | * @package AlainSchlesser\Speaking\Widget 23 | * @author Alain Schlesser 24 | */ 25 | final class Talks extends BaseWidget { 26 | 27 | const ID = 'as-speaking-talks-widget'; 28 | const CLASS_NAME = self::ID; 29 | const FRONTEND_VIEW_URI = 'views/talks-widget'; 30 | const BACKEND_VIEW_URI = 'views/talks-widget-form'; 31 | 32 | const CSS_HANDLE = 'as-speaking-frontend-css'; 33 | const CSS_URI = 'assets/styles/as-speaking-frontend'; 34 | 35 | const FIELD_TITLE = 'title'; 36 | const FIELD_TITLE_ID = 'title_id'; 37 | const FIELD_TITLE_NAME = 'title_name'; 38 | 39 | /** 40 | * Get the identifier of the widget. 41 | * 42 | * @since 0.2.4 43 | * 44 | * @return string ID of the widget. 45 | */ 46 | protected function get_id() { 47 | return self::ID; 48 | } 49 | 50 | /** 51 | * Get the title of the widget. 52 | * 53 | * @since 0.2.4 54 | * 55 | * @return string Title of the widget. 56 | */ 57 | protected function get_title() { 58 | return esc_html__( 'Latest Talks', 'as-speaking' ); 59 | } 60 | 61 | /** 62 | * Get the view URI used to render the front-end version of the widget. 63 | * 64 | * @since 0.2.4 65 | * 66 | * @return string URI of the frontend view. 67 | */ 68 | protected function get_frontend_view_uri() { 69 | return self::FRONTEND_VIEW_URI; 70 | } 71 | 72 | /** 73 | * Get the view URI used to render the back-end version of the widget. 74 | * 75 | * @since 0.2.4 76 | * 77 | * @return string|false URI of the backend view, or false if none. 78 | */ 79 | protected function get_backend_view_uri() { 80 | return self::BACKEND_VIEW_URI; 81 | } 82 | 83 | /** 84 | * Get the widget options. 85 | * 86 | * @since 0.2.4 87 | * 88 | * @return array Associative array of widget options. 89 | */ 90 | protected function get_widget_options() { 91 | return [ 92 | 'classname' => self::CLASS_NAME, 93 | 'description' => __( 94 | 'List of the latest published talks.', 95 | 'as-speaking' 96 | ), 97 | ]; 98 | } 99 | 100 | /** 101 | * Get the context to pass onto the front-end view. 102 | * 103 | * Override to provide data to the view that is not part of the widget 104 | * arguments. 105 | * 106 | * @since 0.2.4 107 | * 108 | * @param array $instance Saved values from the database. 109 | * @param array $args Array of widget arguments. 110 | * 111 | * @return array Context to pass onto view. 112 | */ 113 | protected function get_frontend_context( $instance, $args ) { 114 | $talk_repository = new TalkRepository(); 115 | 116 | return [ 117 | 'title' => isset( $instance[ self::FIELD_TITLE ] ) 118 | ? $instance[ self::FIELD_TITLE ] 119 | : esc_html__( 'My Talks', 'as-speaking' ), 120 | 'talks' => $talk_repository->find_latest(), 121 | ]; 122 | } 123 | 124 | /** 125 | * Get the context to pass onto the back-end view. 126 | * 127 | * Override to provide data to the view that is not part of the widget 128 | * arguments. 129 | * 130 | * @since 0.2.4 131 | * 132 | * @param array $instance Saved values from the database. 133 | * 134 | * @return array Context to pass onto view. 135 | */ 136 | protected function get_backend_context( $instance ) { 137 | return [ 138 | self::FIELD_TITLE => isset( $instance[ self::FIELD_TITLE ] ) 139 | ? $instance[ self::FIELD_TITLE ] 140 | : esc_html__( 'My Talks', 'as-speaking' ), 141 | self::FIELD_TITLE_ID => $this->get_field_id( self::FIELD_TITLE ), 142 | self::FIELD_TITLE_NAME => $this->get_field_name( self::FIELD_TITLE ), 143 | ]; 144 | } 145 | 146 | /** 147 | * Sanitize a set of instance values. 148 | * 149 | * @since 0.2.4 150 | * 151 | * @param array $instance Set of instance values to sanitize. 152 | * 153 | * @return array Sanitized instance values. 154 | */ 155 | protected function sanitize_instance( $instance ) { 156 | $sanitized = []; 157 | 158 | $sanitized[ self::FIELD_TITLE ] = isset( $instance[ self::FIELD_TITLE ] ) 159 | ? strip_tags( $instance[ self::FIELD_TITLE ] ) 160 | : ''; 161 | 162 | return $sanitized; 163 | } 164 | 165 | /** 166 | * Get the array of known assets. 167 | * 168 | * @since 0.2.4 169 | * 170 | * @return array 171 | */ 172 | protected function get_assets() { 173 | return is_admin() 174 | ? [] 175 | : [ 176 | new StyleAsset( 177 | self::CSS_HANDLE, 178 | self::CSS_URI 179 | ), 180 | ]; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/View/BaseView.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\View; 13 | 14 | use AlainSchlesser\Speaking\Exception\FailedToLoadView; 15 | use AlainSchlesser\Speaking\Exception\InvalidURI; 16 | 17 | /** 18 | * Class BaseView. 19 | * 20 | * Very basic View class to abstract away PHP view rendering. 21 | * 22 | * Note: This should normally be done through a dedicated package. 23 | * 24 | * @since 0.1.0 25 | * 26 | * @package AlainSchlesser\Speaking 27 | * @author Alain Schlesser 28 | */ 29 | class BaseView implements View { 30 | 31 | /** 32 | * Extension to use for view files. 33 | * 34 | * @since 0.1.0 35 | */ 36 | const VIEW_EXTENSION = 'php'; 37 | 38 | /** 39 | * Contexts to use for escaping. 40 | * 41 | * @since 0.2.3 42 | */ 43 | const CONTEXT_HTML = 'html'; 44 | const CONTEXT_JAVASCRIPT = 'js'; 45 | 46 | /** 47 | * URI to the view file to render. 48 | * 49 | * @since 0.1.0 50 | * 51 | * @var string 52 | */ 53 | protected $uri; 54 | 55 | /** 56 | * Internal storage for passed-in context. 57 | * 58 | * @since 0.1.0 59 | * 60 | * @var array 61 | */ 62 | protected $_context_ = []; 63 | 64 | /** 65 | * Instantiate a View object. 66 | * 67 | * @since 0.1.0 68 | * 69 | * @param string $uri URI to the view file to render. 70 | * 71 | * @throws InvalidURI If an invalid URI was passed into the View. 72 | */ 73 | public function __construct( $uri ) { 74 | $this->uri = $this->validate( $uri ); 75 | } 76 | 77 | /** 78 | * Render a given URI. 79 | * 80 | * @since 0.1.0 81 | * 82 | * @param array $context Context in which to render. 83 | * 84 | * @return string Rendered HTML. 85 | * @throws FailedToLoadView If the View URI could not be loaded. 86 | */ 87 | public function render( array $context = [] ) { 88 | 89 | // Add context to the current instance to make it available within the 90 | // rendered view. 91 | foreach ( $context as $key => $value ) { 92 | $this->$key = $value; 93 | } 94 | 95 | // Add entire context as array to the current instance to pass onto 96 | // partial views. 97 | $this->_context_ = $context; 98 | 99 | // Save current buffering level so we can backtrack in case of an error. 100 | // This is needed because the view itself might also add an unknown 101 | // number of output buffering levels. 102 | $buffer_level = ob_get_level(); 103 | ob_start(); 104 | 105 | try { 106 | include $this->uri; 107 | } catch ( \Exception $exception ) { 108 | // Remove whatever levels were added up until now. 109 | while ( ob_get_level() > $buffer_level ) { 110 | ob_end_clean(); 111 | } 112 | throw FailedToLoadView::view_exception( 113 | $this->uri, 114 | $exception 115 | ); 116 | } 117 | 118 | return ob_get_clean(); 119 | } 120 | 121 | /** 122 | * Render a partial view. 123 | * 124 | * This can be used from within a currently rendered view, to include 125 | * nested partials. 126 | * 127 | * The passed-in context is optional, and will fall back to the parent's 128 | * context if omitted. 129 | * 130 | * @since 0.1.0 131 | * 132 | * @param string $uri URI of the partial to render. 133 | * @param array|null $context Context in which to render the partial. 134 | * 135 | * @return string Rendered HTML. 136 | * @throws InvalidURI If the provided URI was not valid. 137 | * @throws FailedToLoadView If the view could not be loaded. 138 | */ 139 | public function render_partial( $uri, array $context = null ) { 140 | $view = new static( $uri ); 141 | 142 | return $view->render( $context ?: $this->_context_ ); 143 | } 144 | 145 | /** 146 | * Validate an URI. 147 | * 148 | * @since 0.1.0 149 | * 150 | * @param string $uri URI to validate. 151 | * 152 | * @return string Validated URI. 153 | * @throws InvalidURI If an invalid URI was passed into the View. 154 | */ 155 | protected function validate( $uri ) { 156 | $uri = $this->check_extension( $uri, static::VIEW_EXTENSION ); 157 | $uri = trailingslashit( dirname( __DIR__, 2 ) ) . $uri; 158 | 159 | if ( ! is_readable( $uri ) ) { 160 | throw InvalidURI::from_uri( $uri ); 161 | } 162 | 163 | return $uri; 164 | } 165 | 166 | /** 167 | * Check that the URI has the correct extension. 168 | * 169 | * Optionally adds the extension if none was detected. 170 | * 171 | * @since 0.2.5 172 | * 173 | * @param string $uri URI to check the extension of. 174 | * @param string $extension Extension to use. 175 | * 176 | * @return string URI with correct extension. 177 | */ 178 | protected function check_extension( $uri, $extension ) { 179 | $detected_extension = pathinfo( $uri, PATHINFO_EXTENSION ); 180 | 181 | if ( $extension !== $detected_extension ) { 182 | $uri .= '.' . $extension; 183 | } 184 | 185 | return $uri; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Autoloader.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking; 13 | 14 | /** 15 | * Class Autoloader. 16 | * 17 | * This is a custom autoloader to replace the functionality that we would 18 | * normally get through the autoloader generated by Composer. 19 | * 20 | * @since 0.1.0 21 | * 22 | * @package AlainSchlesser\Speaking 23 | * @author Alain Schlesser 24 | */ 25 | final class Autoloader { 26 | 27 | /** 28 | * Array containing the registered namespace structures 29 | * 30 | * @since 0.1.0 31 | * 32 | * @var array 33 | */ 34 | private $namespaces = []; 35 | 36 | /** 37 | * Destructor for the Autoloader class. 38 | * 39 | * The destructor automatically unregisters the autoload callback function 40 | * with the SPL autoload system. 41 | * 42 | * @since 0.1.0 43 | */ 44 | public function __destruct() { 45 | $this->unregister(); 46 | } 47 | 48 | /** 49 | * Registers the autoload callback with the SPL autoload system. 50 | * 51 | * @since 0.1.0 52 | */ 53 | public function register() { 54 | spl_autoload_register( [ $this, 'autoload' ] ); 55 | } 56 | 57 | /** 58 | * Unregisters the autoload callback with the SPL autoload system. 59 | * 60 | * @since 0.1.0 61 | */ 62 | public function unregister() { 63 | spl_autoload_unregister( [ $this, 'autoload' ] ); 64 | } 65 | 66 | /** 67 | * Add a specific namespace structure with our custom autoloader. 68 | * 69 | * @since 0.1.0 70 | * 71 | * @param string $root Root namespace name. 72 | * @param string $base_dir Directory containing the class files. 73 | * @param string $prefix Prefix to be added before the class. 74 | * @param string $suffix Suffix to be added after the class. 75 | * @param boolean $lowercase Whether the class should be changed to 76 | * lowercase. 77 | * @param boolean $underscores Whether the underscores should be changed to 78 | * hyphens. 79 | * 80 | * @return self 81 | */ 82 | public function add_namespace( 83 | $root, 84 | $base_dir, 85 | $prefix = '', 86 | $suffix = '.php', 87 | $lowercase = false, 88 | $underscores = false 89 | ) { 90 | $this->namespaces[] = [ 91 | 'root' => $this->normalize_root( (string) $root ), 92 | 'base_dir' => trailingslashit( (string) $base_dir ), 93 | 'prefix' => (string) $prefix, 94 | 'suffix' => (string) $suffix, 95 | 'lowercase' => (bool) $lowercase, 96 | 'underscores' => (bool) $underscores, 97 | ]; 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * The autoload function that gets registered with the SPL Autoloader 104 | * system. 105 | * 106 | * @since 0.1.0 107 | * 108 | * @param string $class The class that got requested by the spl_autoloader. 109 | */ 110 | public function autoload( $class ) { 111 | 112 | // Iterate over namespaces to find a match. 113 | foreach ( $this->namespaces as $namespace ) { 114 | 115 | // Move on if the object does not belong to the current namespace. 116 | if ( 0 !== strpos( $class, $namespace['root'] ) ) { 117 | continue; 118 | } 119 | 120 | // Remove namespace root level to correspond with root filesystem. 121 | $filename = str_replace( 122 | $namespace['root'], '', 123 | $class 124 | ); 125 | 126 | // Remove a leading backslash from the class name. 127 | $filename = $this->remove_leading_backslash( $filename ); 128 | 129 | 130 | // Replace the namespace separator "\" by the system-dependent 131 | // directory separator. 132 | $filename = str_replace( 133 | '\\', DIRECTORY_SEPARATOR, 134 | $filename 135 | ); 136 | 137 | // Change to lower case if requested. 138 | if ( true === $namespace['lowercase'] ) { 139 | $filename = strtolower( $filename ); 140 | } 141 | 142 | // Change underscores into hyphens if requested. 143 | if ( true === $namespace['underscores'] ) { 144 | $filename = str_replace( '_', '-', $filename ); 145 | } 146 | 147 | // Add base_dir, prefix and suffix. 148 | $filepath = $namespace['base_dir'] 149 | . $namespace['prefix'] 150 | . $filename 151 | . $namespace['suffix']; 152 | 153 | // Require the file if it exists and is readable. 154 | if ( is_readable( $filepath ) ) { 155 | require $filepath; 156 | } 157 | } 158 | } 159 | 160 | /** 161 | * Normalize a namespace root. 162 | * 163 | * @since 0.1.0 164 | * 165 | * @param string $root Namespace root that needs to be normalized. 166 | * 167 | * @return string Normalized namespace root. 168 | */ 169 | private function normalize_root( $root ) { 170 | $root = $this->remove_leading_backslash( $root ); 171 | 172 | return $this->add_trailing_backslash( $root ); 173 | } 174 | 175 | /** 176 | * Remove a leading backslash from a string. 177 | * 178 | * @since 0.1.0 179 | * 180 | * @param string $string String to remove the leading backslash from. 181 | * 182 | * @return string Modified string. 183 | */ 184 | private function remove_leading_backslash( $string ) { 185 | return ltrim( $string, '\\' ); 186 | } 187 | 188 | /** 189 | * Make sure a string ends with a trailing backslash. 190 | * 191 | * @since 0.1.0 192 | * 193 | * @param string $string String to check the trailing backslash of. 194 | * 195 | * @return string Modified string. 196 | */ 197 | private function add_trailing_backslash( $string ) { 198 | return rtrim( $string, '\\' ) . '\\'; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Assets/BaseAsset.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Assets; 13 | 14 | use Closure; 15 | 16 | /** 17 | * Abstract class BaseAsset. 18 | * 19 | * @since 0.1.0 20 | * 21 | * @package AlainSchlesser\Speaking\Assets 22 | * @author Alain Schlesser 23 | */ 24 | abstract class BaseAsset implements Asset { 25 | 26 | const REGISTER_PRIORITY = 1; 27 | const ENQUEUE_PRIORITY = 10; 28 | const DEQUEUE_PRIORITY = 20; 29 | 30 | /** 31 | * Handle of the asset. 32 | * 33 | * @since 0.1.0 34 | * 35 | * @var string 36 | */ 37 | protected $handle; 38 | 39 | /** 40 | * Get the handle of the asset. 41 | * 42 | * @since 0.1.0 43 | * 44 | * @return string 45 | */ 46 | public function get_handle() { 47 | return $this->handle; 48 | } 49 | 50 | /** 51 | * Register the current Registerable. 52 | * 53 | * @since 0.1.0 54 | * 55 | * @return void 56 | */ 57 | public function register() { 58 | $this->deferred_action( 59 | $this->get_register_action(), 60 | $this->get_register_closure(), 61 | static::REGISTER_PRIORITY 62 | ); 63 | } 64 | 65 | /** 66 | * Enqueue the asset. 67 | * 68 | * @since 0.1.0 69 | * 70 | * @return void 71 | */ 72 | public function enqueue() { 73 | $this->deferred_action( 74 | $this->get_enqueue_action(), 75 | $this->get_enqueue_closure(), 76 | static::ENQUEUE_PRIORITY 77 | ); 78 | } 79 | 80 | /** 81 | * Dequeue the asset. 82 | * 83 | * @since 0.2.7 84 | * 85 | * @return void 86 | */ 87 | public function dequeue() { 88 | $this->deferred_action( 89 | $this->get_dequeue_action(), 90 | $this->get_dequeue_closure(), 91 | static::DEQUEUE_PRIORITY 92 | ); 93 | } 94 | 95 | /** 96 | * Add a deferred action hook. 97 | * 98 | * If the action has already passed, the closure will be called directly. 99 | * 100 | * @since 0.1.0 101 | * 102 | * @param string $action Deferred action to hook to. 103 | * @param Closure $closure Closure to attach to the action. 104 | * @param int $priority Optional. Priority to use. Defaults to 10. 105 | */ 106 | protected function deferred_action( $action, $closure, $priority = 10 ) { 107 | if ( did_action( $action ) ) { 108 | $closure(); 109 | 110 | return; 111 | } 112 | 113 | add_action( 114 | $action, 115 | $closure, 116 | $priority 117 | ); 118 | } 119 | 120 | /** 121 | * Get the register action to use. 122 | * 123 | * @since 0.1.0 124 | * 125 | * @return string Register action to use. 126 | */ 127 | protected function get_register_action() { 128 | return $this->get_enqueue_action(); 129 | } 130 | 131 | /** 132 | * Get the enqueue action to use. 133 | * 134 | * @since 0.1.0 135 | * 136 | * @return string Enqueue action name. 137 | */ 138 | protected function get_enqueue_action() { 139 | return is_admin() ? 'admin_enqueue_scripts' : 'wp_enqueue_scripts'; 140 | } 141 | 142 | /** 143 | * Get the enqueue action to use. 144 | * 145 | * @since 0.1.0 146 | * 147 | * @return string Enqueue action name. 148 | */ 149 | protected function get_dequeue_action() { 150 | return is_admin() ? 'admin_print_scripts' : 'wp_print_scripts'; 151 | } 152 | 153 | /** 154 | * Normalize the source URI. 155 | * 156 | * @since 0.1.0 157 | * 158 | * @param string $uri Source URI to normalize. 159 | * @param string $extension Default extension to use. 160 | * 161 | * @return string Normalized source URI. 162 | */ 163 | protected function normalize_source( $uri, $extension ) { 164 | $uri = $this->check_extension( $uri, $extension ); 165 | $uri = plugins_url( $uri, dirname( __FILE__, 2 ) ); 166 | 167 | return $this->check_for_minified_asset( $uri, $extension ); 168 | } 169 | 170 | /** 171 | * Return the URI of the minified asset if it is readable and 172 | * `SCRIPT_DEBUG` is not set. 173 | * 174 | * @since 0.1.9 175 | * 176 | * @param string $uri Source URI. 177 | * @param string $extension Default extension to use. 178 | * 179 | * @return string URI of the asset to use. 180 | */ 181 | protected function check_for_minified_asset( $uri, $extension ) { 182 | $debug = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG; 183 | $minified_uri = str_replace( $extension, "min.{$extension}", $uri ); 184 | 185 | return ! $debug && is_readable( $minified_uri ) ? $minified_uri : $uri; 186 | } 187 | 188 | /** 189 | * Check that the URI has the correct extension. 190 | * 191 | * Optionally adds the extension if none was detected. 192 | * 193 | * @since 0.2.5 194 | * 195 | * @param string $uri URI to check the extension of. 196 | * @param string $extension Extension to use. 197 | * 198 | * @return string URI with correct extension. 199 | */ 200 | public function check_extension( $uri, $extension ) { 201 | $detected_extension = pathinfo( $uri, PATHINFO_EXTENSION ); 202 | 203 | if ( $extension !== $detected_extension ) { 204 | $uri .= '.' . $extension; 205 | } 206 | 207 | return $uri; 208 | } 209 | 210 | /** 211 | * Get the enqueue closure to use. 212 | * 213 | * @since 0.1.0 214 | * 215 | * @return Closure 216 | */ 217 | abstract protected function get_register_closure(); 218 | 219 | /** 220 | * Get the enqueue closure to use. 221 | * 222 | * @since 0.1.0 223 | * 224 | * @return Closure 225 | */ 226 | abstract protected function get_enqueue_closure(); 227 | 228 | /** 229 | * Get the dequeue closure to use. 230 | * 231 | * @since 0.2.7 232 | * 233 | * @return Closure 234 | */ 235 | abstract protected function get_dequeue_closure(); 236 | } 237 | -------------------------------------------------------------------------------- /assets/scripts/php-date-formatter.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @copyright Copyright © Kartik Visweswaran, Krajee.com, 2014 - 2017 3 | * @version 1.3.4 4 | * 5 | * Date formatter utility library that allows formatting date/time variables or Date objects using PHP DateTime format. 6 | * This library is a standalone javascript library and does not depend on other libraries or plugins like jQuery. 7 | * @see http://php.net/manual/en/function.date.php 8 | * 9 | * For more JQuery plugins visit http://plugins.krajee.com 10 | * For more Yii related demos visit http://demos.krajee.com 11 | */var DateFormatter;!function(){"use strict";var t,e,r,n,a,u,i;u=864e5,i=3600,t=function(t,e){return"string"==typeof t&&"string"==typeof e&&t.toLowerCase()===e.toLowerCase()},e=function(t,r,n){var a=n||"0",u=t.toString();return u.lengths?"20":"19")+i):s,g=!0;break;case"m":case"n":case"M":case"F":if(isNaN(s)){if(o=d.getMonth(i),!(o>0))return null;y.month=o}else{if(!(s>=1&&12>=s))return null;y.month=s}g=!0;break;case"d":case"j":if(!(s>=1&&31>=s))return null;y.day=s,g=!0;break;case"g":case"h":if(c=n.indexOf("a")>-1?n.indexOf("a"):n.indexOf("A")>-1?n.indexOf("A"):-1,h=a[c],-1!==c)f=t(h,p.meridiem[0])?0:t(h,p.meridiem[1])?12:-1,s>=1&&12>=s&&-1!==f?y.hour=s%12===0?f:s+f:s>=0&&23>=s&&(y.hour=s);else{if(!(s>=0&&23>=s))return null;y.hour=s}m=!0;break;case"G":case"H":if(!(s>=0&&23>=s))return null;y.hour=s,m=!0;break;case"i":if(!(s>=0&&59>=s))return null;y.min=s,m=!0;break;case"s":if(!(s>=0&&59>=s))return null;y.sec=s,m=!0}if(g===!0&&y.year&&y.month&&y.day)y.date=new Date(y.year,y.month-1,y.day,y.hour,y.min,y.sec,0);else{if(m!==!0)return null;y.date=new Date(0,0,0,y.hour,y.min,y.sec,0)}return y.date},guessDate:function(t,e){if("string"!=typeof t)return t;var r,n,a,u,i,s,o=this,c=t.replace(o.separators,"\x00").split("\x00"),f=/^[djmn]/g,l=e.match(o.validParts),h=new Date,d=0;if(!f.test(l[0]))return t;for(a=0;ar?r:4,n=parseInt(4>r?n.toString().substr(0,4-r)+i:i.substr(0,4)),!n)return null;h.setFullYear(n);break;case 3:h.setHours(s);break;case 4:h.setMinutes(s);break;case 5:h.setSeconds(s)}u=i.substr(d),u.length>0&&c.splice(a+1,0,u)}return h},parseFormat:function(t,r){var n,a=this,s=a.dateSettings,o=/\\?(.?)/gi,c=function(t,e){return n[t]?n[t]():e};return n={d:function(){return e(n.j(),2)},D:function(){return s.daysShort[n.w()]},j:function(){return r.getDate()},l:function(){return s.days[n.w()]},N:function(){return n.w()||7},w:function(){return r.getDay()},z:function(){var t=new Date(n.Y(),n.n()-1,n.j()),e=new Date(n.Y(),0,1);return Math.round((t-e)/u)},W:function(){var t=new Date(n.Y(),n.n()-1,n.j()-n.N()+3),r=new Date(t.getFullYear(),0,4);return e(1+Math.round((t-r)/u/7),2)},F:function(){return s.months[r.getMonth()]},m:function(){return e(n.n(),2)},M:function(){return s.monthsShort[r.getMonth()]},n:function(){return r.getMonth()+1},t:function(){return new Date(n.Y(),n.n(),0).getDate()},L:function(){var t=n.Y();return t%4===0&&t%100!==0||t%400===0?1:0},o:function(){var t=n.n(),e=n.W(),r=n.Y();return r+(12===t&&9>e?1:1===t&&e>9?-1:0)},Y:function(){return r.getFullYear()},y:function(){return n.Y().toString().slice(-2)},a:function(){return n.A().toLowerCase()},A:function(){var t=n.G()<12?0:1;return s.meridiem[t]},B:function(){var t=r.getUTCHours()*i,n=60*r.getUTCMinutes(),a=r.getUTCSeconds();return e(Math.floor((t+n+a+i)/86.4)%1e3,3)},g:function(){return n.G()%12||12},G:function(){return r.getHours()},h:function(){return e(n.g(),2)},H:function(){return e(n.G(),2)},i:function(){return e(r.getMinutes(),2)},s:function(){return e(r.getSeconds(),2)},u:function(){return e(1e3*r.getMilliseconds(),6)},e:function(){var t=/\((.*)\)/.exec(String(r))[1];return t||"Coordinated Universal Time"},I:function(){var t=new Date(n.Y(),0),e=Date.UTC(n.Y(),0),r=new Date(n.Y(),6),a=Date.UTC(n.Y(),6);return t-e!==r-a?1:0},O:function(){var t=r.getTimezoneOffset(),n=Math.abs(t);return(t>0?"-":"+")+e(100*Math.floor(n/60)+n%60,4)},P:function(){var t=n.O();return t.substr(0,3)+":"+t.substr(3,2)},T:function(){var t=(String(r).match(a.tzParts)||[""]).pop().replace(a.tzClip,"");return t||"UTC"},Z:function(){return 60*-r.getTimezoneOffset()},c:function(){return"Y-m-d\\TH:i:sP".replace(o,c)},r:function(){return"D, d M Y H:i:s O".replace(o,c)},U:function(){return r.getTime()/1e3||0}},c(t,t)},formatDate:function(t,e){var r,n,a,u,i,s=this,o="",c="\\";if("string"==typeof t&&(t=s.parseDate(t,e),!t))return null;if(t instanceof Date){for(a=e.length,r=0;a>r;r++)i=e.charAt(r),"S"!==i&&i!==c&&(r>0&&e.charAt(r-1)===c?o+=i:(u=s.parseFormat(i,t),r!==a-1&&s.intParts.test(i)&&"S"===e.charAt(r+1)&&(n=parseInt(u)||0,u+=s.dateSettings.ordinal(n)),o+=u));return o}return""}}}(); 12 | -------------------------------------------------------------------------------- /src/Widget/BaseWidget.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Widget; 13 | 14 | use AlainSchlesser\Speaking\Assets\AssetsAware; 15 | use AlainSchlesser\Speaking\Assets\AssetsAwareness; 16 | use AlainSchlesser\Speaking\Renderable; 17 | use AlainSchlesser\Speaking\Service; 18 | use AlainSchlesser\Speaking\View\FormEscapedView; 19 | use AlainSchlesser\Speaking\View\PostEscapedView; 20 | use AlainSchlesser\Speaking\View\TemplatedView; 21 | use WP_Widget; 22 | 23 | /** 24 | * Abstract class BaseWidget. 25 | * 26 | * @since 0.2.4 27 | * 28 | * @package AlainSchlesser\Speaking\Widget 29 | * @author Alain Schlesser 30 | */ 31 | abstract class BaseWidget extends WP_Widget implements AssetsAware, Renderable, Service { 32 | 33 | use AssetsAwareness; 34 | 35 | /** 36 | * Instantiate a BaseWidget object. 37 | * 38 | * @since 0.2.4 39 | */ 40 | public function __construct() { 41 | parent::__construct( 42 | $this->get_id(), 43 | $this->get_title(), 44 | $this->get_widget_options(), 45 | $this->get_control_options() 46 | ); 47 | } 48 | 49 | /** 50 | * Register the current Registerable. 51 | * 52 | * @since 0.2.4 53 | * 54 | * @return void 55 | */ 56 | public function register() { 57 | $this->register_assets(); 58 | 59 | add_action( 'widgets_init', function () { 60 | register_widget( $this ); 61 | } ); 62 | } 63 | 64 | /** 65 | * Render the current Renderable. 66 | * 67 | * @since 0.2.4 68 | * 69 | * @param array $context Context in which to render. 70 | * 71 | * @return string Rendered HTML. 72 | */ 73 | public function render( array $context = [] ) { 74 | try { 75 | $this->enqueue_assets(); 76 | 77 | $view = new PostEscapedView( 78 | new TemplatedView( $this->get_frontend_view_uri() ) 79 | ); 80 | 81 | return $view->render( $context ); 82 | } catch ( \Exception $exception ) { 83 | // Don't let exceptions bubble up. Just render an empty shortcode 84 | // instead. 85 | return ''; 86 | } 87 | } 88 | 89 | /** 90 | * Frontend display of the widget. 91 | * 92 | * @since 0.2.4 93 | * 94 | * @param array $args Arguments passed onto the widget. 95 | * @param array $instance Instance to render. 96 | * 97 | * @return void 98 | */ 99 | public function widget( $args, $instance ) { 100 | $args = $this->process_arguments( $args ); 101 | $context = $this->get_frontend_context( $instance, $args ); 102 | 103 | echo $this->render( array_merge( $args, $context ) ); 104 | } 105 | 106 | /** 107 | * Render the backend widget form. 108 | * 109 | * @since 0.2.4 110 | * 111 | * @param array $instance Saved values from the database. 112 | * 113 | * @return void 114 | */ 115 | public function form( $instance ) { 116 | $uri = $this->get_backend_view_uri(); 117 | 118 | if ( ! $uri ) { 119 | parent::form( $instance ); 120 | } 121 | 122 | try { 123 | $this->enqueue_assets(); 124 | 125 | $view = new FormEscapedView( 126 | new TemplatedView( $uri ) 127 | ); 128 | 129 | $context = $this->get_backend_context( $instance ); 130 | 131 | echo $view->render( $context ); 132 | } catch ( \Exception $exception ) { 133 | // Don't let exceptions bubble up. Just render an empty shortcode 134 | // instead. 135 | } 136 | } 137 | 138 | /** 139 | * Updates a particular instance of a widget. 140 | * 141 | * This function should check that `$new_instance` is set correctly. The 142 | * newly-calculated value of `$instance` should be returned. If false is 143 | * returned, the instance won't be saved/updated. 144 | * 145 | * @since 0.2.4 146 | * @access public 147 | * 148 | * @param array $new_instance New settings for this instance as input by 149 | * the user via WP_Widget::form(). 150 | * @param array $old_instance Old settings for this instance. 151 | * 152 | * @return array Settings to save or bool false to cancel saving. 153 | */ 154 | public function update( $new_instance, $old_instance ) { 155 | return $this->sanitize_instance( $new_instance ); 156 | } 157 | 158 | /** 159 | * Process the widget arguments. 160 | * 161 | * Override to add accepted arguments and their default values. 162 | * 163 | * @since 0.2.4 164 | * 165 | * @param array|string $args Raw widget arguments passed into the widget 166 | * function. 167 | * 168 | * @return array Processed shortcode arguments. 169 | */ 170 | protected function process_arguments( $args ) { 171 | return wp_parse_args( 172 | $args, 173 | $this->get_default_arguments() 174 | ); 175 | } 176 | 177 | /** 178 | * Get the context to pass onto the front-end view. 179 | * 180 | * Override to provide data to the view that is not part of the widget 181 | * arguments. 182 | * 183 | * @since 0.2.4 184 | * 185 | * @param array $instance Saved values from the database. 186 | * @param array $args Array of widget arguments. 187 | * 188 | * @return array Context to pass onto view. 189 | */ 190 | protected function get_frontend_context( $instance, $args ) { 191 | return []; 192 | } 193 | 194 | /** 195 | * Get the context to pass onto the back-end view. 196 | * 197 | * Override to provide data to the view that is not part of the widget 198 | * arguments. 199 | * 200 | * @since 0.2.4 201 | * 202 | * @param array $instance Saved values from the database. 203 | * 204 | * @return array Context to pass onto view. 205 | */ 206 | protected function get_backend_context( $instance ) { 207 | return []; 208 | } 209 | 210 | /** 211 | * Associative array of default arguments. 212 | * 213 | * @since 0.2.4 214 | * 215 | * @return array Default arguments. 216 | */ 217 | protected function get_default_arguments() { 218 | return []; 219 | } 220 | 221 | /** 222 | * Get the widget options. 223 | * 224 | * @since 0.2.4 225 | * 226 | * @return array Associative array of widget options. 227 | */ 228 | protected function get_widget_options() { 229 | return []; 230 | } 231 | 232 | /** 233 | * Get the control options for the widget. 234 | * 235 | * @since 0.2.4 236 | * 237 | * @return array Associative array of control options. 238 | */ 239 | protected function get_control_options() { 240 | return []; 241 | } 242 | 243 | /** 244 | * Get the view URI used to render the back-end version of the widget. 245 | * 246 | * @since 0.2.4 247 | * 248 | * @return string|false URI of the backend view, or false if none. 249 | */ 250 | protected function get_backend_view_uri() { 251 | return false; 252 | } 253 | 254 | /** 255 | * Sanitize a set of instance values. 256 | * 257 | * @since 0.2.4 258 | * 259 | * @param array $instance Set of instance values to sanitize. 260 | * 261 | * @return array Sanitized instance values. 262 | */ 263 | protected function sanitize_instance( $instance ) { 264 | return $instance; 265 | } 266 | 267 | /** 268 | * Get the identifier of the widget. 269 | * 270 | * @since 0.2.4 271 | * 272 | * @return string ID of the widget. 273 | */ 274 | abstract protected function get_id(); 275 | 276 | /** 277 | * Get the title of the widget. 278 | * 279 | * @since 0.2.4 280 | * 281 | * @return string Title of the widget. 282 | */ 283 | abstract protected function get_title(); 284 | 285 | /** 286 | * Get the view URI used to render the front-end version of the widget. 287 | * 288 | * @since 0.2.4 289 | * 290 | * @return string URI of the frontend view. 291 | */ 292 | abstract protected function get_frontend_view_uri(); 293 | } 294 | -------------------------------------------------------------------------------- /src/Metabox/BaseMetabox.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Metabox; 13 | 14 | use AlainSchlesser\Speaking\Assets\AssetsAware; 15 | use AlainSchlesser\Speaking\Assets\AssetsAwareness; 16 | use AlainSchlesser\Speaking\Renderable; 17 | use AlainSchlesser\Speaking\Service; 18 | use AlainSchlesser\Speaking\View\FormEscapedView; 19 | use AlainSchlesser\Speaking\View\TemplatedView; 20 | use Closure; 21 | 22 | /** 23 | * Abstract class BaseMetabox. 24 | * 25 | * @since 0.1.0 26 | * 27 | * @package AlainSchlesser\Speaking 28 | * @author Alain Schlesser 29 | */ 30 | abstract class BaseMetabox implements Renderable, Service, AssetsAware { 31 | 32 | use AssetsAwareness; 33 | 34 | const CONTEXT_ADVANCED = 'advanced'; 35 | const CONTEXT_NORMAL = 'normal'; 36 | const CONTEXT_SIDE = 'side'; 37 | 38 | const PRIORITY_DEFAULT = 'default'; 39 | const PRIORITY_HIGH = 'high'; 40 | const PRIORITY_LOW = 'low'; 41 | 42 | /** 43 | * Register the Metabox. 44 | * 45 | * @since 0.1.0 46 | */ 47 | public function register() { 48 | $this->register_assets(); 49 | $this->register_persistence_hooks(); 50 | 51 | add_action( 'add_meta_boxes', function () { 52 | add_meta_box( 53 | $this->get_id(), 54 | $this->get_title(), 55 | [ $this, 'process_metabox' ], 56 | $this->get_screen(), 57 | $this->get_context(), 58 | $this->get_priority(), 59 | $this->get_callback_args() 60 | ); 61 | } ); 62 | } 63 | 64 | /** 65 | * Process the metabox attributes and prepare rendering. 66 | * 67 | * @since 0.1.0 68 | * 69 | * @param array|string $atts Attributes as passed to the metabox. 70 | * 71 | * @return void The rendered content needs to be echoed. 72 | */ 73 | public function process_metabox( $atts ) { 74 | $atts = $this->process_attributes( $atts ); 75 | $atts['metabox_id'] = $this->get_id(); 76 | $atts['nonce_field'] = $this->render_nonce(); 77 | 78 | echo $this->render( (array) $atts ); 79 | } 80 | 81 | /** 82 | * Render the current Renderable. 83 | * 84 | * @since 0.1.0 85 | * 86 | * @param array $context Context in which to render. 87 | * 88 | * @return string Rendered HTML. 89 | */ 90 | public function render( array $context = [] ) { 91 | try { 92 | $this->enqueue_assets(); 93 | 94 | $view = new FormEscapedView( 95 | new TemplatedView( $this->get_view_uri() ) 96 | ); 97 | 98 | return $view->render( $context ); 99 | } catch ( \Exception $exception ) { 100 | // Don't let exceptions bubble up. Just render the exception message 101 | // into the metabox. 102 | return sprintf( 103 | '
%s
', 104 | $exception->getMessage() 105 | ); 106 | } 107 | } 108 | 109 | /** 110 | * Render the nonce. 111 | * 112 | * @since 0.1.0 113 | * 114 | * @return string Hidden field with a nonce. 115 | */ 116 | protected function render_nonce() { 117 | ob_start(); 118 | 119 | wp_nonce_field( 120 | $this->get_nonce_action(), 121 | $this->get_nonce_name() 122 | ); 123 | 124 | return ob_get_clean(); 125 | } 126 | 127 | /** 128 | * Verify the nonce and return the result. 129 | * 130 | * @since 0.1.0 131 | * 132 | * @return bool Whether the nonce could be successfully verified. 133 | */ 134 | protected function verify_nonce() { 135 | $nonce_name = $this->get_nonce_name(); 136 | 137 | if ( ! array_key_exists( $nonce_name, $_POST ) ) { 138 | return false; 139 | } 140 | 141 | $nonce = $_POST[ $nonce_name ]; 142 | 143 | $result = wp_verify_nonce( 144 | $nonce, 145 | $this->get_nonce_action() 146 | ); 147 | 148 | return false !== $result; 149 | } 150 | 151 | /** 152 | * Get the action of the nonce to use. 153 | * 154 | * @since 0.1.0 155 | * 156 | * @return string Action of the nonce. 157 | */ 158 | protected function get_nonce_action() { 159 | return "{$this->get_id()}_action"; 160 | } 161 | 162 | /** 163 | * Get the name of the nonce to use. 164 | * 165 | * @since 0.1.0 166 | * 167 | * @return string Name of the nonce. 168 | */ 169 | protected function get_nonce_name() { 170 | return "{$this->get_id()}_nonce"; 171 | } 172 | 173 | /** 174 | * Get the ID to use for the metabox. 175 | * 176 | * @since 0.1.0 177 | * 178 | * @return string ID to use for the metabox. 179 | */ 180 | abstract protected function get_id(); 181 | 182 | /** 183 | * Get the title to use for the metabox. 184 | * 185 | * @since 0.1.0 186 | * 187 | * @return string Title to use for the metabox. 188 | */ 189 | abstract protected function get_title(); 190 | 191 | /** 192 | * Get the screen on which to show the metabox. 193 | * 194 | * @since 0.1.0 195 | * 196 | * @return string|array|\WP_Screen Screen on which to show the metabox. 197 | */ 198 | protected function get_screen() { 199 | return null; 200 | } 201 | 202 | /** 203 | * Get the context in which to show the metabox. 204 | * 205 | * @since 0.1.0 206 | * 207 | * @return string Context to use. 208 | */ 209 | protected function get_context() { 210 | return static::CONTEXT_ADVANCED; 211 | } 212 | 213 | /** 214 | * Get the priority within the context where the boxes should show. 215 | * 216 | * @since 0.1.0 217 | * 218 | * @return string Priority within context. 219 | */ 220 | protected function get_priority() { 221 | return static::PRIORITY_DEFAULT; 222 | } 223 | 224 | /** 225 | * Get the array of arguments to pass to the render callback. 226 | * 227 | * @since 0.1.0 228 | * 229 | * @return array Array of arguments. 230 | */ 231 | protected function get_callback_args() { 232 | return []; 233 | } 234 | 235 | /** 236 | * Register the persistence hooks to be triggered by a save attempt. 237 | * 238 | * @since 0.1.0 239 | */ 240 | protected function register_persistence_hooks() { 241 | $closure = $this->get_persistence_closure(); 242 | add_action( 'save_post', $closure ); 243 | } 244 | 245 | /** 246 | * Return the persistence closure. 247 | * 248 | * @since 0.1.0 249 | * 250 | * @return Closure 251 | */ 252 | protected function get_persistence_closure() { 253 | return function ( $post_id ) { 254 | // Verify nonce and bail early if it doesn't verify. 255 | if ( ! $this->verify_nonce() ) { 256 | return $post_id; 257 | } 258 | 259 | // Bail early if this is an autosave. 260 | if ( wp_is_post_autosave( $post_id ) ) { 261 | return $post_id; 262 | } 263 | 264 | // Bail early if this is a revision. 265 | if ( wp_is_post_revision( $post_id ) ) { 266 | return $post_id; 267 | } 268 | 269 | // Check the user's permissions. 270 | if ( ! current_user_can( 'edit_post', $post_id ) ) { 271 | return $post_id; 272 | } 273 | 274 | // Check if there was a multisite switch before. 275 | if ( is_multisite() && ms_is_switched() ) { 276 | return $post_id; 277 | } 278 | 279 | $this->persist( $post_id ); 280 | 281 | return $post_id; 282 | }; 283 | } 284 | 285 | /** 286 | * Do the actual persistence of the changed data. 287 | * 288 | * @since 0.1.0 289 | * 290 | * @param int $post_id ID of the post to persist. 291 | * 292 | * @return void 293 | */ 294 | abstract protected function persist( $post_id ); 295 | 296 | /** 297 | * Get the View URI to use for rendering the metabox. 298 | * 299 | * @since 0.1.0 300 | * 301 | * @return string View URI. 302 | */ 303 | abstract protected function get_view_uri(); 304 | 305 | /** 306 | * Process the metabox attributes. 307 | * 308 | * @since 0.1.0 309 | * 310 | * @param array|string $atts Raw metabox attributes passed into the 311 | * metabox function. 312 | * 313 | * @return array Processed metabox attributes. 314 | */ 315 | abstract protected function process_attributes( $atts ); 316 | } 317 | -------------------------------------------------------------------------------- /src/Model/Talk.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | * @link https://www.alainschlesser.com/ 9 | * @copyright 2017 Alain Schlesser 10 | */ 11 | 12 | namespace AlainSchlesser\Speaking\Model; 13 | 14 | /** 15 | * Class Talk. 16 | * 17 | * @since 0.1.0 18 | * 19 | * @package AlainSchlesser\Speaking 20 | * @author Alain Schlesser 21 | */ 22 | class Talk extends CustomPostTypeEntity { 23 | 24 | /** 25 | * Contains a map of custom (meta) properties and their corresponding 26 | * sanitization filters. 27 | */ 28 | const SANITIZATION = [ 29 | TalkMeta::EVENT_NAME => FILTER_SANITIZE_STRING, 30 | TalkMeta::EVENT_LINK => FILTER_SANITIZE_URL, 31 | TalkMeta::SESSION_DATE => FILTER_SANITIZE_NUMBER_INT, 32 | TalkMeta::SESSION_LINK => FILTER_SANITIZE_URL, 33 | TalkMeta::VIDEO => FILTER_SANITIZE_URL, 34 | TalkMeta::SLIDES => FILTER_SANITIZE_URL, 35 | TalkMeta::IMAGE_LINK => FILTER_SANITIZE_STRING, 36 | ]; 37 | 38 | /** 39 | * Get the name of the event. 40 | * 41 | * @since 0.1.0 42 | * 43 | * @return string Name of the event. 44 | */ 45 | public function get_event_name() { 46 | return $this->event_name; 47 | } 48 | 49 | /** 50 | * Set the name of the event. 51 | * 52 | * @since 0.1.0 53 | * 54 | * @param string $event_name New name of the event. 55 | */ 56 | public function set_event_name( $event_name ) { 57 | $this->event_name = filter_var( 58 | $event_name, 59 | static::SANITIZATION[ TalkMeta::EVENT_NAME ] 60 | ); 61 | } 62 | 63 | /** 64 | * Get the URI the event links to. 65 | * 66 | * @since 0.1.0 67 | * 68 | * @return string URI the event links to. 69 | */ 70 | public function get_event_link() { 71 | return $this->event_link; 72 | } 73 | 74 | /** 75 | * Set the URI the event links to. 76 | * 77 | * @since 0.1.0 78 | * 79 | * @param string $event_link New URI the event links to. 80 | */ 81 | public function set_event_link( $event_link ) { 82 | $this->event_link = filter_var( 83 | $event_link, 84 | static::SANITIZATION[ TalkMeta::EVENT_LINK ] 85 | ); 86 | } 87 | 88 | /** 89 | * Get the date of the session. 90 | * 91 | * @since 0.1.0 92 | * 93 | * @return string Date of the session. 94 | */ 95 | public function get_session_date() { 96 | return $this->session_date; 97 | } 98 | 99 | /** 100 | * Set the date of the session. 101 | * 102 | * @since 0.1.0 103 | * 104 | * @param string $session_date New date of the session. 105 | */ 106 | public function set_session_date( $session_date ) { 107 | $this->session_date = filter_var( 108 | $session_date, 109 | static::SANITIZATION[ TalkMeta::SESSION_DATE ] 110 | ); 111 | } 112 | 113 | /** 114 | * Get the URI the session links to. 115 | * 116 | * @since 0.1.0 117 | * 118 | * @return string URI the session links to. 119 | */ 120 | public function get_session_link() { 121 | return $this->session_link; 122 | } 123 | 124 | /** 125 | * Set the URI the session links to. 126 | * 127 | * @since 0.1.0 128 | * 129 | * @param string $session_link New URI the session links to. 130 | */ 131 | public function set_session_link( $session_link ) { 132 | $this->session_link = filter_var( 133 | $session_link, 134 | static::SANITIZATION[ TalkMeta::SESSION_LINK ] 135 | ); 136 | } 137 | 138 | /** 139 | * Get the URI of the video. 140 | * 141 | * @since 0.1.0 142 | * 143 | * @return string URI of the video. 144 | */ 145 | public function get_video() { 146 | return $this->video; 147 | } 148 | 149 | /** 150 | * Set the URI of the video. 151 | * 152 | * @since 0.1.0 153 | * 154 | * @param string $video New URI of the video. 155 | */ 156 | public function set_video( $video ) { 157 | $this->video = filter_var( 158 | $video, 159 | static::SANITIZATION[ TalkMeta::VIDEO ] 160 | ); 161 | } 162 | 163 | /** 164 | * Get the URI of the slides. 165 | * 166 | * @since 0.1.0 167 | * 168 | * @return string URI of the slides. 169 | */ 170 | public function get_slides() { 171 | return $this->slides; 172 | } 173 | 174 | /** 175 | * Set the URI of the slides. 176 | * 177 | * @since 0.1.0 178 | * 179 | * @param string $slides New URI of the slides. 180 | */ 181 | public function set_slides( $slides ) { 182 | $this->slides = filter_var( 183 | $slides, 184 | static::SANITIZATION[ TalkMeta::SLIDES ] 185 | ); 186 | } 187 | 188 | /** 189 | * Get the element the featured image links to. 190 | * 191 | * @since 0.1.0 192 | * 193 | * @return string Element the featured image links to. 194 | */ 195 | public function get_image_link() { 196 | return $this->image_link; 197 | } 198 | 199 | /** 200 | * Set the element the featured image links to. 201 | * 202 | * @since 0.1.0 203 | * 204 | * @param string $image_link New element the featured image links to. 205 | */ 206 | public function set_image_link( $image_link ) { 207 | $this->image_link = filter_var( 208 | $image_link, 209 | static::SANITIZATION[ TalkMeta::IMAGE_LINK ] 210 | ); 211 | } 212 | 213 | /** 214 | * Get the featured image. 215 | * 216 | * Depending on the `$image_link` setting, it might be wrapped in a link. 217 | * 218 | * @since 0.1.0 219 | * 220 | * @param string|null $size Optional. Size to create the featured image in. 221 | * 222 | * @return string Rendered featured image HTML. 223 | */ 224 | public function get_featured_image( $size = null ) { 225 | $image = get_the_post_thumbnail( $this->post->ID, $size ); 226 | 227 | if ( empty( $image ) ) { 228 | return ''; 229 | } 230 | 231 | switch ( $this->image_link ) { 232 | case TalkMeta::IMAGE_LINK_EVENT: 233 | $image = "event_link}\">{$image}"; 234 | break; 235 | case TalkMeta::IMAGE_LINK_SESSION: 236 | $image = "session_link}\">{$image}"; 237 | break; 238 | case TalkMeta::IMAGE_LINK_VIDEO: 239 | $image = "video}\">{$image}"; 240 | break; 241 | case TalkMeta::IMAGE_LINK_SLIDES: 242 | $image = "slides}\">{$image}"; 243 | break; 244 | default: 245 | break; 246 | } 247 | 248 | return $image; 249 | } 250 | 251 | /** 252 | * Parse the data from the superglobal $_POST. 253 | * 254 | * @since 0.1.0 255 | * 256 | * @param array $post $_POST superglobal. 257 | */ 258 | public function parse_post_data( array $post ) { 259 | foreach ( $this->get_lazy_properties() as $key => $default ) { 260 | $this->$key = filter_var( 261 | $post[ TalkMeta::FORM_FIELD_PREFIX . $key ], 262 | array_key_exists( $key, static::SANITIZATION ) 263 | ? static::SANITIZATION[ $key ] 264 | : FILTER_SANITIZE_STRING 265 | ); 266 | } 267 | 268 | $aa = filter_var( 269 | $post[ TalkMeta::FORM_FIELD_SESSION_AA ], 270 | FILTER_SANITIZE_NUMBER_INT 271 | ); 272 | 273 | $mm = filter_var( 274 | $post[ TalkMeta::FORM_FIELD_SESSION_MM ], 275 | FILTER_SANITIZE_NUMBER_INT 276 | ); 277 | 278 | $jj = filter_var( 279 | $post[ TalkMeta::FORM_FIELD_SESSION_JJ ], 280 | FILTER_SANITIZE_NUMBER_INT 281 | ); 282 | 283 | $date = new \DateTimeImmutable( "$aa-$mm-$jj" ); 284 | $this->set_session_date( $date->getTimestamp() ); 285 | } 286 | 287 | /** 288 | * Return the list of lazily-loaded properties and their default values. 289 | * 290 | * @since 0.2.1 291 | * 292 | * @return array 293 | */ 294 | protected function get_lazy_properties() { 295 | return [ 296 | TalkMeta::EVENT_NAME => '', 297 | TalkMeta::EVENT_LINK => '', 298 | TalkMeta::SESSION_DATE => '', 299 | TalkMeta::SESSION_LINK => '', 300 | TalkMeta::VIDEO => '', 301 | TalkMeta::SLIDES => '', 302 | TalkMeta::IMAGE_LINK => TalkMeta::IMAGE_LINK_VIDEO, 303 | ]; 304 | } 305 | 306 | /** 307 | * Load a lazily-loaded property. 308 | * 309 | * After this process, the loaded property should be set within the 310 | * object's state, otherwise the load procedure might be triggered multiple 311 | * times. 312 | * 313 | * @since 0.2.1 314 | * 315 | * @param string $property Name of the property to load. 316 | * 317 | * @return void 318 | */ 319 | protected function load_lazy_property( $property ) { 320 | $meta = get_post_meta( $this->post->ID ); 321 | 322 | foreach ( $this->get_lazy_properties() as $key => $default ) { 323 | $this->$key = array_key_exists( 324 | TalkMeta::META_PREFIX . $key, 325 | $meta 326 | ) 327 | ? $meta[ TalkMeta::META_PREFIX . $key ][0] 328 | : $default; 329 | } 330 | } 331 | 332 | /** 333 | * Persist the additional properties of the entity. 334 | * 335 | * @since 0.2.1 336 | * 337 | * @return void 338 | */ 339 | public function persist_properties() { 340 | foreach ( $this->get_lazy_properties() as $key => $default ) { 341 | if ( $this->$key === $default ) { 342 | delete_post_meta( 343 | $this->post->ID, 344 | TalkMeta::META_PREFIX . $key 345 | ); 346 | continue; 347 | } 348 | 349 | update_post_meta( 350 | $this->post->ID, 351 | TalkMeta::META_PREFIX . $key, 352 | $this->$key 353 | ); 354 | } 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /assets/scripts/as-speaking-backend.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AlainSchlesser.com Speaking Page Plugin. 3 | * 4 | * @package AlainSchlesser\Speaking 5 | * @author Alain Schlesser 6 | * @license MIT 7 | * @link https://www.alainschlesser.com/ 8 | * @copyright 2017 Alain Schlesser 9 | */ 10 | 11 | /* global speakingPageMetabox */ 12 | 13 | jQuery(document).ready(function ($) { 14 | var $talkCPTEventFields = $('#talk-cpt-event-fields'); 15 | var $talkCPTSessionFields = $('#talk-cpt-session-fields'); 16 | var $talkCPTVideoInput = $('#talk-cpt-video-input'); 17 | var $talkCPTSlidesInput = $('#talk-cpt-slides-input'); 18 | var $talkCPTImageLinkSelect = $('#talk-cpt-image-link-select'); 19 | 20 | /** 21 | * Make sure the event represents the current settings. 22 | * 23 | * @returns {boolean} True. 24 | */ 25 | var updateEvent = function () { 26 | 27 | var talkCPTEventName = $('#talk_cpt_event_name'); 28 | var talkCPTEventLink = $('#talk_cpt_event_link'); 29 | var eventName = talkCPTEventName.val(); 30 | var eventLink = talkCPTEventLink.val(); 31 | var eventDisplayHtml = eventName; 32 | 33 | if (talkCPTEventName.is(':hidden') || talkCPTEventLink.is(':hidden')) { 34 | $('.edit-talk-cpt-event').show(); 35 | } 36 | 37 | // Update "Event:" to currently configured event. 38 | if (eventLink) { 39 | eventDisplayHtml = '' + eventName + ''; 40 | } 41 | 42 | $('#talk-cpt-event-display').html(eventDisplayHtml); 43 | return true; 44 | }; 45 | 46 | // Talk CPT Event edit click. 47 | $talkCPTEventFields.siblings('a.edit-talk-cpt-event').click( 48 | function (event) { 49 | if ($talkCPTEventFields.is(':hidden')) { 50 | $talkCPTEventFields.slideDown('fast', function () { 51 | $talkCPTEventFields.find('select').focus(); 52 | }); 53 | $(this).hide(); 54 | } 55 | event.preventDefault(); 56 | } 57 | ); 58 | 59 | // Save the Talk CPT Event changes and hide the options. 60 | $talkCPTEventFields.find('.save-talk-cpt-event').click( 61 | function (event) { 62 | $talkCPTEventFields.slideUp('fast') 63 | .siblings('a.edit-talk-cpt-event').show().focus(); 64 | updateEvent(); 65 | event.preventDefault(); 66 | } 67 | ); 68 | 69 | // Cancel Talk CPT Event editing and hide the options. 70 | $talkCPTEventFields.find('.cancel-talk-cpt-event').click( 71 | function (event) { 72 | $talkCPTEventFields.slideUp('fast') 73 | .siblings('a.edit-talk-cpt-event').show().focus(); 74 | $('#talk_cpt_event_name').val($('#hidden_talk_cpt_event_name').val()); 75 | $('#talk_cpt_event_link').val($('#hidden_talk_cpt_event_link').val()); 76 | updateEvent(); 77 | event.preventDefault(); 78 | } 79 | ); 80 | 81 | /** 82 | * Make sure the session represents the current settings. 83 | * 84 | * @returns {boolean} True. 85 | */ 86 | var updateSession = function () { 87 | 88 | var talkCPTSessionDate = $('#talk_cpt_session_date'); 89 | var talkCPTSessionLink = $('#talk_cpt_session_link'); 90 | var sessionDate = talkCPTSessionDate.val(); 91 | var sessionLink = talkCPTSessionLink.val(); 92 | var aa = $('#talk_cpt_session_aa').val(); 93 | var mm = $('#talk_cpt_session_mm').val(); 94 | var jj = $('#talk_cpt_session_jj').val(); 95 | var attemptedDate = new Date( aa, mm - 1, jj ); 96 | var dateFormatter = new DateFormatter(); 97 | var sessionDisplayHtml = dateFormatter.formatDate( attemptedDate, speakingPageMetabox.dateFormat ); 98 | 99 | if (talkCPTSessionDate.is(':hidden') || talkCPTSessionLink.is(':hidden')) { 100 | $('.edit-talk-cpt-session').show(); 101 | } 102 | 103 | // Update "Session:" to currently configured session. 104 | if (sessionLink) { 105 | sessionDisplayHtml = '' + sessionDisplayHtml + ''; 106 | } 107 | 108 | $('#talk-cpt-session-display').html(sessionDisplayHtml); 109 | return true; 110 | }; 111 | 112 | // Talk CPT Session edit click. 113 | $talkCPTSessionFields.siblings('a.edit-talk-cpt-session').click( 114 | function (event) { 115 | if ($talkCPTSessionFields.is(':hidden')) { 116 | $talkCPTSessionFields.slideDown('fast', function () { 117 | $talkCPTSessionFields.find('select').focus(); 118 | }); 119 | $(this).hide(); 120 | } 121 | event.preventDefault(); 122 | } 123 | ); 124 | 125 | // Save the Talk CPT Session changes and hide the options. 126 | $talkCPTSessionFields.find('.save-talk-cpt-session').click( 127 | function (event) { 128 | $talkCPTSessionFields.slideUp('fast') 129 | .siblings('a.edit-talk-cpt-session').show().focus(); 130 | updateSession(); 131 | event.preventDefault(); 132 | } 133 | ); 134 | 135 | // Cancel Talk CPT Session editing and hide the options. 136 | $talkCPTSessionFields.find('.cancel-talk-cpt-session').click( 137 | function (event) { 138 | $talkCPTSessionFields.slideUp('fast') 139 | .siblings('a.edit-talk-cpt-session').show().focus(); 140 | $('#talk_cpt_session_date').val($('#hidden_talk_cpt_session_date').val()); 141 | $('#talk_cpt_session_mm').val($('#hidden_talk_cpt_session_mm').val()); 142 | $('#talk_cpt_session_jj').val($('#hidden_talk_cpt_session_jj').val()); 143 | $('#talk_cpt_session_aa').val($('#hidden_talk_cpt_session_aa').val()); 144 | $('#talk_cpt_session_link').val($('#hidden_talk_cpt_session_link').val()); 145 | updateSession(); 146 | event.preventDefault(); 147 | } 148 | ); 149 | 150 | /** 151 | * Make sure the video represents the current settings. 152 | * 153 | * @returns {boolean} True. 154 | */ 155 | var updateVideo = function () { 156 | 157 | var talkCPTVideo = $('#talk_cpt_video'); 158 | var video = talkCPTVideo.val(); 159 | var videoDisplayHtml = ''; 160 | 161 | if (talkCPTVideo.is(':hidden')) { 162 | $('.edit-talk-cpt-video').show(); 163 | } 164 | 165 | // Update "Video:" to currently configured video. 166 | if (video) { 167 | videoDisplayHtml = 'Link'; 168 | } 169 | 170 | $('#talk-cpt-video-display').html(videoDisplayHtml); 171 | return true; 172 | }; 173 | 174 | // Talk CPT Video edit click. 175 | $talkCPTVideoInput.siblings('a.edit-talk-cpt-video').click( 176 | function (event) { 177 | if ($talkCPTVideoInput.is(':hidden')) { 178 | $talkCPTVideoInput.slideDown('fast', function () { 179 | $talkCPTVideoInput.find('select').focus(); 180 | }); 181 | $(this).hide(); 182 | } 183 | event.preventDefault(); 184 | } 185 | ); 186 | 187 | // Save the Talk CPT Video changes and hide the options. 188 | $talkCPTVideoInput.find('.save-talk-cpt-video').click( 189 | function (event) { 190 | $talkCPTVideoInput.slideUp('fast') 191 | .siblings('a.edit-talk-cpt-video').show().focus(); 192 | updateVideo(); 193 | event.preventDefault(); 194 | } 195 | ); 196 | 197 | // Cancel Talk CPT Video editing and hide the options. 198 | $talkCPTVideoInput.find('.cancel-talk-cpt-video').click( 199 | function (event) { 200 | $talkCPTVideoInput.slideUp('fast') 201 | .siblings('a.edit-talk-cpt-video').show().focus(); 202 | $('#talk_cpt_video').val($('#hidden_talk_cpt_video').val()); 203 | updateVideo(); 204 | event.preventDefault(); 205 | } 206 | ); 207 | 208 | /** 209 | * Make sure the slides represents the current settings. 210 | * 211 | * @returns {boolean} True. 212 | */ 213 | var updateSlides = function () { 214 | 215 | var talkCPTSlides = $('#talk_cpt_slides'); 216 | var slides = talkCPTSlides.val(); 217 | var slidesDisplayHtml = ''; 218 | 219 | if (talkCPTSlides.is(':hidden')) { 220 | $('.edit-talk-cpt-slides').show(); 221 | } 222 | 223 | // Update "Slides:" to currently configured slides. 224 | if (slides) { 225 | slidesDisplayHtml = 'Link'; 226 | } 227 | 228 | $('#talk-cpt-slides-display').html(slidesDisplayHtml); 229 | return true; 230 | }; 231 | 232 | // Talk CPT Slides edit click. 233 | $talkCPTSlidesInput.siblings('a.edit-talk-cpt-slides').click( 234 | function (event) { 235 | if ($talkCPTSlidesInput.is(':hidden')) { 236 | $talkCPTSlidesInput.slideDown('fast', function () { 237 | $talkCPTSlidesInput.find('select').focus(); 238 | }); 239 | $(this).hide(); 240 | } 241 | event.preventDefault(); 242 | } 243 | ); 244 | 245 | // Save the Talk CPT Slides changes and hide the options. 246 | $talkCPTSlidesInput.find('.save-talk-cpt-slides').click( 247 | function (event) { 248 | $talkCPTSlidesInput.slideUp('fast') 249 | .siblings('a.edit-talk-cpt-slides').show().focus(); 250 | updateSlides(); 251 | event.preventDefault(); 252 | } 253 | ); 254 | 255 | // Cancel Talk CPT Slides editing and hide the options. 256 | $talkCPTSlidesInput.find('.cancel-talk-cpt-slides').click( 257 | function (event) { 258 | $talkCPTSlidesInput.slideUp('fast') 259 | .siblings('a.edit-talk-cpt-slides').show().focus(); 260 | $('#talk_cpt_slides').val($('#hidden_talk_cpt_slides').val()); 261 | updateSlides(); 262 | event.preventDefault(); 263 | } 264 | ); 265 | 266 | /** 267 | * Make sure the image link represents the current settings. 268 | * 269 | * @returns {boolean} True. 270 | */ 271 | var updateImageLink = function () { 272 | 273 | var talkCPTImageLink = $('#talk_cpt_image_link'); 274 | 275 | if (talkCPTImageLink.is(':hidden')) { 276 | $('.edit-talk-cpt-image-link').show(); 277 | } 278 | 279 | // Update "Image links to:" to currently selected link. 280 | $('#talk-cpt-image-link-display').html( 281 | $('option:selected', talkCPTImageLink).text() 282 | ); 283 | return true; 284 | }; 285 | 286 | // Talk CPT Image Link edit click. 287 | $talkCPTImageLinkSelect.siblings('a.edit-talk-cpt-image-link').click( 288 | function (event) { 289 | if ($talkCPTImageLinkSelect.is(':hidden')) { 290 | $talkCPTImageLinkSelect.slideDown('fast', function () { 291 | $talkCPTImageLinkSelect.find('select').focus(); 292 | }); 293 | $(this).hide(); 294 | } 295 | event.preventDefault(); 296 | } 297 | ); 298 | 299 | // Save the Talk CPT Image Link changes and hide the options. 300 | $talkCPTImageLinkSelect.find('.save-talk-cpt-image-link').click( 301 | function (event) { 302 | $talkCPTImageLinkSelect.slideUp('fast') 303 | .siblings('a.edit-talk-cpt-image-link').show().focus(); 304 | updateImageLink(); 305 | event.preventDefault(); 306 | } 307 | ); 308 | 309 | // Cancel Talk CPT Image Link editing and hide the options. 310 | $talkCPTImageLinkSelect.find('.cancel-talk-cpt-image-link').click( 311 | function (event) { 312 | $talkCPTImageLinkSelect.slideUp('fast') 313 | .siblings('a.edit-talk-cpt-image-link').show().focus(); 314 | $('#talk_cpt_image_link').val($('#hidden_talk_cpt_image_link').val()); 315 | updateImageLink(); 316 | event.preventDefault(); 317 | } 318 | ); 319 | }); 320 | -------------------------------------------------------------------------------- /assets/scripts/php-date-formatter.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @copyright Copyright © Kartik Visweswaran, Krajee.com, 2014 - 2017 3 | * @version 1.3.4 4 | * 5 | * Date formatter utility library that allows formatting date/time variables or Date objects using PHP DateTime format. 6 | * This library is a standalone javascript library and does not depend on other libraries or plugins like jQuery. 7 | * 8 | * @see http://php.net/manual/en/function.date.php 9 | * 10 | * For more JQuery plugins visit http://plugins.krajee.com 11 | * For more Yii related demos visit http://demos.krajee.com 12 | */ 13 | var DateFormatter; 14 | (function () { 15 | "use strict"; 16 | 17 | var _compare, _lpad, _extend, _indexOf, defaultSettings, DAY, HOUR; 18 | DAY = 1000 * 60 * 60 * 24; 19 | HOUR = 3600; 20 | 21 | _compare = function (str1, str2) { 22 | return typeof(str1) === 'string' && typeof(str2) === 'string' && str1.toLowerCase() === str2.toLowerCase(); 23 | }; 24 | _lpad = function (value, length, chr) { 25 | var val = value.toString(); 26 | chr = chr || '0'; 27 | return val.length < length ? _lpad(chr + val, length) : val; 28 | }; 29 | _extend = function (out) { 30 | var i, obj; 31 | out = out || {}; 32 | for (i = 1; i < arguments.length; i++) { 33 | obj = arguments[i]; 34 | if (!obj) { 35 | continue; 36 | } 37 | for (var key in obj) { 38 | if (obj.hasOwnProperty(key)) { 39 | if (typeof obj[key] === 'object') { 40 | _extend(out[key], obj[key]); 41 | } else { 42 | out[key] = obj[key]; 43 | } 44 | } 45 | } 46 | } 47 | return out; 48 | }; 49 | _indexOf = function (val, arr) { 50 | for (var i = 0; i < arr.length; i++) { 51 | if (arr[i].toLowerCase() === val.toLowerCase()) { 52 | return i; 53 | } 54 | } 55 | return -1; 56 | }; 57 | defaultSettings = { 58 | dateSettings: { 59 | days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], 60 | daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 61 | months: [ 62 | 'January', 'February', 'March', 'April', 'May', 'June', 'July', 63 | 'August', 'September', 'October', 'November', 'December' 64 | ], 65 | monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], 66 | meridiem: ['AM', 'PM'], 67 | ordinal: function (number) { 68 | var n = number % 10, suffixes = {1: 'st', 2: 'nd', 3: 'rd'}; 69 | return Math.floor(number % 100 / 10) === 1 || !suffixes[n] ? 'th' : suffixes[n]; 70 | } 71 | }, 72 | separators: /[ \-+\/\.T:@]/g, 73 | validParts: /[dDjlNSwzWFmMntLoYyaABgGhHisueTIOPZcrU]/g, 74 | intParts: /[djwNzmnyYhHgGis]/g, 75 | tzParts: /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, 76 | tzClip: /[^-+\dA-Z]/g 77 | }; 78 | 79 | DateFormatter = function (options) { 80 | var self = this, config = _extend(defaultSettings, options); 81 | self.dateSettings = config.dateSettings; 82 | self.separators = config.separators; 83 | self.validParts = config.validParts; 84 | self.intParts = config.intParts; 85 | self.tzParts = config.tzParts; 86 | self.tzClip = config.tzClip; 87 | }; 88 | 89 | DateFormatter.prototype = { 90 | constructor: DateFormatter, 91 | getMonth: function (val) { 92 | var self = this, i; 93 | i = _indexOf(val, self.dateSettings.monthsShort) + 1; 94 | if (i === 0) { 95 | i = _indexOf(val, self.dateSettings.months) + 1; 96 | } 97 | return i; 98 | }, 99 | parseDate: function (vDate, vFormat) { 100 | var self = this, vFormatParts, vDateParts, i, vDateFlag = false, vTimeFlag = false, vDatePart, iDatePart, 101 | vSettings = self.dateSettings, vMonth, vMeriIndex, vMeriOffset, len, mer, 102 | out = {date: null, year: null, month: null, day: null, hour: 0, min: 0, sec: 0}; 103 | if (!vDate) { 104 | return null; 105 | } 106 | if (vDate instanceof Date) { 107 | return vDate; 108 | } 109 | if (vFormat === 'U') { 110 | i = parseInt(vDate); 111 | return i ? new Date(i * 1000) : vDate; 112 | } 113 | switch (typeof vDate) { 114 | case 'number': 115 | return new Date(vDate); 116 | case 'string': 117 | break; 118 | default: 119 | return null; 120 | } 121 | vFormatParts = vFormat.match(self.validParts); 122 | if (!vFormatParts || vFormatParts.length === 0) { 123 | throw new Error("Invalid date format definition."); 124 | } 125 | vDateParts = vDate.replace(self.separators, '\0').split('\0'); 126 | for (i = 0; i < vDateParts.length; i++) { 127 | vDatePart = vDateParts[i]; 128 | iDatePart = parseInt(vDatePart); 129 | switch (vFormatParts[i]) { 130 | case 'y': 131 | case 'Y': 132 | if (iDatePart) { 133 | len = vDatePart.length; 134 | out.year = len === 2 ? parseInt((iDatePart < 70 ? '20' : '19') + vDatePart) : iDatePart; 135 | } else { 136 | return null; 137 | } 138 | vDateFlag = true; 139 | break; 140 | case 'm': 141 | case 'n': 142 | case 'M': 143 | case 'F': 144 | if (isNaN(iDatePart)) { 145 | vMonth = self.getMonth(vDatePart); 146 | if (vMonth > 0) { 147 | out.month = vMonth; 148 | } else { 149 | return null; 150 | } 151 | } else { 152 | if (iDatePart >= 1 && iDatePart <= 12) { 153 | out.month = iDatePart; 154 | } else { 155 | return null; 156 | } 157 | } 158 | vDateFlag = true; 159 | break; 160 | case 'd': 161 | case 'j': 162 | if (iDatePart >= 1 && iDatePart <= 31) { 163 | out.day = iDatePart; 164 | } else { 165 | return null; 166 | } 167 | vDateFlag = true; 168 | break; 169 | case 'g': 170 | case 'h': 171 | vMeriIndex = (vFormatParts.indexOf('a') > -1) ? vFormatParts.indexOf('a') : 172 | (vFormatParts.indexOf('A') > -1) ? vFormatParts.indexOf('A') : -1; 173 | mer = vDateParts[vMeriIndex]; 174 | if (vMeriIndex !== -1) { 175 | vMeriOffset = _compare(mer, vSettings.meridiem[0]) ? 0 : 176 | (_compare(mer, vSettings.meridiem[1]) ? 12 : -1); 177 | if (iDatePart >= 1 && iDatePart <= 12 && vMeriOffset !== -1) { 178 | out.hour = iDatePart % 12 === 0 ? vMeriOffset : iDatePart + vMeriOffset; 179 | } else { 180 | if (iDatePart >= 0 && iDatePart <= 23) { 181 | out.hour = iDatePart; 182 | } 183 | } 184 | } else { 185 | if (iDatePart >= 0 && iDatePart <= 23) { 186 | out.hour = iDatePart; 187 | } else { 188 | return null; 189 | } 190 | } 191 | vTimeFlag = true; 192 | break; 193 | case 'G': 194 | case 'H': 195 | if (iDatePart >= 0 && iDatePart <= 23) { 196 | out.hour = iDatePart; 197 | } else { 198 | return null; 199 | } 200 | vTimeFlag = true; 201 | break; 202 | case 'i': 203 | if (iDatePart >= 0 && iDatePart <= 59) { 204 | out.min = iDatePart; 205 | } else { 206 | return null; 207 | } 208 | vTimeFlag = true; 209 | break; 210 | case 's': 211 | if (iDatePart >= 0 && iDatePart <= 59) { 212 | out.sec = iDatePart; 213 | } else { 214 | return null; 215 | } 216 | vTimeFlag = true; 217 | break; 218 | } 219 | } 220 | if (vDateFlag === true && out.year && out.month && out.day) { 221 | out.date = new Date(out.year, out.month - 1, out.day, out.hour, out.min, out.sec, 0); 222 | } else { 223 | if (vTimeFlag !== true) { 224 | return null; 225 | } 226 | out.date = new Date(0, 0, 0, out.hour, out.min, out.sec, 0); 227 | } 228 | return out.date; 229 | }, 230 | guessDate: function (vDateStr, vFormat) { 231 | if (typeof vDateStr !== 'string') { 232 | return vDateStr; 233 | } 234 | var self = this, vParts = vDateStr.replace(self.separators, '\0').split('\0'), vPattern = /^[djmn]/g, len, 235 | vFormatParts = vFormat.match(self.validParts), vDate = new Date(), vDigit = 0, vYear, i, n, iPart, iSec; 236 | 237 | if (!vPattern.test(vFormatParts[0])) { 238 | return vDateStr; 239 | } 240 | 241 | for (i = 0; i < vParts.length; i++) { 242 | vDigit = 2; 243 | iPart = vParts[i]; 244 | iSec = parseInt(iPart.substr(0, 2)); 245 | if (isNaN(iSec)) { 246 | return null; 247 | } 248 | switch (i) { 249 | case 0: 250 | if (vFormatParts[0] === 'm' || vFormatParts[0] === 'n') { 251 | vDate.setMonth(iSec - 1); 252 | } else { 253 | vDate.setDate(iSec); 254 | } 255 | break; 256 | case 1: 257 | if (vFormatParts[0] === 'm' || vFormatParts[0] === 'n') { 258 | vDate.setDate(iSec); 259 | } else { 260 | vDate.setMonth(iSec - 1); 261 | } 262 | break; 263 | case 2: 264 | vYear = vDate.getFullYear(); 265 | len = iPart.length; 266 | vDigit = len < 4 ? len : 4; 267 | vYear = parseInt(len < 4 ? vYear.toString().substr(0, 4 - len) + iPart : iPart.substr(0, 4)); 268 | if (!vYear) { 269 | return null; 270 | } 271 | vDate.setFullYear(vYear); 272 | break; 273 | case 3: 274 | vDate.setHours(iSec); 275 | break; 276 | case 4: 277 | vDate.setMinutes(iSec); 278 | break; 279 | case 5: 280 | vDate.setSeconds(iSec); 281 | break; 282 | } 283 | n = iPart.substr(vDigit); 284 | if (n.length > 0) { 285 | vParts.splice(i + 1, 0, n); 286 | } 287 | } 288 | return vDate; 289 | }, 290 | parseFormat: function (vChar, vDate) { 291 | var self = this, vSettings = self.dateSettings, fmt, backslash = /\\?(.?)/gi, doFormat = function (t, s) { 292 | return fmt[t] ? fmt[t]() : s; 293 | }; 294 | fmt = { 295 | ///////// 296 | // DAY // 297 | ///////// 298 | /** 299 | * Day of month with leading 0: `01..31` 300 | * @return {string} 301 | */ 302 | d: function () { 303 | return _lpad(fmt.j(), 2); 304 | }, 305 | /** 306 | * Shorthand day name: `Mon...Sun` 307 | * @return {string} 308 | */ 309 | D: function () { 310 | return vSettings.daysShort[fmt.w()]; 311 | }, 312 | /** 313 | * Day of month: `1..31` 314 | * @return {number} 315 | */ 316 | j: function () { 317 | return vDate.getDate(); 318 | }, 319 | /** 320 | * Full day name: `Monday...Sunday` 321 | * @return {number} 322 | */ 323 | l: function () { 324 | return vSettings.days[fmt.w()]; 325 | }, 326 | /** 327 | * ISO-8601 day of week: `1[Mon]..7[Sun]` 328 | * @return {number} 329 | */ 330 | N: function () { 331 | return fmt.w() || 7; 332 | }, 333 | /** 334 | * Day of week: `0[Sun]..6[Sat]` 335 | * @return {number} 336 | */ 337 | w: function () { 338 | return vDate.getDay(); 339 | }, 340 | /** 341 | * Day of year: `0..365` 342 | * @return {number} 343 | */ 344 | z: function () { 345 | var a = new Date(fmt.Y(), fmt.n() - 1, fmt.j()), b = new Date(fmt.Y(), 0, 1); 346 | return Math.round((a - b) / DAY); 347 | }, 348 | 349 | ////////// 350 | // WEEK // 351 | ////////// 352 | /** 353 | * ISO-8601 week number 354 | * @return {number} 355 | */ 356 | W: function () { 357 | var a = new Date(fmt.Y(), fmt.n() - 1, fmt.j() - fmt.N() + 3), b = new Date(a.getFullYear(), 0, 4); 358 | return _lpad(1 + Math.round((a - b) / DAY / 7), 2); 359 | }, 360 | 361 | /////////// 362 | // MONTH // 363 | /////////// 364 | /** 365 | * Full month name: `January...December` 366 | * @return {string} 367 | */ 368 | F: function () { 369 | return vSettings.months[vDate.getMonth()]; 370 | }, 371 | /** 372 | * Month w/leading 0: `01..12` 373 | * @return {string} 374 | */ 375 | m: function () { 376 | return _lpad(fmt.n(), 2); 377 | }, 378 | /** 379 | * Shorthand month name; `Jan...Dec` 380 | * @return {string} 381 | */ 382 | M: function () { 383 | return vSettings.monthsShort[vDate.getMonth()]; 384 | }, 385 | /** 386 | * Month: `1...12` 387 | * @return {number} 388 | */ 389 | n: function () { 390 | return vDate.getMonth() + 1; 391 | }, 392 | /** 393 | * Days in month: `28...31` 394 | * @return {number} 395 | */ 396 | t: function () { 397 | return (new Date(fmt.Y(), fmt.n(), 0)).getDate(); 398 | }, 399 | 400 | ////////// 401 | // YEAR // 402 | ////////// 403 | /** 404 | * Is leap year? `0 or 1` 405 | * @return {number} 406 | */ 407 | L: function () { 408 | var Y = fmt.Y(); 409 | return (Y % 4 === 0 && Y % 100 !== 0 || Y % 400 === 0) ? 1 : 0; 410 | }, 411 | /** 412 | * ISO-8601 year 413 | * @return {number} 414 | */ 415 | o: function () { 416 | var n = fmt.n(), W = fmt.W(), Y = fmt.Y(); 417 | return Y + (n === 12 && W < 9 ? 1 : n === 1 && W > 9 ? -1 : 0); 418 | }, 419 | /** 420 | * Full year: `e.g. 1980...2010` 421 | * @return {number} 422 | */ 423 | Y: function () { 424 | return vDate.getFullYear(); 425 | }, 426 | /** 427 | * Last two digits of year: `00...99` 428 | * @return {string} 429 | */ 430 | y: function () { 431 | return fmt.Y().toString().slice(-2); 432 | }, 433 | 434 | ////////// 435 | // TIME // 436 | ////////// 437 | /** 438 | * Meridian lower: `am or pm` 439 | * @return {string} 440 | */ 441 | a: function () { 442 | return fmt.A().toLowerCase(); 443 | }, 444 | /** 445 | * Meridian upper: `AM or PM` 446 | * @return {string} 447 | */ 448 | A: function () { 449 | var n = fmt.G() < 12 ? 0 : 1; 450 | return vSettings.meridiem[n]; 451 | }, 452 | /** 453 | * Swatch Internet time: `000..999` 454 | * @return {string} 455 | */ 456 | B: function () { 457 | var H = vDate.getUTCHours() * HOUR, i = vDate.getUTCMinutes() * 60, s = vDate.getUTCSeconds(); 458 | return _lpad(Math.floor((H + i + s + HOUR) / 86.4) % 1000, 3); 459 | }, 460 | /** 461 | * 12-Hours: `1..12` 462 | * @return {number} 463 | */ 464 | g: function () { 465 | return fmt.G() % 12 || 12; 466 | }, 467 | /** 468 | * 24-Hours: `0..23` 469 | * @return {number} 470 | */ 471 | G: function () { 472 | return vDate.getHours(); 473 | }, 474 | /** 475 | * 12-Hours with leading 0: `01..12` 476 | * @return {string} 477 | */ 478 | h: function () { 479 | return _lpad(fmt.g(), 2); 480 | }, 481 | /** 482 | * 24-Hours w/leading 0: `00..23` 483 | * @return {string} 484 | */ 485 | H: function () { 486 | return _lpad(fmt.G(), 2); 487 | }, 488 | /** 489 | * Minutes w/leading 0: `00..59` 490 | * @return {string} 491 | */ 492 | i: function () { 493 | return _lpad(vDate.getMinutes(), 2); 494 | }, 495 | /** 496 | * Seconds w/leading 0: `00..59` 497 | * @return {string} 498 | */ 499 | s: function () { 500 | return _lpad(vDate.getSeconds(), 2); 501 | }, 502 | /** 503 | * Microseconds: `000000-999000` 504 | * @return {string} 505 | */ 506 | u: function () { 507 | return _lpad(vDate.getMilliseconds() * 1000, 6); 508 | }, 509 | 510 | ////////////// 511 | // TIMEZONE // 512 | ////////////// 513 | /** 514 | * Timezone identifier: `e.g. Atlantic/Azores, ...` 515 | * @return {string} 516 | */ 517 | e: function () { 518 | var str = /\((.*)\)/.exec(String(vDate))[1]; 519 | return str || 'Coordinated Universal Time'; 520 | }, 521 | /** 522 | * DST observed? `0 or 1` 523 | * @return {number} 524 | */ 525 | I: function () { 526 | var a = new Date(fmt.Y(), 0), c = Date.UTC(fmt.Y(), 0), 527 | b = new Date(fmt.Y(), 6), d = Date.UTC(fmt.Y(), 6); 528 | return ((a - c) !== (b - d)) ? 1 : 0; 529 | }, 530 | /** 531 | * Difference to GMT in hour format: `e.g. +0200` 532 | * @return {string} 533 | */ 534 | O: function () { 535 | var tzo = vDate.getTimezoneOffset(), a = Math.abs(tzo); 536 | return (tzo > 0 ? '-' : '+') + _lpad(Math.floor(a / 60) * 100 + a % 60, 4); 537 | }, 538 | /** 539 | * Difference to GMT with colon: `e.g. +02:00` 540 | * @return {string} 541 | */ 542 | P: function () { 543 | var O = fmt.O(); 544 | return (O.substr(0, 3) + ':' + O.substr(3, 2)); 545 | }, 546 | /** 547 | * Timezone abbreviation: `e.g. EST, MDT, ...` 548 | * @return {string} 549 | */ 550 | T: function () { 551 | var str = (String(vDate).match(self.tzParts) || [""]).pop().replace(self.tzClip, ""); 552 | return str || 'UTC'; 553 | }, 554 | /** 555 | * Timezone offset in seconds: `-43200...50400` 556 | * @return {number} 557 | */ 558 | Z: function () { 559 | return -vDate.getTimezoneOffset() * 60; 560 | }, 561 | 562 | //////////////////// 563 | // FULL DATE TIME // 564 | //////////////////// 565 | /** 566 | * ISO-8601 date 567 | * @return {string} 568 | */ 569 | c: function () { 570 | return 'Y-m-d\\TH:i:sP'.replace(backslash, doFormat); 571 | }, 572 | /** 573 | * RFC 2822 date 574 | * @return {string} 575 | */ 576 | r: function () { 577 | return 'D, d M Y H:i:s O'.replace(backslash, doFormat); 578 | }, 579 | /** 580 | * Seconds since UNIX epoch 581 | * @return {number} 582 | */ 583 | U: function () { 584 | return vDate.getTime() / 1000 || 0; 585 | } 586 | }; 587 | return doFormat(vChar, vChar); 588 | }, 589 | formatDate: function (vDate, vFormat) { 590 | var self = this, i, n, len, str, vChar, vDateStr = '', BACKSLASH = '\\'; 591 | if (typeof vDate === 'string') { 592 | vDate = self.parseDate(vDate, vFormat); 593 | if (!vDate) { 594 | return null; 595 | } 596 | } 597 | if (vDate instanceof Date) { 598 | len = vFormat.length; 599 | for (i = 0; i < len; i++) { 600 | vChar = vFormat.charAt(i); 601 | if (vChar === 'S' || vChar === BACKSLASH) { 602 | continue; 603 | } 604 | if (i > 0 && vFormat.charAt(i - 1) === BACKSLASH) { 605 | vDateStr += vChar; 606 | continue; 607 | } 608 | str = self.parseFormat(vChar, vDate); 609 | if (i !== (len - 1) && self.intParts.test(vChar) && vFormat.charAt(i + 1) === 'S') { 610 | n = parseInt(str) || 0; 611 | str += self.dateSettings.ordinal(n); 612 | } 613 | vDateStr += str; 614 | } 615 | return vDateStr; 616 | } 617 | return ''; 618 | } 619 | }; 620 | })(); 621 | --------------------------------------------------------------------------------