From 291c467193e8ac29e899cbd7cd8cf1633ded6a63 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Tue, 27 Feb 2018 10:05:11 -0500 Subject: [PATCH 01/27] Save progress --- inc/class-voicewp-setup.php | 18 +++++ inc/classes/class-settings.php | 130 +++++++++++++++++++++++++++++++++ inc/custom-fields.php | 56 ++++++++++++++ inc/traits/trait-singleton.php | 38 ++++++++++ voicewp.php | 6 ++ 5 files changed, 248 insertions(+) create mode 100644 inc/classes/class-settings.php create mode 100644 inc/custom-fields.php create mode 100644 inc/traits/trait-singleton.php diff --git a/inc/class-voicewp-setup.php b/inc/class-voicewp-setup.php index 9a0db46..4936adf 100644 --- a/inc/class-voicewp-setup.php +++ b/inc/class-voicewp-setup.php @@ -70,6 +70,24 @@ public function __construct() { foreach ( voicewp_news_post_types() as $post_type ) { add_action( 'publish_' . $post_type, array( $this, 'publish_clear_cache' ), 10, 2 ); } + + // Add settings. + new VoiceWp\Settings( + 'voicewp-settings-new', + 'tools.php', + __( 'Voice WP', 'voicewp' ), + [ + 'example' => [ + 'label' => 'Example', + ], + 'example-1' => [ + 'label' => 'Example 1', + ], + 'example-2' => [ + 'label' => 'Example 2', + ], + ] + ); } /** diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php new file mode 100644 index 0000000..a8da3bf --- /dev/null +++ b/inc/classes/class-settings.php @@ -0,0 +1,130 @@ +_settings_name = $settings_name; + $this->_settings_parent_page = $settings_parent_page; + $this->_settings_page_title = $settings_page_title; + $this->_settings = $settings; + + // Only get the settings once per page. + $this->_retrieved_settings = get_option( $this->_settings_name ); + + add_action( 'admin_menu', [ $this, 'add_settings_page' ] ); + add_action( 'admin_init', [ $this, 'add_settings' ] ); + } + + /** + * Add the settings page. + */ + public function add_settings_page() { + add_submenu_page( + $this->_settings_parent_page, + $this->_settings_page_title, + $this->_settings_page_title, + 'manage_options', + $this->_settings_name, + function () { + ?> +
+

_settings_page_title ); ?>

+ +
+ get_settings_group_name() ); ?> + _settings_name ); ?> + +
+
+ _settings ) ) { + return; + } + + register_setting( $this->get_settings_group_name(), $this->_settings_name ); + + add_settings_section( + $this->get_settings_section_name(), + '', + '', + $this->_settings_name + ); + + foreach ( (array) $this->_settings as $name => $setting ) { + add_settings_field( + $name, + $setting['label'], + function () use ( $name ) { + $this->render_text_field( $name ); + }, + $this->_settings_name, + $this->get_settings_section_name() + ); + } + } + + public function get_field( $field_name ) { + return $this->_settings[ $field_name ] ?? null; + } + + public function get_field_value( $field_name ) { + return $this->_retrieved_settings[ $field_name ] ?? null; + } + + public function get_settings_group_name() { + return $this->_settings_name . '-group'; + } + + public function get_settings_section_name() { + return $this->_settings_name . '-section'; + } + + public function render_text_field( $field_name ) { + $field = $this->get_field( $field_name ); + + if ( empty( $field ) ) { + return; + } + + // Get the field value. + $value = ''; + + // Get the field description. + $description = '

Description

'; + + return printf( + '%3$s', + esc_attr( $this->_settings_name . '[' . $field_name . ']' ), + esc_attr( $this->get_field_value( $field_name ) ), + $description + ); + } +} diff --git a/inc/custom-fields.php b/inc/custom-fields.php new file mode 100644 index 0000000..9eafe32 --- /dev/null +++ b/inc/custom-fields.php @@ -0,0 +1,56 @@ + +
+

Voice WP Settings

+ +
+ + + +
+
+ 'string', + 'description' => __( 'Site title.' ), + ) + ); +} ); diff --git a/inc/traits/trait-singleton.php b/inc/traits/trait-singleton.php new file mode 100644 index 0000000..d9b8ca1 --- /dev/null +++ b/inc/traits/trait-singleton.php @@ -0,0 +1,38 @@ +setup(); + } + return static::$instance; + } + + /** + * Setup the singleton. + */ + public function setup() { + // Silence. + } +} diff --git a/voicewp.php b/voicewp.php index 5caee3b..c41a2eb 100644 --- a/voicewp.php +++ b/voicewp.php @@ -23,6 +23,12 @@ function voicewp_deactivate() { flush_rewrite_rules(); } +// Singleton. +require_once VOICEWP_PATH . '/inc/traits/trait-singleton.php'; + +// Settings. +require_once VOICEWP_PATH . '/inc/classes/class-settings.php'; + /** * Compatibility requirements. */ From 419b1da94a6e4a26019673425df46c3f480020eb Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Tue, 27 Feb 2018 10:51:01 -0500 Subject: [PATCH 02/27] Improve docs --- inc/class-voicewp-setup.php | 5 +- inc/classes/class-settings.php | 211 ++++++++++++++++++++++++--------- inc/custom-fields.php | 56 --------- 3 files changed, 160 insertions(+), 112 deletions(-) delete mode 100644 inc/custom-fields.php diff --git a/inc/class-voicewp-setup.php b/inc/class-voicewp-setup.php index 4936adf..30bf43f 100644 --- a/inc/class-voicewp-setup.php +++ b/inc/class-voicewp-setup.php @@ -73,8 +73,8 @@ public function __construct() { // Add settings. new VoiceWp\Settings( + 'options', 'voicewp-settings-new', - 'tools.php', __( 'Voice WP', 'voicewp' ), [ 'example' => [ @@ -86,6 +86,9 @@ public function __construct() { 'example-2' => [ 'label' => 'Example 2', ], + ], + [ + 'parent_page' => 'tools.php', ] ); } diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index a8da3bf..d63d324 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -11,48 +11,96 @@ * Settings class use to create a new settings page. */ class Settings { - private $_settings_name = ''; + /** + * The setting type. + * + * @var string + */ + private $_type = ''; - private $_settings_parent_page = ''; + /** + * The setting name. + * + * @var string + */ + private $_name = ''; - private $_settings_page_title = ''; + /** + * The setting title. + * + * @var string + */ + private $_title = ''; - private $_settings = []; + /** + * The setting fields. + * + * @var array + */ + private $_fields = []; /** - * Setup the class. + * The setting args. + * + * @var string */ - public function __construct( $settings_name, $settings_parent_page, $settings_page_title, $settings ) { - $this->_settings_name = $settings_name; - $this->_settings_parent_page = $settings_parent_page; - $this->_settings_page_title = $settings_page_title; - $this->_settings = $settings; + private $_args = ''; - // Only get the settings once per page. - $this->_retrieved_settings = get_option( $this->_settings_name ); + /** + * Cached data if the field value. + * + * @var mixed + */ + private $_retrieved_data = null; - add_action( 'admin_menu', [ $this, 'add_settings_page' ] ); - add_action( 'admin_init', [ $this, 'add_settings' ] ); + /** + * Setup the class. + * + * @param string $type The settings type. + * @param string $name The settings name. + * @param string $title The settings title. + * @param array $fields The settings fields. + * @param array $args The settings args. + */ + public function __construct( $type, $name, $title, $fields, $args = [] ) { + $this->_type = $type; + $this->_name = $name; + $this->_title = $title; + $this->_fields = $fields; + $this->_args = $args; + + // Prime the cache. + $this->get_data(); + + if ( 'options' === $this->_type ) { + add_action( 'admin_menu', [ $this, 'add_options_page' ] ); + add_action( 'admin_init', [ $this, 'add_options_fields' ] ); + } } /** * Add the settings page. */ - public function add_settings_page() { + public function add_options_page() { + // No parent page. + if ( empty( $this->_args['parent_page'] ) ) { + return; + } + add_submenu_page( - $this->_settings_parent_page, - $this->_settings_page_title, - $this->_settings_page_title, + $this->_args['parent_page'], + $this->_title, + $this->_title, 'manage_options', - $this->_settings_name, + $this->_name, function () { ?>
-

_settings_page_title ); ?>

+

_title ); ?>

- get_settings_group_name() ); ?> - _settings_name ); ?> + get_options_group_name() ); ?> + _name ); ?>
@@ -64,67 +112,120 @@ function () { /** * Add the settings to the page. */ - public function add_settings() { - if ( empty( $this->_settings ) ) { + public function add_options_fields() { + if ( empty( $this->_fields ) ) { return; } - register_setting( $this->get_settings_group_name(), $this->_settings_name ); + register_setting( $this->get_options_group_name(), $this->_name ); add_settings_section( - $this->get_settings_section_name(), + $this->get_options_section_name(), '', '', - $this->_settings_name + $this->_name ); - foreach ( (array) $this->_settings as $name => $setting ) { + foreach ( (array) $this->_fields as $name => $setting ) { add_settings_field( $name, $setting['label'], function () use ( $name ) { - $this->render_text_field( $name ); + $this->render_field( $name ); }, - $this->_settings_name, - $this->get_settings_section_name() + $this->_name, + $this->get_options_section_name() ); } } - public function get_field( $field_name ) { - return $this->_settings[ $field_name ] ?? null; - } + /** + * Renders the field. + * + * @param string $field_name The field name to be rendered. + */ + public function render_field( $field_name ) { + $field = $this->get_field( $field_name ); - public function get_field_value( $field_name ) { - return $this->_retrieved_settings[ $field_name ] ?? null; - } + if ( empty( $field ) ) { + return; + } - public function get_settings_group_name() { - return $this->_settings_name . '-group'; + // Render the correct field type. + switch ( $field['type'] ) { + case 'text': + default: + printf( + '%3$s', + esc_attr( $this->_name . '[' . $field_name . ']' ), + esc_attr( $this->get_field_value( $field_name ) ), + ! empty( $field['description'] ) ? '

' . esc_html( $field['description'] ) . '

' : '' + ); + break; + } } - public function get_settings_section_name() { - return $this->_settings_name . '-section'; + /** + * Get all fields. + * + * @return array The field array. + */ + public function get_fields() { + return $this->_fields; } - public function render_text_field( $field_name ) { - $field = $this->get_field( $field_name ); + /** + * Get a field by name. + * + * @param string $field_name The field name. + * @return array The field array. + */ + public function get_field( $field_name ) { + return wp_parse_args( $this->get_fields()[ $field_name ], [ + 'type' => 'text', + ] ) ?? null; + } - if ( empty( $field ) ) { - return; + /** + * Get the entire field data. + * + * @return mixed The field data. + */ + public function get_data() { + if ( null === $this->_retrieved_data ) { + switch ( $this->_type ) { + case 'options': + $this->_retrieved_data = get_option( $this->_name ); + break; + } } + } - // Get the field value. - $value = ''; + /** + * Get the field value by name. + * + * @param string $field_name The field name. + * @return mixed The field value. + */ + public function get_field_value( $field_name ) { + return $this->_retrieved_data[ $field_name ] ?? null; + } - // Get the field description. - $description = '

Description

'; + /** + * Get the field group name used when registering the settings. + * + * @return string The field group name. + */ + public function get_options_group_name() { + return $this->_name . '-group'; + } - return printf( - '%3$s', - esc_attr( $this->_settings_name . '[' . $field_name . ']' ), - esc_attr( $this->get_field_value( $field_name ) ), - $description - ); + /** + * Get the field section name used when registering the settings. + * + * @return string The field section name. + */ + public function get_options_section_name() { + return $this->_name . '-section'; } } diff --git a/inc/custom-fields.php b/inc/custom-fields.php deleted file mode 100644 index 9eafe32..0000000 --- a/inc/custom-fields.php +++ /dev/null @@ -1,56 +0,0 @@ - -
-

Voice WP Settings

- -
- - - -
-
- 'string', - 'description' => __( 'Site title.' ), - ) - ); -} ); From 50eacbed2cb6baf57a0ca21846902cb3b861c010 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Tue, 27 Feb 2018 11:13:47 -0500 Subject: [PATCH 03/27] Save progress --- inc/class-voicewp-setup.php | 54 +++++++++++++++++++++------- inc/classes/class-settings.php | 65 ++++++++++++++++++++++++++++------ 2 files changed, 95 insertions(+), 24 deletions(-) diff --git a/inc/class-voicewp-setup.php b/inc/class-voicewp-setup.php index 30bf43f..8f2162a 100644 --- a/inc/class-voicewp-setup.php +++ b/inc/class-voicewp-setup.php @@ -76,20 +76,48 @@ public function __construct() { 'options', 'voicewp-settings-new', __( 'Voice WP', 'voicewp' ), - [ - 'example' => [ - 'label' => 'Example', - ], - 'example-1' => [ - 'label' => 'Example 1', - ], - 'example-2' => [ - 'label' => 'Example 2', - ], - ], - [ + array( + 'skill_name' => array( + 'label' => __( 'Skill name', 'voicewp' ), + 'description' => __( 'Optional name of skill. If empty, site name will be used instead.', 'voicewp' ), + 'attributes' => array( + 'style' => 'width: 95%;', + ), + ), + 'launch_request' => array( + 'type' => 'textarea', + 'label' => __( 'Welcome message', 'voicewp' ), + 'description' => __( 'This is the message a person hears when they open your skill with an utterance such as "Alexa, open {your skill name}"', 'voicewp' ), + 'default_value' => __( 'Welcome to the {put your skill name here} Skill. This skill allows you to listen to content from {your site name}. You can ask questions like: What are the latest articles? ... Now, what can I help you with.', 'voicewp' ), + 'attributes' => array( + 'style' => 'width: 95%; height: 70px;', + ), + ), + 'help_intent' => array( + 'type' => 'textarea', + 'label' => __( 'Help message', 'voicewp' ), + 'description' => __( "This is the message a person hears when they ask your skill for 'help'", 'voicewp' ), + 'default_value' => __( "{put your skill name here} provides you with the latest content from {your site name}. You can ask me for the latest articles, and then select an item from the list by saying, for example, 'read the 3rd article' Or you can also say exit... What can I help you with?", 'voicewp' ), + 'attributes' => array( 'style' => 'width: 95%; height: 70px;' ), + ), + 'list_prompt' => array( + 'type' => 'textarea', + 'label' => __( 'List Prompt', 'voicewp' ), + 'description' => __( 'This message prompts the user to select a piece of content to be read after hearing the headlines.', 'voicewp' ), + 'default_value' => __( 'Which article would you like to hear?', 'voicewp' ), + 'attributes' => array( 'style' => 'width: 95%; height: 50px;' ), + ), + 'stop_intent' => array( + 'type' => 'textarea', + 'label' => __( 'Stop message', 'voicewp' ), + 'description' => __( 'You can optionally provide a message when a person is done with your skill.', 'voicewp' ), + 'default_value' => __( 'Thanks for listening!', 'voicewp' ), + 'attributes' => array( 'style' => 'width: 95%; height: 50px;' ), + ), + ), + array( 'parent_page' => 'tools.php', - ] + ) ); } diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index d63d324..f5bd971 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -37,7 +37,7 @@ class Settings { * * @var array */ - private $_fields = []; + private $_fields = array(); /** * The setting args. @@ -62,7 +62,7 @@ class Settings { * @param array $fields The settings fields. * @param array $args The settings args. */ - public function __construct( $type, $name, $title, $fields, $args = [] ) { + public function __construct( $type, $name, $title, $fields, $args = array() ) { $this->_type = $type; $this->_name = $name; $this->_title = $title; @@ -73,8 +73,8 @@ public function __construct( $type, $name, $title, $fields, $args = [] ) { $this->get_data(); if ( 'options' === $this->_type ) { - add_action( 'admin_menu', [ $this, 'add_options_page' ] ); - add_action( 'admin_init', [ $this, 'add_options_fields' ] ); + add_action( 'admin_menu', array( $this, 'add_options_page' ) ); + add_action( 'admin_init', array( $this, 'add_options_fields' ) ); } } @@ -153,18 +153,48 @@ public function render_field( $field_name ) { // Render the correct field type. switch ( $field['type'] ) { + case 'textarea': + printf( + '%4$s', + esc_attr( $this->_name . '[' . $field_name . ']' ), + esc_attr( $this->get_field_value( $field ) ), + ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '', + ! empty( $field['description'] ) ? '

' . esc_html( $field['description'] ) . '

' : '' + ); + break; case 'text': default: printf( - '%3$s', + '%4$s', esc_attr( $this->_name . '[' . $field_name . ']' ), - esc_attr( $this->get_field_value( $field_name ) ), + esc_attr( $this->get_field_value( $field ) ), + ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '', ! empty( $field['description'] ) ? '

' . esc_html( $field['description'] ) . '

' : '' ); break; } } + /** + * Get all of the field attributes. + * + * @param array $attributes The field attributes. + * @return string $html The field attributes HTML. + */ + public function add_attributes( $attributes ) { + if ( empty( $attributes ) ) { + return ''; + } + + $html = ''; + + foreach ( (array) $attributes as $key => $content ) { + $html .= esc_attr( $key ) . '="' . esc_attr( $content ) . '" '; + } + + return $html; + } + /** * Get all fields. * @@ -181,9 +211,10 @@ public function get_fields() { * @return array The field array. */ public function get_field( $field_name ) { - return wp_parse_args( $this->get_fields()[ $field_name ], [ + return wp_parse_args( $this->get_fields()[ $field_name ], array( + 'name' => $field_name, 'type' => 'text', - ] ) ?? null; + ) ) ?? null; } /** @@ -204,11 +235,23 @@ public function get_data() { /** * Get the field value by name. * - * @param string $field_name The field name. + * @param array $field The field. * @return mixed The field value. */ - public function get_field_value( $field_name ) { - return $this->_retrieved_data[ $field_name ] ?? null; + public function get_field_value( $field ) { + // No name. + if ( empty( $field['name'] ) ) { + return null; + } + + $value = $this->_retrieved_data[ $field['name'] ] ?? null; + + // If empty use the default value. + if ( empty( $value ) && ! empty( $field['default_value'] ) ) { + $value = $field['default_value']; + } + + return $value; } /** From d1d0d52b562fd6aaa3fab31090483bd5d8996c27 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Tue, 27 Feb 2018 11:25:36 -0500 Subject: [PATCH 04/27] Refactor functions --- inc/classes/class-settings.php | 45 +++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index f5bd971..7cf0aa1 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -155,24 +155,30 @@ public function render_field( $field_name ) { switch ( $field['type'] ) { case 'textarea': printf( - '%4$s', - esc_attr( $this->_name . '[' . $field_name . ']' ), - esc_attr( $this->get_field_value( $field ) ), - ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '', - ! empty( $field['description'] ) ? '

' . esc_html( $field['description'] ) . '

' : '' - ); + '', + esc_attr( $this->get_field_name( $field ) ), + esc_html( $this->get_field_value( $field ) ), + ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '' // Escaped internally. + ); // WPCS XSS okay. break; case 'text': default: printf( - '%4$s', - esc_attr( $this->_name . '[' . $field_name . ']' ), + '', + esc_attr( $this->get_field_name( $field ) ), esc_attr( $this->get_field_value( $field ) ), - ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '', - ! empty( $field['description'] ) ? '

' . esc_html( $field['description'] ) . '

' : '' - ); + ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '' // Escaped internally. + ); // WPCS XSS okay. break; } + + // The field description. + if ( ! empty( $field['description'] ) ) { + printf( + '

%1$s

', + esc_html( $field['description'] ) + ); + } } /** @@ -233,7 +239,22 @@ public function get_data() { } /** - * Get the field value by name. + * Get the field name. + * + * @param array $field The field. + * @return mixed The field name. + */ + public function get_field_name( $field ) { + // No name. + if ( empty( $field['name'] ) ) { + return null; + } + + return $this->_name . "[{$field['name']}]"; + } + + /** + * Get the field value. * * @param array $field The field. * @return mixed The field value. From b35a35c5083c704d282f057197a0b4d346c51f4b Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Tue, 27 Feb 2018 13:00:39 -0500 Subject: [PATCH 05/27] Save progress --- inc/class-voicewp-setup.php | 46 ++++++++++++++++++++++++ inc/classes/class-settings.php | 65 ++++++++++++++++++++-------------- 2 files changed, 84 insertions(+), 27 deletions(-) diff --git a/inc/class-voicewp-setup.php b/inc/class-voicewp-setup.php index 8f2162a..0756b65 100644 --- a/inc/class-voicewp-setup.php +++ b/inc/class-voicewp-setup.php @@ -71,6 +71,15 @@ public function __construct() { add_action( 'publish_' . $post_type, array( $this, 'publish_clear_cache' ), 10, 2 ); } + $news_post_types = voicewp_news_post_types(); + // All public taxonomies associated with news post types. Could be abstracted into a function. + $eligible_news_taxonomy_objects = array_filter( + get_taxonomies( array( 'public' => true ), 'objects' ), + function ( $taxonomy ) use ( $news_post_types ) { + return ( $taxonomy->label && array_intersect( $news_post_types, $taxonomy->object_type ) ); + } + ); + // Add settings. new VoiceWp\Settings( 'options', @@ -114,6 +123,43 @@ public function __construct() { 'default_value' => __( 'Thanks for listening!', 'voicewp' ), 'attributes' => array( 'style' => 'width: 95%; height: 50px;' ), ), + 'news_id' => array( + 'label' => __( 'News skill ID', 'voicewp' ), + 'description' => __( 'Add the application ID given by Amazon', 'voicewp' ), + 'attributes' => array( + 'style' => 'width: 95%;', + ), + ), + 'latest_taxonomies' => array( + 'label' => __( 'Allow people to ask for content from specific:', 'voicewp' ), + 'options' => wp_list_pluck( $eligible_news_taxonomy_objects, 'label', 'name' ), + ), + 'user_dictionary' => array( + 'type' => 'group', + 'label' => __( 'Word Pronunciation Substitutions', 'voicewp' ), + 'description' => __( "This allows you to define a global dictionary of words, phrases, abbreviations that Alexa should pronounce a certain way. For example, perhaps every occurrance of the state abreviation 'TN' should be pronounced as 'Tennessee', or 'NYC should be read as 'New York City' or the chemical 'Mg' read as 'Magnesium'. ", 'voicewp' ), + 'children' => array( + 'dictionary' => array( + 'type' => 'group', + 'label' => __( 'Phrase / Word / Abbreviation', 'voicewp' ), + 'description' => __( "This allows you to define a global dictionary of words, phrases, abbreviations that Alexa should pronounce a certain way. For example, perhaps every occurrance of the state abreviation 'TN' should be pronounced as 'Tennessee', or 'NYC should be read as 'New York City' or the chemical 'Mg' read as 'Magnesium'. ", 'voicewp' ), + 'children' => array( + 'search' => array( + 'description' => __( 'Phrase to pronounce differently', 'voicewp' ), + 'attributes' => array( + 'style' => 'width: 45%;', + ), + ), + 'replace' => array( + 'description' => __( 'How the above phrase should be pronounced.', 'voicewp' ), + 'attributes' => array( + 'style' => 'width: 45%;', + ), + ), + ), + ), + ), + ), ), array( 'parent_page' => 'tools.php', diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index 7cf0aa1..f1779e5 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -126,12 +126,36 @@ public function add_options_fields() { $this->_name ); - foreach ( (array) $this->_fields as $name => $setting ) { + $this->add_options_field( $this->_name, $this->_fields ); + } + + public function add_options_field( $option_name, $fields ) { + if ( empty( $fields ) || ! is_array( $fields ) ) { + return; + } + + foreach ( $fields as $name => $field ) { + // Ensure we have the default strucuture. + $field = wp_parse_args( $field, array( + 'name' => $name, + 'type' => 'text', + ) ); + + // Group field. + if ( + 'group' === $field['type'] + && ! empty( $field['children'] ) + && is_array( $field['children'] ) + ) { + $this->add_options_field( $this->get_field_name( $option_name, $field ), $field['children'] ); + continue; + } + add_settings_field( $name, - $setting['label'], - function () use ( $name ) { - $this->render_field( $name ); + $field['label'] ?? '', + function () use ( $option_name, $field ) { + $this->render_field( $this->get_field_name( $option_name, $field ), $field ); }, $this->_name, $this->get_options_section_name() @@ -142,11 +166,10 @@ function () use ( $name ) { /** * Renders the field. * - * @param string $field_name The field name to be rendered. + * @param string $name The field name. + * @param string $field The field to be rendered. */ - public function render_field( $field_name ) { - $field = $this->get_field( $field_name ); - + public function render_field( $name, $field ) { if ( empty( $field ) ) { return; } @@ -156,7 +179,7 @@ public function render_field( $field_name ) { case 'textarea': printf( '', - esc_attr( $this->get_field_name( $field ) ), + esc_attr( $name ), esc_html( $this->get_field_value( $field ) ), ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '' // Escaped internally. ); // WPCS XSS okay. @@ -165,7 +188,7 @@ public function render_field( $field_name ) { default: printf( '', - esc_attr( $this->get_field_name( $field ) ), + esc_attr( $name ), esc_attr( $this->get_field_value( $field ) ), ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '' // Escaped internally. ); // WPCS XSS okay. @@ -210,19 +233,6 @@ public function get_fields() { return $this->_fields; } - /** - * Get a field by name. - * - * @param string $field_name The field name. - * @return array The field array. - */ - public function get_field( $field_name ) { - return wp_parse_args( $this->get_fields()[ $field_name ], array( - 'name' => $field_name, - 'type' => 'text', - ) ) ?? null; - } - /** * Get the entire field data. * @@ -241,16 +251,17 @@ public function get_data() { /** * Get the field name. * - * @param array $field The field. + * @param string $name The field name. + * @param array $field The field. * @return mixed The field name. */ - public function get_field_name( $field ) { + public function get_field_name( $name, $field ) { // No name. - if ( empty( $field['name'] ) ) { + if ( empty( $name ) && empty( $field['name'] ) ) { return null; } - return $this->_name . "[{$field['name']}]"; + return $name . "[{$field['name']}]"; } /** From 62bd30421e7063584aa649268795b908c6eabc25 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Tue, 27 Feb 2018 13:38:57 -0500 Subject: [PATCH 06/27] Save progress --- inc/class-voicewp-setup.php | 91 ++++++++++++++++++++++++++++++++++ inc/classes/class-settings.php | 6 +++ 2 files changed, 97 insertions(+) diff --git a/inc/class-voicewp-setup.php b/inc/class-voicewp-setup.php index 0756b65..756573b 100644 --- a/inc/class-voicewp-setup.php +++ b/inc/class-voicewp-setup.php @@ -160,11 +160,102 @@ function ( $taxonomy ) use ( $news_post_types ) { ), ), ), + 'interaction_model' => array( + 'type' => 'group', + 'label' => __( 'Interaction Model', 'voicewp' ), + 'children' => array( + 'news_intent_schema' => array( + 'type' => 'textarea', + 'label' => __( 'The Intent Schema for your News skill. Add this to your news skill in the Amazon developer console.', 'voicewp' ), + 'escape' => array( 'label' => 'wp_kses_post' ), + 'default_value' => file_get_contents( __DIR__ . '/../speechAssets/news/IntentSchema.json', FILE_USE_INCLUDE_PATH ), + 'attributes' => array( + 'readonly' => 'readonly', + 'style' => 'width: 100%; height: 300px; font-family: monospace;', + ), + ), + 'custom_slot_types' => array( + 'type' => 'group', + 'label' => __( 'Custom Slot Types', 'voicewp' ), + 'children' => array( + 'custom_slot_type_children' => array( + 'type' => 'group', + 'description' => __( 'These slot types must be added to your news skill in the Amazon developer portal.', 'voicewp' ), + 'children' => array( + 'VOICEWP_POST_NUMBER_WORD' => array( + 'label' => __( 'Type', 'voicewp' ), + 'default_value' => 'VOICEWP_POST_NUMBER_WORD', + 'attributes' => array( + 'readonly' => 'readonly', + 'style' => 'width: 50%; font-family: monospace;', + ), + ), + 'VOICEWP_POST_NUMBER_WORD_values' => array( + 'type' => 'textarea', + 'label' => __( 'Values', 'voicewp' ), + 'default_value' => "first\nsecond\nthird\nfourth\nfifth", + 'attributes' => array( + 'readonly' => 'readonly', + 'style' => 'width: 50%; height: 150px; font-family: monospace;', + ), + ), + 'VOICEWP_TERM_NAME' => array( + 'label' => __( 'Type', 'voicewp' ), + 'default_value' => 'VOICEWP_TERM_NAME', + 'attributes' => array( + 'readonly' => 'readonly', + 'style' => 'width: 50%; font-family: monospace;', + ), + ), + 'VOICEWP_TERM_NAME_values' => array( + 'type' => 'textarea', + 'label' => __( 'Values', 'voicewp' ), + 'default_value' => implode( + "\n", + // Generate sample terms from all available taxonomies. + // We want someone to add this slot even if they haven't + // turned on taxonomies so it's already there if they do. + array_values( array_unique( array_map( 'strtolower', wp_list_pluck( get_terms( array( + 'number' => 100, + 'order' => 'DESC', + 'orderby' => 'count', + 'taxonomy' => array_values( wp_list_pluck( $eligible_news_taxonomy_objects, 'name' ) ), + ) ), 'name' ) ) ) ) + ), + 'attributes' => array( + 'readonly' => 'readonly', + 'style' => 'width: 50%; height: 150px; font-family: monospace;', + ), + ), + ), + ), + ), + ), + 'news_utterances' => array( + 'type' => 'textarea', + 'label' => __( 'Here\'s a starting point for your skill\'s Sample Utterances. You can add these to your news skill in the Amazon developer console.', 'voicewp' ), + 'escape' => array( 'label' => 'wp_kses_post' ), + 'default_value' => file_get_contents( __DIR__ . '/../speechAssets/news/Utterances.txt', FILE_USE_INCLUDE_PATH ), + 'skip_save' => true, + 'attributes' => array( + 'readonly' => 'readonly', + 'style' => 'width: 100%; height: 300px;', + ), + ), + ), + ), ), array( 'parent_page' => 'tools.php', ) ); + + // Add settings. + // new VoiceWp\Settings( + // 'options', + // 'voicewp-settings-new', + // __( 'Voice WP', 'voicewp' ) + // ); } /** diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index f1779e5..9e80506 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -129,6 +129,12 @@ public function add_options_fields() { $this->add_options_field( $this->_name, $this->_fields ); } + /** + * Add an options field. + * + * @param string $option_name The option name. + * @param array $fields The array of fields. + */ public function add_options_field( $option_name, $fields ) { if ( empty( $fields ) || ! is_array( $fields ) ) { return; From 03a71a76e60e310e47cf996eaed72abb053e7127 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Tue, 27 Feb 2018 14:15:47 -0500 Subject: [PATCH 07/27] Fix group sections --- inc/classes/class-settings.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index 9e80506..36cd29e 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -134,8 +134,9 @@ public function add_options_fields() { * * @param string $option_name The option name. * @param array $fields The array of fields. + * @param bool $is_group Whether or not this is a group. */ - public function add_options_field( $option_name, $fields ) { + public function add_options_field( $option_name, $fields, $is_group = false ) { if ( empty( $fields ) || ! is_array( $fields ) ) { return; } @@ -153,10 +154,21 @@ public function add_options_field( $option_name, $fields ) { && ! empty( $field['children'] ) && is_array( $field['children'] ) ) { - $this->add_options_field( $this->get_field_name( $option_name, $field ), $field['children'] ); + $full_option_name = $this->get_field_name( $option_name, $field ); + + add_settings_section( + $full_option_name . '-section', + $field['label'] ?? '', + '', + $this->_name + ); + + $this->add_options_field( $full_option_name, $field['children'], true ); continue; } + $full_option_name = $this->get_field_name( $option_name, $field ); + add_settings_field( $name, $field['label'] ?? '', @@ -164,7 +176,7 @@ function () use ( $option_name, $field ) { $this->render_field( $this->get_field_name( $option_name, $field ), $field ); }, $this->_name, - $this->get_options_section_name() + $is_group ? $option_name . '-section' : $this->get_options_section_name() ); } } From 9cab8e3fd37da2a21189e9fdb6863ff3ca758fe2 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Tue, 27 Feb 2018 14:56:44 -0500 Subject: [PATCH 08/27] Add support for checkboxes --- inc/class-voicewp-setup.php | 3 ++- inc/classes/class-settings.php | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/inc/class-voicewp-setup.php b/inc/class-voicewp-setup.php index 756573b..f5d91d3 100644 --- a/inc/class-voicewp-setup.php +++ b/inc/class-voicewp-setup.php @@ -131,6 +131,7 @@ function ( $taxonomy ) use ( $news_post_types ) { ), ), 'latest_taxonomies' => array( + 'type' => 'checkboxes', 'label' => __( 'Allow people to ask for content from specific:', 'voicewp' ), 'options' => wp_list_pluck( $eligible_news_taxonomy_objects, 'label', 'name' ), ), @@ -141,7 +142,7 @@ function ( $taxonomy ) use ( $news_post_types ) { 'children' => array( 'dictionary' => array( 'type' => 'group', - 'label' => __( 'Phrase / Word / Abbreviation', 'voicewp' ), + 'limit' => 0, 'description' => __( "This allows you to define a global dictionary of words, phrases, abbreviations that Alexa should pronounce a certain way. For example, perhaps every occurrance of the state abreviation 'TN' should be pronounced as 'Tennessee', or 'NYC should be read as 'New York City' or the chemical 'Mg' read as 'Magnesium'. ", 'voicewp' ), 'children' => array( 'search' => array( diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index 36cd29e..31ed476 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -146,6 +146,7 @@ public function add_options_field( $option_name, $fields, $is_group = false ) { $field = wp_parse_args( $field, array( 'name' => $name, 'type' => 'text', + 'limit' => 1, ) ); // Group field. @@ -194,6 +195,21 @@ public function render_field( $name, $field ) { // Render the correct field type. switch ( $field['type'] ) { + case 'checkboxes': + if ( empty( $field['options'] ) ) { + break; + } + + foreach ( $field['options'] as $value => $label ) { + printf( + '

', + esc_attr( $name ), + esc_html( $this->get_field_value( $field ) ), + ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '', // Escaped internally. + esc_html( $label ) + ); // WPCS XSS okay. + } + break; case 'textarea': printf( '', From 29bebf3d6053bf92d3e68d54f9f14e88c7e21fa4 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Tue, 27 Feb 2018 20:52:50 -0500 Subject: [PATCH 09/27] Move settings --- inc/class-voicewp-setup.php | 187 ------------------------------------ inc/fields.php | 180 ++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 187 deletions(-) diff --git a/inc/class-voicewp-setup.php b/inc/class-voicewp-setup.php index f5d91d3..9a0db46 100644 --- a/inc/class-voicewp-setup.php +++ b/inc/class-voicewp-setup.php @@ -70,193 +70,6 @@ public function __construct() { foreach ( voicewp_news_post_types() as $post_type ) { add_action( 'publish_' . $post_type, array( $this, 'publish_clear_cache' ), 10, 2 ); } - - $news_post_types = voicewp_news_post_types(); - // All public taxonomies associated with news post types. Could be abstracted into a function. - $eligible_news_taxonomy_objects = array_filter( - get_taxonomies( array( 'public' => true ), 'objects' ), - function ( $taxonomy ) use ( $news_post_types ) { - return ( $taxonomy->label && array_intersect( $news_post_types, $taxonomy->object_type ) ); - } - ); - - // Add settings. - new VoiceWp\Settings( - 'options', - 'voicewp-settings-new', - __( 'Voice WP', 'voicewp' ), - array( - 'skill_name' => array( - 'label' => __( 'Skill name', 'voicewp' ), - 'description' => __( 'Optional name of skill. If empty, site name will be used instead.', 'voicewp' ), - 'attributes' => array( - 'style' => 'width: 95%;', - ), - ), - 'launch_request' => array( - 'type' => 'textarea', - 'label' => __( 'Welcome message', 'voicewp' ), - 'description' => __( 'This is the message a person hears when they open your skill with an utterance such as "Alexa, open {your skill name}"', 'voicewp' ), - 'default_value' => __( 'Welcome to the {put your skill name here} Skill. This skill allows you to listen to content from {your site name}. You can ask questions like: What are the latest articles? ... Now, what can I help you with.', 'voicewp' ), - 'attributes' => array( - 'style' => 'width: 95%; height: 70px;', - ), - ), - 'help_intent' => array( - 'type' => 'textarea', - 'label' => __( 'Help message', 'voicewp' ), - 'description' => __( "This is the message a person hears when they ask your skill for 'help'", 'voicewp' ), - 'default_value' => __( "{put your skill name here} provides you with the latest content from {your site name}. You can ask me for the latest articles, and then select an item from the list by saying, for example, 'read the 3rd article' Or you can also say exit... What can I help you with?", 'voicewp' ), - 'attributes' => array( 'style' => 'width: 95%; height: 70px;' ), - ), - 'list_prompt' => array( - 'type' => 'textarea', - 'label' => __( 'List Prompt', 'voicewp' ), - 'description' => __( 'This message prompts the user to select a piece of content to be read after hearing the headlines.', 'voicewp' ), - 'default_value' => __( 'Which article would you like to hear?', 'voicewp' ), - 'attributes' => array( 'style' => 'width: 95%; height: 50px;' ), - ), - 'stop_intent' => array( - 'type' => 'textarea', - 'label' => __( 'Stop message', 'voicewp' ), - 'description' => __( 'You can optionally provide a message when a person is done with your skill.', 'voicewp' ), - 'default_value' => __( 'Thanks for listening!', 'voicewp' ), - 'attributes' => array( 'style' => 'width: 95%; height: 50px;' ), - ), - 'news_id' => array( - 'label' => __( 'News skill ID', 'voicewp' ), - 'description' => __( 'Add the application ID given by Amazon', 'voicewp' ), - 'attributes' => array( - 'style' => 'width: 95%;', - ), - ), - 'latest_taxonomies' => array( - 'type' => 'checkboxes', - 'label' => __( 'Allow people to ask for content from specific:', 'voicewp' ), - 'options' => wp_list_pluck( $eligible_news_taxonomy_objects, 'label', 'name' ), - ), - 'user_dictionary' => array( - 'type' => 'group', - 'label' => __( 'Word Pronunciation Substitutions', 'voicewp' ), - 'description' => __( "This allows you to define a global dictionary of words, phrases, abbreviations that Alexa should pronounce a certain way. For example, perhaps every occurrance of the state abreviation 'TN' should be pronounced as 'Tennessee', or 'NYC should be read as 'New York City' or the chemical 'Mg' read as 'Magnesium'. ", 'voicewp' ), - 'children' => array( - 'dictionary' => array( - 'type' => 'group', - 'limit' => 0, - 'description' => __( "This allows you to define a global dictionary of words, phrases, abbreviations that Alexa should pronounce a certain way. For example, perhaps every occurrance of the state abreviation 'TN' should be pronounced as 'Tennessee', or 'NYC should be read as 'New York City' or the chemical 'Mg' read as 'Magnesium'. ", 'voicewp' ), - 'children' => array( - 'search' => array( - 'description' => __( 'Phrase to pronounce differently', 'voicewp' ), - 'attributes' => array( - 'style' => 'width: 45%;', - ), - ), - 'replace' => array( - 'description' => __( 'How the above phrase should be pronounced.', 'voicewp' ), - 'attributes' => array( - 'style' => 'width: 45%;', - ), - ), - ), - ), - ), - ), - 'interaction_model' => array( - 'type' => 'group', - 'label' => __( 'Interaction Model', 'voicewp' ), - 'children' => array( - 'news_intent_schema' => array( - 'type' => 'textarea', - 'label' => __( 'The Intent Schema for your News skill. Add this to your news skill in the Amazon developer console.', 'voicewp' ), - 'escape' => array( 'label' => 'wp_kses_post' ), - 'default_value' => file_get_contents( __DIR__ . '/../speechAssets/news/IntentSchema.json', FILE_USE_INCLUDE_PATH ), - 'attributes' => array( - 'readonly' => 'readonly', - 'style' => 'width: 100%; height: 300px; font-family: monospace;', - ), - ), - 'custom_slot_types' => array( - 'type' => 'group', - 'label' => __( 'Custom Slot Types', 'voicewp' ), - 'children' => array( - 'custom_slot_type_children' => array( - 'type' => 'group', - 'description' => __( 'These slot types must be added to your news skill in the Amazon developer portal.', 'voicewp' ), - 'children' => array( - 'VOICEWP_POST_NUMBER_WORD' => array( - 'label' => __( 'Type', 'voicewp' ), - 'default_value' => 'VOICEWP_POST_NUMBER_WORD', - 'attributes' => array( - 'readonly' => 'readonly', - 'style' => 'width: 50%; font-family: monospace;', - ), - ), - 'VOICEWP_POST_NUMBER_WORD_values' => array( - 'type' => 'textarea', - 'label' => __( 'Values', 'voicewp' ), - 'default_value' => "first\nsecond\nthird\nfourth\nfifth", - 'attributes' => array( - 'readonly' => 'readonly', - 'style' => 'width: 50%; height: 150px; font-family: monospace;', - ), - ), - 'VOICEWP_TERM_NAME' => array( - 'label' => __( 'Type', 'voicewp' ), - 'default_value' => 'VOICEWP_TERM_NAME', - 'attributes' => array( - 'readonly' => 'readonly', - 'style' => 'width: 50%; font-family: monospace;', - ), - ), - 'VOICEWP_TERM_NAME_values' => array( - 'type' => 'textarea', - 'label' => __( 'Values', 'voicewp' ), - 'default_value' => implode( - "\n", - // Generate sample terms from all available taxonomies. - // We want someone to add this slot even if they haven't - // turned on taxonomies so it's already there if they do. - array_values( array_unique( array_map( 'strtolower', wp_list_pluck( get_terms( array( - 'number' => 100, - 'order' => 'DESC', - 'orderby' => 'count', - 'taxonomy' => array_values( wp_list_pluck( $eligible_news_taxonomy_objects, 'name' ) ), - ) ), 'name' ) ) ) ) - ), - 'attributes' => array( - 'readonly' => 'readonly', - 'style' => 'width: 50%; height: 150px; font-family: monospace;', - ), - ), - ), - ), - ), - ), - 'news_utterances' => array( - 'type' => 'textarea', - 'label' => __( 'Here\'s a starting point for your skill\'s Sample Utterances. You can add these to your news skill in the Amazon developer console.', 'voicewp' ), - 'escape' => array( 'label' => 'wp_kses_post' ), - 'default_value' => file_get_contents( __DIR__ . '/../speechAssets/news/Utterances.txt', FILE_USE_INCLUDE_PATH ), - 'skip_save' => true, - 'attributes' => array( - 'readonly' => 'readonly', - 'style' => 'width: 100%; height: 300px;', - ), - ), - ), - ), - ), - array( - 'parent_page' => 'tools.php', - ) - ); - - // Add settings. - // new VoiceWp\Settings( - // 'options', - // 'voicewp-settings-new', - // __( 'Voice WP', 'voicewp' ) - // ); } /** diff --git a/inc/fields.php b/inc/fields.php index 5e7d23e..27edd02 100644 --- a/inc/fields.php +++ b/inc/fields.php @@ -440,3 +440,183 @@ function voicewp_briefing_category_url() { $fm->add_term_meta_box( __( 'Flash Briefing URL', 'voicewp' ), array( 'voicewp-briefing-category' ) ); } add_action( 'fm_term_voicewp-briefing-category', 'voicewp_briefing_category_url' ); + +$news_post_types = voicewp_news_post_types(); +// All public taxonomies associated with news post types. Could be abstracted into a function. +$eligible_news_taxonomy_objects = array_filter( + get_taxonomies( array( 'public' => true ), 'objects' ), + function ( $taxonomy ) use ( $news_post_types ) { + return ( $taxonomy->label && array_intersect( $news_post_types, $taxonomy->object_type ) ); + } +); + +// Add Option settings. +$option_settings = new VoiceWp\Settings( + 'options', + 'voicewp-settings-new', + __( 'Voice WP', 'voicewp' ), + array( + 'skill_name' => array( + 'label' => __( 'Skill name', 'voicewp' ), + 'description' => __( 'Optional name of skill. If empty, site name will be used instead.', 'voicewp' ), + 'attributes' => array( + 'style' => 'width: 95%;', + ), + ), + 'launch_request' => array( + 'type' => 'textarea', + 'label' => __( 'Welcome message', 'voicewp' ), + 'description' => __( 'This is the message a person hears when they open your skill with an utterance such as "Alexa, open {your skill name}"', 'voicewp' ), + 'default_value' => __( 'Welcome to the {put your skill name here} Skill. This skill allows you to listen to content from {your site name}. You can ask questions like: What are the latest articles? ... Now, what can I help you with.', 'voicewp' ), + 'attributes' => array( + 'style' => 'width: 95%; height: 70px;', + ), + ), + 'help_intent' => array( + 'type' => 'textarea', + 'label' => __( 'Help message', 'voicewp' ), + 'description' => __( "This is the message a person hears when they ask your skill for 'help'", 'voicewp' ), + 'default_value' => __( "{put your skill name here} provides you with the latest content from {your site name}. You can ask me for the latest articles, and then select an item from the list by saying, for example, 'read the 3rd article' Or you can also say exit... What can I help you with?", 'voicewp' ), + 'attributes' => array( 'style' => 'width: 95%; height: 70px;' ), + ), + 'list_prompt' => array( + 'type' => 'textarea', + 'label' => __( 'List Prompt', 'voicewp' ), + 'description' => __( 'This message prompts the user to select a piece of content to be read after hearing the headlines.', 'voicewp' ), + 'default_value' => __( 'Which article would you like to hear?', 'voicewp' ), + 'attributes' => array( 'style' => 'width: 95%; height: 50px;' ), + ), + 'stop_intent' => array( + 'type' => 'textarea', + 'label' => __( 'Stop message', 'voicewp' ), + 'description' => __( 'You can optionally provide a message when a person is done with your skill.', 'voicewp' ), + 'default_value' => __( 'Thanks for listening!', 'voicewp' ), + 'attributes' => array( 'style' => 'width: 95%; height: 50px;' ), + ), + 'news_id' => array( + 'label' => __( 'News skill ID', 'voicewp' ), + 'description' => __( 'Add the application ID given by Amazon', 'voicewp' ), + 'attributes' => array( + 'style' => 'width: 95%;', + ), + ), + 'latest_taxonomies' => array( + 'type' => 'checkboxes', + 'label' => __( 'Allow people to ask for content from specific:', 'voicewp' ), + 'options' => wp_list_pluck( $eligible_news_taxonomy_objects, 'label', 'name' ), + ), + 'user_dictionary' => array( + 'type' => 'group', + 'label' => __( 'Word Pronunciation Substitutions', 'voicewp' ), + 'description' => __( "This allows you to define a global dictionary of words, phrases, abbreviations that Alexa should pronounce a certain way. For example, perhaps every occurrance of the state abreviation 'TN' should be pronounced as 'Tennessee', or 'NYC should be read as 'New York City' or the chemical 'Mg' read as 'Magnesium'. ", 'voicewp' ), + 'children' => array( + 'dictionary' => array( + 'type' => 'group', + 'limit' => 0, + 'description' => __( "This allows you to define a global dictionary of words, phrases, abbreviations that Alexa should pronounce a certain way. For example, perhaps every occurrance of the state abreviation 'TN' should be pronounced as 'Tennessee', or 'NYC should be read as 'New York City' or the chemical 'Mg' read as 'Magnesium'. ", 'voicewp' ), + 'children' => array( + 'search' => array( + 'description' => __( 'Phrase to pronounce differently', 'voicewp' ), + 'attributes' => array( + 'style' => 'width: 45%;', + ), + ), + 'replace' => array( + 'description' => __( 'How the above phrase should be pronounced.', 'voicewp' ), + 'attributes' => array( + 'style' => 'width: 45%;', + ), + ), + ), + ), + ), + ), + 'interaction_model' => array( + 'type' => 'group', + 'label' => __( 'Interaction Model', 'voicewp' ), + 'children' => array( + 'news_intent_schema' => array( + 'type' => 'textarea', + 'label' => __( 'The Intent Schema for your News skill. Add this to your news skill in the Amazon developer console.', 'voicewp' ), + 'escape' => array( 'label' => 'wp_kses_post' ), + 'default_value' => file_get_contents( __DIR__ . '/../speechAssets/news/IntentSchema.json', FILE_USE_INCLUDE_PATH ), + 'attributes' => array( + 'readonly' => 'readonly', + 'style' => 'width: 100%; height: 300px; font-family: monospace;', + ), + ), + 'custom_slot_types' => array( + 'type' => 'group', + 'label' => __( 'Custom Slot Types', 'voicewp' ), + 'children' => array( + 'custom_slot_type_children' => array( + 'type' => 'group', + 'description' => __( 'These slot types must be added to your news skill in the Amazon developer portal.', 'voicewp' ), + 'children' => array( + 'VOICEWP_POST_NUMBER_WORD' => array( + 'label' => __( 'Type', 'voicewp' ), + 'default_value' => 'VOICEWP_POST_NUMBER_WORD', + 'attributes' => array( + 'readonly' => 'readonly', + 'style' => 'width: 50%; font-family: monospace;', + ), + ), + 'VOICEWP_POST_NUMBER_WORD_values' => array( + 'type' => 'textarea', + 'label' => __( 'Values', 'voicewp' ), + 'default_value' => "first\nsecond\nthird\nfourth\nfifth", + 'attributes' => array( + 'readonly' => 'readonly', + 'style' => 'width: 50%; height: 150px; font-family: monospace;', + ), + ), + 'VOICEWP_TERM_NAME' => array( + 'label' => __( 'Type', 'voicewp' ), + 'default_value' => 'VOICEWP_TERM_NAME', + 'attributes' => array( + 'readonly' => 'readonly', + 'style' => 'width: 50%; font-family: monospace;', + ), + ), + 'VOICEWP_TERM_NAME_values' => array( + 'type' => 'textarea', + 'label' => __( 'Values', 'voicewp' ), + 'default_value' => implode( + "\n", + // Generate sample terms from all available taxonomies. + // We want someone to add this slot even if they haven't + // turned on taxonomies so it's already there if they do. + array_values( array_unique( array_map( 'strtolower', wp_list_pluck( get_terms( array( + 'number' => 100, + 'order' => 'DESC', + 'orderby' => 'count', + 'taxonomy' => array_values( wp_list_pluck( $eligible_news_taxonomy_objects, 'name' ) ), + ) ), 'name' ) ) ) ) + ), + 'attributes' => array( + 'readonly' => 'readonly', + 'style' => 'width: 50%; height: 150px; font-family: monospace;', + ), + ), + ), + ), + ), + ), + 'news_utterances' => array( + 'type' => 'textarea', + 'label' => __( 'Here\'s a starting point for your skill\'s Sample Utterances. You can add these to your news skill in the Amazon developer console.', 'voicewp' ), + 'escape' => array( 'label' => 'wp_kses_post' ), + 'default_value' => file_get_contents( __DIR__ . '/../speechAssets/news/Utterances.txt', FILE_USE_INCLUDE_PATH ), + 'skip_save' => true, + 'attributes' => array( + 'readonly' => 'readonly', + 'style' => 'width: 100%; height: 300px;', + ), + ), + ), + ), + ), + array( + 'parent_page' => 'tools.php', + ) +); From 8ca7c4a2b427c537557349c52c36a23f688beefd Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Wed, 28 Feb 2018 08:41:27 -0500 Subject: [PATCH 10/27] Fix checkboxes --- inc/classes/class-settings.php | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index 31ed476..c29cfb3 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -143,11 +143,7 @@ public function add_options_field( $option_name, $fields, $is_group = false ) { foreach ( $fields as $name => $field ) { // Ensure we have the default strucuture. - $field = wp_parse_args( $field, array( - 'name' => $name, - 'type' => 'text', - 'limit' => 1, - ) ); + $field = $this->sanitize_field( $name, $field ); // Group field. if ( @@ -182,6 +178,21 @@ function () use ( $option_name, $field ) { } } + /** + * Sanitize a field to ensure it has a certain shape. + * + * @param string $name The field name. + * @param array $field The field array. + * @return array The sanitized field array. + */ + public function sanitize_field( string $name, array $field ) : array { + return wp_parse_args( $field, array( + 'name' => $name, + 'type' => 'text', + 'limit' => 1, + ) ); + } + /** * Renders the field. * @@ -200,11 +211,14 @@ public function render_field( $name, $field ) { break; } + $current_values = $this->get_field_value( $field ); + foreach ( $field['options'] as $value => $label ) { printf( - '

', + '

', esc_attr( $name ), - esc_html( $this->get_field_value( $field ) ), + esc_attr( $value ), + in_array( $value, $current_values, true ) ? 'checked="checked"' : '', ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '', // Escaped internally. esc_html( $label ) ); // WPCS XSS okay. From 2548e3c891803298ee59d4ea5752f1e4714d9902 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Wed, 28 Feb 2018 09:55:43 -0500 Subject: [PATCH 11/27] Save Progress --- client/js/admin/settings.js | 35 +++++++++ inc/classes/class-settings.php | 132 ++++++++++++++++++++++++++++++--- inc/fields.php | 1 - voicewp.php | 1 + 4 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 client/js/admin/settings.js diff --git a/client/js/admin/settings.js b/client/js/admin/settings.js new file mode 100644 index 0000000..28f0138 --- /dev/null +++ b/client/js/admin/settings.js @@ -0,0 +1,35 @@ +voicewp_add_another = function( $element ) { + const repeaterGroup = $element.parent().parent(); + const cloneElement = repeaterGroup.find( '.voicewp-wrapper' ).last().clone(); + + cloneElement.insertBefore( $element.parent() ); +} + +voicewp_remove = function( $element ) { + $wrapper = $element.parents( '.voicewp-wrapper' ).first(); + $element.parents( '.voicewp-item' ).first().remove(); + voicewp_renumber( $wrapper ); +} + +jQuery( document ).ready( function ( $ ) { + // Handle adding another element. + $( document ).on( 'click', '.voicewp-add-another', function( e ) { + e.preventDefault(); + voicewp_add_another( $( this ) ); + } ); + + // Handle remove events + $( document ).on( 'click', '.voicewpjs-remove', function( e ) { + e.preventDefault(); + voicewp_remove( $( this ) ); + } ); + + // Special handling for Option pages. + if ( 0 !== $( 'form[action="options.php"]' ).length ) { + + // Move table into repeating group. + $( '.voicewpjs-options-repeating-group' ).each( function () { + $( this ).next( 'table.form-table' ).prependTo( $( this ).find( '.voicewp-wrapper' ) ); + } ); + } +} ); diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index c29cfb3..1494b3e 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -72,12 +72,22 @@ public function __construct( $type, $name, $title, $fields, $args = array() ) { // Prime the cache. $this->get_data(); + // Add scripts. + add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) ); + if ( 'options' === $this->_type ) { add_action( 'admin_menu', array( $this, 'add_options_page' ) ); add_action( 'admin_init', array( $this, 'add_options_fields' ) ); } } + /** + * Add the scripts. + */ + public function admin_enqueue_scripts() { + wp_enqueue_script( 'voicewp-settings-js', VOICEWP_URL . '/client/js/admin/settings.js', [ 'jquery' ] ); + } + /** * Add the settings page. */ @@ -156,7 +166,17 @@ public function add_options_field( $option_name, $fields, $is_group = false ) { add_settings_section( $full_option_name . '-section', $field['label'] ?? '', - '', + function () use ( $full_option_name, $field ) { + // Add description. + if ( ! empty( $field['description'] ) ) { + echo '

' . esc_html( $field['description'] ) . '

'; + } + + // Check if this is a repeater field. + if ( 0 === $field['limit'] || 1 < $field['limit'] ) { + echo '
' . $this->add_another( $full_option_name, $field ) . '
'; // WPCS: XSS okay. + } + }, $this->_name ); @@ -164,8 +184,6 @@ public function add_options_field( $option_name, $fields, $is_group = false ) { continue; } - $full_option_name = $this->get_field_name( $option_name, $field ); - add_settings_field( $name, $field['label'] ?? '', @@ -186,11 +204,29 @@ function () use ( $option_name, $field ) { * @return array The sanitized field array. */ public function sanitize_field( string $name, array $field ) : array { - return wp_parse_args( $field, array( - 'name' => $name, - 'type' => 'text', - 'limit' => 1, + $field = wp_parse_args( $field, array( + 'name' => $name, + 'type' => 'text', + 'limit' => 1, + 'add_more_label' => __( 'Add field', 'voicewp' ), + 'is_group' => false, + 'group_repeater' => false, ) ); + + if ( isset( $field['type'] ) && 'group' === $field['type'] ) { + $field['is_group'] = true; + $field['add_more_label'] = __( 'Add group', 'voicewp' ); + + if ( 0 === $field['limit'] || 1 < $field['limit'] ) { + if ( ! empty( $field['children'] ) && is_array( $field['children'] ) ) { + foreach ( $field['children'] as &$child ) { + $child['group_repeater'] = true; + } + } + } + } + + return $field; } /** @@ -204,6 +240,8 @@ public function render_field( $name, $field ) { return; } + $field_html = ''; + // Render the correct field type. switch ( $field['type'] ) { case 'checkboxes': @@ -214,7 +252,7 @@ public function render_field( $name, $field ) { $current_values = $this->get_field_value( $field ); foreach ( $field['options'] as $value => $label ) { - printf( + $field_html .= sprintf( '

', esc_attr( $name ), esc_attr( $value ), @@ -225,7 +263,7 @@ public function render_field( $name, $field ) { } break; case 'textarea': - printf( + $field_html .= sprintf( '', esc_attr( $name ), esc_html( $this->get_field_value( $field ) ), @@ -234,7 +272,7 @@ public function render_field( $name, $field ) { break; case 'text': default: - printf( + $field_html .= sprintf( '', esc_attr( $name ), esc_attr( $this->get_field_value( $field ) ), @@ -245,11 +283,76 @@ public function render_field( $name, $field ) { // The field description. if ( ! empty( $field['description'] ) ) { - printf( + $field_html .= sprintf( '

%1$s

', esc_html( $field['description'] ) ); } + + // Wrap the field with tools as needed. + $field_html = $this->wrap_with_multi_tools( $field, $field_html ); + + echo $field_html; // WPCS XSS okay. + } + + /** + * Wrap a chunk of HTML with "remove" and "move" buttons if applicable. + * + * @param array $field The current field. + * @param string $html HTML to wrap. + * @param array $classes An array of classes. + * @return string Wrapped HTML. + */ + public function wrap_with_multi_tools( $field, $html, $classes = array() ) { + $classes[] = 'voicewpjs-removable'; + $out = sprintf( '
', implode( ' ', $classes ) ); + + $out .= '
'; + $out .= $html; + $out .= '
'; + + if ( 0 === $field['limit'] ) { + $out .= $this->get_remove_handle(); + } + + $out .= '
'; + return $out; + } + + /** + * Return HTML for the remove handle (multi-tools); a separate function to override. + * + * @return string + */ + public function get_remove_handle() { + return sprintf( '%1$s', esc_attr__( 'Remove', 'voicewp' ) ); + } + + /** + * Generates HTML for the "Add Another" button. + * + * @param string $name The field name. + * @param array $field The field. + * @return string Button HTML. + */ + public function add_another( $name, $field ) { + $classes = array( 'voicewp-add-another', 'voicewp-' . $name . '-add-another', 'button-secondary' ); + if ( empty( $field['add_more_label'] ) ) { + $field['add_more_label'] = $field['is_group'] ? __( 'Add group', 'voicewp' ) : __( 'Add field', 'voicewp' ); + } + + $out = '
'; + $out .= sprintf( + '', + esc_attr( implode( ' ', $classes ) ), + esc_attr( $field['add_more_label'] ), + esc_attr( 'fm_add_another_' . $name ), + esc_attr( $name ), + intval( $field['limit'] ) + ); + + $out .= '
'; + return $out; } /** @@ -309,7 +412,12 @@ public function get_field_name( $name, $field ) { return null; } - return $name . "[{$field['name']}]"; + $repeater = ''; + if ( ! empty( $field['group_repeater'] ) ) { + $repeater = '[0]'; + } + + return $name . $repeater . "[{$field['name']}]"; } /** diff --git a/inc/fields.php b/inc/fields.php index 27edd02..26ef8a1 100644 --- a/inc/fields.php +++ b/inc/fields.php @@ -508,7 +508,6 @@ function ( $taxonomy ) use ( $news_post_types ) { 'user_dictionary' => array( 'type' => 'group', 'label' => __( 'Word Pronunciation Substitutions', 'voicewp' ), - 'description' => __( "This allows you to define a global dictionary of words, phrases, abbreviations that Alexa should pronounce a certain way. For example, perhaps every occurrance of the state abreviation 'TN' should be pronounced as 'Tennessee', or 'NYC should be read as 'New York City' or the chemical 'Mg' read as 'Magnesium'. ", 'voicewp' ), 'children' => array( 'dictionary' => array( 'type' => 'group', diff --git a/voicewp.php b/voicewp.php index c41a2eb..6352333 100644 --- a/voicewp.php +++ b/voicewp.php @@ -11,6 +11,7 @@ */ define( 'VOICEWP_PATH', dirname( __FILE__ ) ); +define( 'VOICEWP_URL', plugins_url( '/', __FILE__ ) ); register_activation_hook( __FILE__, 'voicewp_activate' ); function voicewp_activate() { From a54a31a63b87751bbd53d9301c732c0a4b14240e Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Wed, 28 Feb 2018 13:04:29 -0500 Subject: [PATCH 12/27] Fix group values --- client/js/admin/settings.js | 2 +- inc/classes/class-settings.php | 165 +++++++++++++++++++++++++-------- 2 files changed, 127 insertions(+), 40 deletions(-) diff --git a/client/js/admin/settings.js b/client/js/admin/settings.js index 28f0138..0abde3c 100644 --- a/client/js/admin/settings.js +++ b/client/js/admin/settings.js @@ -29,7 +29,7 @@ jQuery( document ).ready( function ( $ ) { // Move table into repeating group. $( '.voicewpjs-options-repeating-group' ).each( function () { - $( this ).next( 'table.form-table' ).prependTo( $( this ).find( '.voicewp-wrapper' ) ); + // $( this ).next( 'table.form-table' ).prependTo( $( this ).find( '.voicewp-wrapper' ) ); } ); } } ); diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index 1494b3e..0178f52 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -66,7 +66,7 @@ public function __construct( $type, $name, $title, $fields, $args = array() ) { $this->_type = $type; $this->_name = $name; $this->_title = $title; - $this->_fields = $fields; + $this->_fields = $this->sanitize_fields( $fields ); $this->_args = $args; // Prime the cache. @@ -136,24 +136,23 @@ public function add_options_fields() { $this->_name ); - $this->add_options_field( $this->_name, $this->_fields ); + $this->add_options_field( $this->_name, $this->_fields, false, $this->get_data() ); } /** * Add an options field. * - * @param string $option_name The option name. - * @param array $fields The array of fields. - * @param bool $is_group Whether or not this is a group. + * @param string $option_name The option name. + * @param array $fields The array of fields. + * @param bool $is_group Whether or not this is a group. + * @param mixed $current_value The current value of the field. */ - public function add_options_field( $option_name, $fields, $is_group = false ) { + public function add_options_field( $option_name, $fields, $is_group = false, $current_value = null ) { if ( empty( $fields ) || ! is_array( $fields ) ) { return; } foreach ( $fields as $name => $field ) { - // Ensure we have the default strucuture. - $field = $this->sanitize_field( $name, $field ); // Group field. if ( @@ -171,16 +170,26 @@ function () use ( $full_option_name, $field ) { if ( ! empty( $field['description'] ) ) { echo '

' . esc_html( $field['description'] ) . '

'; } - - // Check if this is a repeater field. - if ( 0 === $field['limit'] || 1 < $field['limit'] ) { - echo '
' . $this->add_another( $full_option_name, $field ) . '
'; // WPCS: XSS okay. - } }, $this->_name ); - $this->add_options_field( $full_option_name, $field['children'], true ); + // Only add the children as separate fields if this group is a not + // repeater group. + if ( 0 === $field['limit'] || 1 < $field['limit'] ) { + add_settings_field( + $name, + $field['label'] ?? '', + function () use ( $full_option_name, $field, $current_value ) { + $this->render_group( $full_option_name, $field, $current_value ); + }, + $this->_name, + $full_option_name . '-section' + ); + } else { + $this->add_options_field( $full_option_name, $field['children'], true, $this->get_field_value( $field, $current_value ) ); + } + continue; } @@ -196,6 +205,32 @@ function () use ( $option_name, $field ) { } } + /** + * Santizie the fields. + * + * @param array $fields The current fields. + * @return array The sanitized fields. + */ + public function sanitize_fields( array $fields ) { + if ( empty( $fields ) || ! is_array( $fields ) ) { + return []; + } + + foreach ( $fields as $name => &$field ) { + $field = $this->sanitize_field( $name, $field ); + + if ( + 'group' === $field['type'] + && ! empty( $field['children'] ) + && is_array( $field['children'] ) + ) { + $field['children'] = $this->sanitize_fields( $field['children'] ); + } + } + + return $fields; + } + /** * Sanitize a field to ensure it has a certain shape. * @@ -208,25 +243,73 @@ public function sanitize_field( string $name, array $field ) : array { 'name' => $name, 'type' => 'text', 'limit' => 1, - 'add_more_label' => __( 'Add field', 'voicewp' ), + 'add_more_label' => ( isset( $field['type'] ) && 'group' === $field['type'] ) ? __( 'Add group', 'voicewp' ) : __( 'Add field', 'voicewp' ), 'is_group' => false, - 'group_repeater' => false, ) ); - if ( isset( $field['type'] ) && 'group' === $field['type'] ) { - $field['is_group'] = true; - $field['add_more_label'] = __( 'Add group', 'voicewp' ); + if ( 'group' === $field['type'] ) { + $field['is_group'] = true; + } - if ( 0 === $field['limit'] || 1 < $field['limit'] ) { - if ( ! empty( $field['children'] ) && is_array( $field['children'] ) ) { - foreach ( $field['children'] as &$child ) { - $child['group_repeater'] = true; - } + return $field; + } + + /** + * Renders the field group. + * + * @param string $name The field name. + * @param string $group The field to be rendered. + * @param array $group_value The current group value. + */ + public function render_group( $name, $group, $group_value ) { + if ( empty( $group['children'] ) || ! is_array( $group['children'] ) ) { + return ''; + } + + // Get the current group value. + $group_value = $this->get_field_value( $group, $group_value ); + + $repeater = ( 0 === $group['limit'] || 1 < $group['limit'] ); + + // Check if this is a repeater field. + if ( $repeater ) { + echo '
'; + + if ( ! empty( $group_value ) && is_array( $group_value ) ) { + foreach ( $group_value as $index => $value ) { + $this->render_group_children( $name . "[{$index}]", $group, $value ); } + } else { + $this->render_group_children( $name . '[0]', $group, $group_value ); } + + echo $this->add_another( $name, $group ) . '
'; // WPCS: XSS okay. + } else { + $this->render_group_children( $name, $group, $group_value ); } + } - return $field; + /** + * Render the children fields in a group. + * + * @param string $name The field name. + * @param array $group The group. + * @param array $value The group value. + */ + public function render_group_children( $name, $group, $value ) { + foreach ( $group['children'] as $child ) { + if ( $child['is_group'] ) { + $this->render_group( $name, $child, $value ); + continue; + } + + // Get the proper child value. + $child['value'] = $this->get_field_value( $child, $value ); + + echo '
'; + $this->render_field( $this->get_field_name( $name, $child ), $child ); + echo '
'; + } } /** @@ -240,6 +323,11 @@ public function render_field( $name, $field ) { return; } + // If no field value is passed then try to get it. + if ( ! isset( $field['value'] ) ) { + $field['value'] = $this->get_field_value( $field ); + } + $field_html = ''; // Render the correct field type. @@ -249,14 +337,12 @@ public function render_field( $name, $field ) { break; } - $current_values = $this->get_field_value( $field ); - foreach ( $field['options'] as $value => $label ) { $field_html .= sprintf( '

', esc_attr( $name ), esc_attr( $value ), - in_array( $value, $current_values, true ) ? 'checked="checked"' : '', + ! empty( $field['value'] ) && in_array( $value, $field['value'], true ) ? 'checked="checked"' : '', ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '', // Escaped internally. esc_html( $label ) ); // WPCS XSS okay. @@ -266,7 +352,7 @@ public function render_field( $name, $field ) { $field_html .= sprintf( '', esc_attr( $name ), - esc_html( $this->get_field_value( $field ) ), + esc_html( $field['value'] ), ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '' // Escaped internally. ); // WPCS XSS okay. break; @@ -275,9 +361,10 @@ public function render_field( $name, $field ) { $field_html .= sprintf( '', esc_attr( $name ), - esc_attr( $this->get_field_value( $field ) ), + esc_attr( $field['value'] ), ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '' // Escaped internally. ); // WPCS XSS okay. + break; } @@ -412,27 +499,27 @@ public function get_field_name( $name, $field ) { return null; } - $repeater = ''; - if ( ! empty( $field['group_repeater'] ) ) { - $repeater = '[0]'; - } - - return $name . $repeater . "[{$field['name']}]"; + return $name . "[{$field['name']}]"; } /** * Get the field value. * - * @param array $field The field. + * @param array $field The field. + * @param array $search_data The data to search through. * @return mixed The field value. */ - public function get_field_value( $field ) { + public function get_field_value( $field, $search_data = null ) { // No name. if ( empty( $field['name'] ) ) { return null; } - $value = $this->_retrieved_data[ $field['name'] ] ?? null; + if ( null === $search_data ) { + $search_data = $this->_retrieved_data; + } + + $value = $search_data[ $field['name'] ] ?? null; // If empty use the default value. if ( empty( $value ) && ! empty( $field['default_value'] ) ) { From bc289a6e631b7aae70add443e12effdb116e03e5 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Wed, 28 Feb 2018 16:12:47 -0500 Subject: [PATCH 13/27] Update group functionality --- client/css/admin/settings.css | 44 ++++++++++++++++++++++++++++++++ client/js/admin/settings.js | 44 +++++++++++++++++++++++--------- inc/classes/class-settings.php | 46 ++++++++++++++++++++++++---------- 3 files changed, 109 insertions(+), 25 deletions(-) create mode 100644 client/css/admin/settings.css diff --git a/client/css/admin/settings.css b/client/css/admin/settings.css new file mode 100644 index 0000000..260bcac --- /dev/null +++ b/client/css/admin/settings.css @@ -0,0 +1,44 @@ +.voicewp-wrapper { + border: 1px solid #ddd; + padding: 12px; + background-color: #fff; + margin-bottom: 12px; + border: solid 1px #dfdfdf; + -webkit-box-shadow: 0 1px 1px rgba(0,0,0,.04); + box-shadow: 0 1px 1px rgba(0,0,0,.04); +} + +.voicewpjs-removable { + position: relative; +} + +.voicewpjs-remove:before { + font-family: dashicons; + font-size: 15px; + line-height: 1; + color: #a0a5aa; +} + +.voicewpjs-remove { + margin: 4px 0 0 5px; + display: block; + height: 15px; + width: 15px; + float: left; + text-decoration: none; + position: absolute; + top: 0; + right: 0; +} + +.voicewpjs-remove:before { + content: "\f153"; +} + +.voicewpjs-remove:hover { + cursor: pointer; +} + +.voicewpjs-remove:hover:before { + color: #d54e21; +} diff --git a/client/js/admin/settings.js b/client/js/admin/settings.js index 0abde3c..7ca20c9 100644 --- a/client/js/admin/settings.js +++ b/client/js/admin/settings.js @@ -1,14 +1,43 @@ voicewp_add_another = function( $element ) { const repeaterGroup = $element.parent().parent(); const cloneElement = repeaterGroup.find( '.voicewp-wrapper' ).last().clone(); + const items = repeaterGroup.find('.voicewp-wrapper'); cloneElement.insertBefore( $element.parent() ); + + // Clear data. + const fields = cloneElement.find('.voicewp-item'); + fields.each( function() { + jQuery( this ).val( '' ); + }); + + voicewp_renumber( $element.closest( '.voicewpjs-repeating-group' ) ); } voicewp_remove = function( $element ) { - $wrapper = $element.parents( '.voicewp-wrapper' ).first(); - $element.parents( '.voicewp-item' ).first().remove(); - voicewp_renumber( $wrapper ); + const removedEl = $element.parent().parent(); + const wrapper = removedEl.closest( '.voicewpjs-repeating-group' ); + removedEl.remove(); + voicewp_renumber( wrapper ); +} + +voicewp_renumber = function( $element ) { + const repeaterName = $element.data( 'repeater-name' ); + const items = $element.find( '.voicewp-wrapper' ); + + // Update name and clear data. + items.each( function( index, value ) { + const fields = jQuery( this ).find('.voicewp-item'); + + fields.each( function() { + let groupName = + repeaterName.replace( 'voicewp-index', Math.max( 0, index ) ) + + '[' + jQuery( this ).data( 'base-name' ) + ']'; + + jQuery( this ).attr( 'name', groupName ); + jQuery( this ).attr( 'id', groupName ); + }); + }); } jQuery( document ).ready( function ( $ ) { @@ -23,13 +52,4 @@ jQuery( document ).ready( function ( $ ) { e.preventDefault(); voicewp_remove( $( this ) ); } ); - - // Special handling for Option pages. - if ( 0 !== $( 'form[action="options.php"]' ).length ) { - - // Move table into repeating group. - $( '.voicewpjs-options-repeating-group' ).each( function () { - // $( this ).next( 'table.form-table' ).prependTo( $( this ).find( '.voicewp-wrapper' ) ); - } ); - } } ); diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index 0178f52..aa012e3 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -85,6 +85,7 @@ public function __construct( $type, $name, $title, $fields, $args = array() ) { * Add the scripts. */ public function admin_enqueue_scripts() { + wp_enqueue_style( 'voicewp-settings-css', VOICEWP_URL . '/client/css/admin/settings.css' ); wp_enqueue_script( 'voicewp-settings-js', VOICEWP_URL . '/client/js/admin/settings.js', [ 'jquery' ] ); } @@ -273,7 +274,7 @@ public function render_group( $name, $group, $group_value ) { // Check if this is a repeater field. if ( $repeater ) { - echo '
'; + echo '
'; if ( ! empty( $group_value ) && is_array( $group_value ) ) { foreach ( $group_value as $index => $value ) { @@ -297,19 +298,25 @@ public function render_group( $name, $group, $group_value ) { * @param array $value The group value. */ public function render_group_children( $name, $group, $value ) { + ob_start(); foreach ( $group['children'] as $child ) { if ( $child['is_group'] ) { $this->render_group( $name, $child, $value ); continue; } - // Get the proper child value. $child['value'] = $this->get_field_value( $child, $value ); - echo '
'; $this->render_field( $this->get_field_name( $name, $child ), $child ); - echo '
'; } + + if ( 0 === $group['limit'] || 1 < $group['limit'] ) { + $repeater_html = $this->wrap_with_multi_tools( $group, ob_get_clean() ); + } + + echo '
'; + echo $repeater_html; // WPCS: XSS okay. + echo '
'; } /** @@ -323,9 +330,17 @@ public function render_field( $name, $field ) { return; } - // If no field value is passed then try to get it. if ( ! isset( $field['value'] ) ) { - $field['value'] = $this->get_field_value( $field ); + $field_value = $this->get_field_value( $field ); + } else { + $field_value = $field['value']; + } + + // Get the base name. + if ( 0 === $field['limit'] || 1 < $field['limit'] ) { + $base_name = ''; + } else { + $base_name = $field['name']; } $field_html = ''; @@ -339,10 +354,11 @@ public function render_field( $name, $field ) { foreach ( $field['options'] as $value => $label ) { $field_html .= sprintf( - '

', + '

', esc_attr( $name ), + esc_attr( $base_name ), esc_attr( $value ), - ! empty( $field['value'] ) && in_array( $value, $field['value'], true ) ? 'checked="checked"' : '', + ! empty( $field_value ) && in_array( $value, $field_value, true ) ? 'checked="checked"' : '', ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '', // Escaped internally. esc_html( $label ) ); // WPCS XSS okay. @@ -350,18 +366,20 @@ public function render_field( $name, $field ) { break; case 'textarea': $field_html .= sprintf( - '', + '', esc_attr( $name ), - esc_html( $field['value'] ), + esc_attr( $base_name ), + esc_html( $field_value ), ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '' // Escaped internally. ); // WPCS XSS okay. break; case 'text': default: $field_html .= sprintf( - '', + '', esc_attr( $name ), - esc_attr( $field['value'] ), + esc_attr( $base_name ), + esc_attr( $field_value ), ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '' // Escaped internally. ); // WPCS XSS okay. @@ -377,7 +395,9 @@ public function render_field( $name, $field ) { } // Wrap the field with tools as needed. - $field_html = $this->wrap_with_multi_tools( $field, $field_html ); + if ( 0 === $field['limit'] || 1 < $field['limit'] ) { + $field_html = $this->wrap_with_multi_tools( $field, $field_html ); + } echo $field_html; // WPCS XSS okay. } From ab6a7003f64e9b04e1b4ce7d1bd55a68f1585819 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Wed, 28 Feb 2018 18:45:56 -0500 Subject: [PATCH 14/27] Remove option settings --- inc/classes/class-settings.php | 30 +++--- inc/fields.php | 190 +-------------------------------- 2 files changed, 16 insertions(+), 204 deletions(-) diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index aa012e3..8103406 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -12,11 +12,11 @@ */ class Settings { /** - * The setting type. + * The setting context. * * @var string */ - private $_type = ''; + private $_context = ''; /** * The setting name. @@ -56,18 +56,18 @@ class Settings { /** * Setup the class. * - * @param string $type The settings type. - * @param string $name The settings name. - * @param string $title The settings title. - * @param array $fields The settings fields. - * @param array $args The settings args. + * @param string $context The settings type. + * @param string $name The settings name. + * @param string $title The settings title. + * @param array $fields The settings fields. + * @param array $args The settings args. */ - public function __construct( $type, $name, $title, $fields, $args = array() ) { - $this->_type = $type; - $this->_name = $name; - $this->_title = $title; - $this->_fields = $this->sanitize_fields( $fields ); - $this->_args = $args; + public function __construct( $context, $name, $title, $fields, $args = array() ) { + $this->_context = $context; + $this->_name = $name; + $this->_title = $title; + $this->_fields = $this->sanitize_fields( $fields ); + $this->_args = $args; // Prime the cache. $this->get_data(); @@ -75,7 +75,7 @@ public function __construct( $type, $name, $title, $fields, $args = array() ) { // Add scripts. add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) ); - if ( 'options' === $this->_type ) { + if ( 'options' === $this->_context ) { add_action( 'admin_menu', array( $this, 'add_options_page' ) ); add_action( 'admin_init', array( $this, 'add_options_fields' ) ); } @@ -498,7 +498,7 @@ public function get_fields() { */ public function get_data() { if ( null === $this->_retrieved_data ) { - switch ( $this->_type ) { + switch ( $this->_context ) { case 'options': $this->_retrieved_data = get_option( $this->_name ); break; diff --git a/inc/fields.php b/inc/fields.php index 26ef8a1..31d7636 100644 --- a/inc/fields.php +++ b/inc/fields.php @@ -216,194 +216,6 @@ function voicewp_fm_skill_fact_quote() { } add_action( 'fm_post_voicewp-skill', 'voicewp_fm_skill_fact_quote' ); -/** - * Create a settings page for the news/post consumption skill - */ -function voicewp_fm_alexa_settings() { - $readonly = array( 'readonly' => 'readonly' ); - - $news_post_types = voicewp_news_post_types(); - // All public taxonomies associated with news post types. Could be abstracted into a function. - $eligible_news_taxonomy_objects = array_filter( - get_taxonomies( array( 'public' => true ), 'objects' ), - function ( $taxonomy ) use ( $news_post_types ) { - return ( $taxonomy->label && array_intersect( $news_post_types, $taxonomy->object_type ) ); - } - ); - - $children = array( - 'skill_name' => new Fieldmanager_TextField( array( - 'label' => __( 'Skill name', 'voicewp' ), - 'description' => __( 'Optional name of skill. If empty, site name will be used instead.', 'voicewp' ), - 'attributes' => array( - 'style' => 'width: 95%;', - ), - ) ), - 'launch_request' => new Fieldmanager_TextArea( array( - 'label' => __( 'Welcome message', 'voicewp' ), - 'description' => __( 'This is the message a person hears when they open your skill with an utterance such as "Alexa, open {your skill name}"', 'voicewp' ), - 'default_value' => __( 'Welcome to the {put your skill name here} Skill. This skill allows you to listen to content from {your site name}. You can ask questions like: What are the latest articles? ... Now, what can I help you with.', 'voicewp' ), - 'attributes' => array( 'style' => 'width: 95%; height: 70px;' ), - ) ), - 'help_intent' => new Fieldmanager_TextArea( array( - 'label' => __( 'Help message', 'voicewp' ), - 'description' => __( "This is the message a person hears when they ask your skill for 'help'", 'voicewp' ), - 'default_value' => __( "{put your skill name here} provides you with the latest content from {your site name}. You can ask me for the latest articles, and then select an item from the list by saying, for example, 'read the 3rd article' Or you can also say exit... What can I help you with?", 'voicewp' ), - 'attributes' => array( 'style' => 'width: 95%; height: 70px;' ), - ) ), - 'list_prompt' => new Fieldmanager_TextArea( array( - 'label' => __( 'List Prompt', 'voicewp' ), - 'description' => __( 'This message prompts the user to select a piece of content to be read after hearing the headlines.', 'voicewp' ), - 'default_value' => __( 'Which article would you like to hear?', 'voicewp' ), - 'attributes' => array( 'style' => 'width: 95%; height: 50px;' ), - ) ), - 'stop_intent' => new Fieldmanager_TextArea( array( - 'label' => __( 'Stop message', 'voicewp' ), - 'description' => __( 'You can optionally provide a message when a person is done with your skill.', 'voicewp' ), - 'default_value' => __( 'Thanks for listening!', 'voicewp' ), - 'attributes' => array( 'style' => 'width: 95%; height: 50px;' ), - ) ), - 'news_id' => new Fieldmanager_TextField( array( - 'label' => __( 'News skill ID', 'voicewp' ), - 'description' => __( 'Add the application ID given by Amazon', 'voicewp' ), - 'attributes' => array( - 'style' => 'width: 95%;', - ), - ) ), - 'latest_taxonomies' => new \Fieldmanager_Checkboxes( array( - 'label' => __( 'Allow people to ask for content from specific:', 'voicewp' ), - 'options' => wp_list_pluck( $eligible_news_taxonomy_objects, 'label', 'name' ), - ) ), - ); - - $children['user_dictionary'] = new Fieldmanager_Group( array( - 'label' => __( 'Word Pronunciation Substitutions', 'voicewp' ), - 'collapsible' => true, - 'description' => __( "This allows you to define a global dictionary of words, phrases, abbreviations that Alexa should pronounce a certain way. For example, perhaps every occurrance of the state abreviation 'TN' should be pronounced as 'Tennessee', or 'NYC should be read as 'New York City' or the chemical 'Mg' read as 'Magnesium'. ", 'voicewp' ), - 'description_after_element' => false, - 'children' => array( - 'dictionary' => new Fieldmanager_Group( array( - 'limit' => 0, - 'extra_elements' => 0, - 'label' => __( 'Phrase / Word / Abbreviation', 'voicewp' ), - 'label_macro' => array( '%s', 'search' ), - 'add_more_label' => __( 'Add another phrase, word, or abbreviation', 'voicewp' ), - 'collapsible' => true, - 'children' => array( - 'search' => new Fieldmanager_TextField( array( - 'description' => __( 'Phrase to pronounce differently', 'voicewp' ), - 'attributes' => array( - 'style' => 'width: 45%;', - ), - ) ), - 'replace' => new Fieldmanager_TextField( array( - 'description' => __( 'How the above phrase should be pronounced.', 'voicewp' ), - 'attributes' => array( - 'style' => 'width: 45%;', - ), - ) ), - ), - ) ), - ), - ) ); - - $interaction_model = array(); - - $interaction_model['news_intent_schema'] = new \Fieldmanager_TextArea( array( - 'label' => __( 'The Intent Schema for your News skill. Add this to your news skill in the Amazon developer console.', 'voicewp' ), - 'escape' => array( 'label' => 'wp_kses_post' ), - 'default_value' => file_get_contents( __DIR__ . '/../speechAssets/news/IntentSchema.json', FILE_USE_INCLUDE_PATH ), - 'skip_save' => true, - 'attributes' => array_merge( - $readonly, - array( 'style' => 'width: 100%; height: 300px; font-family: monospace;' ) - ), - ) ); - - $interaction_model['custom_slot_types'] = new \Fieldmanager_Group( array( - 'label' => __( 'Custom Slot Types', 'voicewp' ), - 'children' => array( - new \Fieldmanager_Group( array( - 'name' => 'custom_slot_type_children', - 'description' => __( 'These slot types must be added to your news skill in the Amazon developer portal.', 'voicewp' ), - 'children' => array( - new \Fieldmanager_TextField( __( 'Type', 'voicewp' ), array( - 'name' => 'VOICEWP_POST_NUMBER_WORD', - 'default_value' => 'VOICEWP_POST_NUMBER_WORD', - 'attributes' => array_merge( - $readonly, - array( 'style' => 'width: 50%; font-family: monospace' ) - ), - ) ), - new \Fieldmanager_TextArea( __( 'Values', 'voicewp' ), array( - 'name' => 'VOICEWP_POST_NUMBER_WORD_values', - 'default_value' => "first\nsecond\nthird\nfourth\nfifth", - 'attributes' => array_merge( - $readonly, - array( 'style' => 'width: 50%; height: 150px; font-family: monospace;' ) - ), - ) ), - new \Fieldmanager_TextField( __( 'Type', 'voicewp' ), array( - 'name' => 'VOICEWP_TERM_NAME', - 'default_value' => 'VOICEWP_TERM_NAME', - 'attributes' => array_merge( - $readonly, - array( 'style' => 'width: 50%; font-family: monospace' ) - ), - ) ), - new \Fieldmanager_TextArea( __( 'Values', 'voicewp' ), array( - 'name' => 'VOICEWP_TERM_NAME_values', - 'default_value' => implode( - "\n", - // Generate sample terms from all available taxonomies. - // We want someone to add this slot even if they haven't - // turned on taxonomies so it's already there if they do. - array_values( array_unique( array_map( 'strtolower', wp_list_pluck( get_terms( array( - 'number' => 100, - 'order' => 'DESC', - 'orderby' => 'count', - 'taxonomy' => array_values( wp_list_pluck( $eligible_news_taxonomy_objects, 'name' ) ), - ) ), 'name' ) ) ) ) - ), - 'attributes' => array_merge( - $readonly, - array( 'style' => 'width: 50%; height: 150px; font-family: monospace;' ) - ), - ) ), - ), - ) ), - ), - 'skip_save' => true, - ) ); - - $interaction_model['news_utterances'] = new Fieldmanager_TextArea( array( - 'label' => __( 'Here\'s a starting point for your skill\'s Sample Utterances. You can add these to your news skill in the Amazon developer console.', 'voicewp' ), - 'escape' => array( 'label' => 'wp_kses_post' ), - 'default_value' => file_get_contents( __DIR__ . '/../speechAssets/news/Utterances.txt', FILE_USE_INCLUDE_PATH ), - 'skip_save' => true, - 'attributes' => array_merge( - $readonly, - array( 'style' => 'width: 100%; height: 300px;' ) - ), - ) ); - - $children['interaction_model'] = new \Fieldmanager_Group( array( - 'label' => __( 'Interaction Model', 'voicewp' ), - 'collapsible' => true, - 'children' => $interaction_model, - ) ); - - $fm = new Fieldmanager_Group( array( - 'name' => 'voicewp-settings', - 'children' => $children, - ) ); - $fm->activate_submenu_page(); -} -add_action( 'fm_submenu_voicewp-settings', 'voicewp_fm_alexa_settings' ); -if ( function_exists( 'fm_register_submenu_page' ) ) { - fm_register_submenu_page( 'voicewp-settings', 'tools.php', __( 'Alexa Skill Settings', 'voicewp' ), __( 'Alexa Skill Settings', 'voicewp' ), 'manage_options', 'voicewp-settings' ); -} - /** * Creates option of user defined dictionary terms for replacement within * Alexa content. Uses the 'sub' element to specify pronunciations of words. @@ -453,7 +265,7 @@ function ( $taxonomy ) use ( $news_post_types ) { // Add Option settings. $option_settings = new VoiceWp\Settings( 'options', - 'voicewp-settings-new', + 'voicewp-settings', __( 'Voice WP', 'voicewp' ), array( 'skill_name' => array( From a24e6158be99fba4f94d1426b8c2e5a7a35d1f53 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Wed, 28 Feb 2018 18:55:54 -0500 Subject: [PATCH 15/27] Remove FM flash briefing URL --- inc/fields.php | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/inc/fields.php b/inc/fields.php index 31d7636..5bd9efd 100644 --- a/inc/fields.php +++ b/inc/fields.php @@ -239,20 +239,6 @@ function voicewp_fm_submenu_presave_data( $data ) { } add_filter( 'fm_submenu_presave_data', 'voicewp_fm_submenu_presave_data' ); -/* - * Display a readonly field with URL of category briefing - */ -function voicewp_briefing_category_url() { - $id = ( isset( $_GET['tag_ID'] ) ) ? absint( $_GET['tag_ID'] ) : 0; - $fm = new Fieldmanager_TextField( array( - 'name' => 'briefing_url', - 'default_value' => home_url( '/wp-json/voicewp/v1/skill/briefing/' . $id ), - 'attributes' => array( 'readonly' => 'readonly' ), - ) ); - $fm->add_term_meta_box( __( 'Flash Briefing URL', 'voicewp' ), array( 'voicewp-briefing-category' ) ); -} -add_action( 'fm_term_voicewp-briefing-category', 'voicewp_briefing_category_url' ); - $news_post_types = voicewp_news_post_types(); // All public taxonomies associated with news post types. Could be abstracted into a function. $eligible_news_taxonomy_objects = array_filter( @@ -262,6 +248,23 @@ function ( $taxonomy ) use ( $news_post_types ) { } ); +/** + * Add the Flash Breifing URL. + */ +function voicewp_briefing_category_url() { + $id = ( isset( $_GET['tag_ID'] ) ) ? absint( $_GET['tag_ID'] ) : 0; + ?> + + + + + + + + Date: Wed, 28 Feb 2018 19:01:49 -0500 Subject: [PATCH 16/27] Update filter --- inc/fields.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/inc/fields.php b/inc/fields.php index 5bd9efd..a2f36cd 100644 --- a/inc/fields.php +++ b/inc/fields.php @@ -220,24 +220,26 @@ function voicewp_fm_skill_fact_quote() { * Creates option of user defined dictionary terms for replacement within * Alexa content. Uses the 'sub' element to specify pronunciations of words. * - * @param array $data FM data - * @return array + * @param array $new_value The new data. + * @param array $old_value The old data. + * @return array $new_value The new data. */ -function voicewp_fm_submenu_presave_data( $data ) { - if ( empty( $data['user_dictionary']['dictionary'] ) || ! is_array( $data['user_dictionary']['dictionary'] ) ) { - return $data; +function voicewp_fm_submenu_presave_data( $new_value, $old_value ) { + if ( empty( $new_value['user_dictionary']['dictionary'] ) || ! is_array( $new_value['user_dictionary']['dictionary'] ) ) { + return $new_value; } $dictionary = get_option( 'voicewp_user_dictionary', array() ); - foreach ( $data['user_dictionary']['dictionary'] as $key => $value ) { + foreach ( $new_value['user_dictionary']['dictionary'] as $key => $value ) { if ( ! empty( $value['search'] ) ) { $dictionary[ $value['search'] ] = sprintf( '%s', $value['replace'], $value['search'] ); } } update_option( 'voicewp_user_dictionary', $dictionary ); - return $data; + + return $new_value; } -add_filter( 'fm_submenu_presave_data', 'voicewp_fm_submenu_presave_data' ); +add_filter( 'pre_update_option_voicewp-settings', 'voicewp_fm_submenu_presave_data', 10, 2 ); $news_post_types = voicewp_news_post_types(); // All public taxonomies associated with news post types. Could be abstracted into a function. From cbae119a981bafec9e7b00e62be0b350dca0b2d1 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Wed, 28 Feb 2018 20:04:11 -0500 Subject: [PATCH 17/27] Start post meta --- inc/classes/class-settings.php | 94 ++++++++++++++++++++++++++++++++-- inc/fields.php | 39 ++++++++++---- 2 files changed, 120 insertions(+), 13 deletions(-) diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index 8103406..167fbf6 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -69,15 +69,14 @@ public function __construct( $context, $name, $title, $fields, $args = array() ) $this->_fields = $this->sanitize_fields( $fields ); $this->_args = $args; - // Prime the cache. - $this->get_data(); - // Add scripts. add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) ); if ( 'options' === $this->_context ) { add_action( 'admin_menu', array( $this, 'add_options_page' ) ); add_action( 'admin_init', array( $this, 'add_options_fields' ) ); + } elseif ( 'post' === $this->_context ) { + add_action( 'add_meta_boxes', array( $this, 'add_post_fields' ) ); } } @@ -206,6 +205,61 @@ function () use ( $option_name, $field ) { } } + /** + * Add the post meta box. + */ + public function add_post_fields() { + // No screen. + if ( empty( $this->_args['screen'] ) ) { + $this->_args['screen'] = 'post'; + } + + // No fields. + if ( empty( $this->_fields ) || ! is_array( $this->_fields ) ) { + return; + } + + // Prime the cache. + $this->get_data(); + + // Add the meta box. + add_meta_box( + $this->_name, + $this->_title, + function ( $post ) { + // Add the fields. + foreach ( $this->_fields as $name => $field ) { + + // Group field. + if ( + 'group' === $field['type'] + && ! empty( $field['children'] ) + && is_array( $field['children'] ) + ) { + continue; + } + + // Serialized data. + if ( isset( $this->_args['serialize_data'] ) && ! $this->_args['serialize_data'] ) { + $temp_field = $field; + $temp_field['name'] = $this->_name . '_' . $field['name']; + + $field['value'] = $this->get_field_value( $temp_field ); + } + + $field_name = $this->get_field_name( $this->_name, $field ); + + // Render the field. + echo '
'; + $this->render_field_label( $field['label'], $field_name ); + $this->render_field( $field_name, $field ); + echo '
'; + } + }, + $this->_args['screen'] + ); + } + /** * Santizie the fields. * @@ -402,6 +456,16 @@ public function render_field( $name, $field ) { echo $field_html; // WPCS XSS okay. } + /** + * Render the field label. + * + * @param string $label The field label. + * @param string $id The field ID. + */ + public function render_field_label( $label, $id = '' ) { + echo '
'; + } + /** * Wrap a chunk of HTML with "remove" and "move" buttons if applicable. * @@ -501,6 +565,20 @@ public function get_data() { switch ( $this->_context ) { case 'options': $this->_retrieved_data = get_option( $this->_name ); + break; + case 'post': + // Get the current post ID. + $post_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; + + if ( isset( $this->_args['serialize_data'] ) && ! $this->_args['serialize_data'] ) { + foreach ( $this->_fields as $name => $field ) { + $field_name = $this->_name . '_' . $field['name']; + $this->_retrieved_data[ $field_name ] = get_post_meta( $post_id, $field_name, true ); + } + } else { + $this->_retrieved_data = get_post_meta( $post_id, $this->_name, true ); + } + break; } } @@ -519,6 +597,11 @@ public function get_field_name( $name, $field ) { return null; } + // The data is not serialized. + if ( isset( $this->_args['serialize_data'] ) && ! $this->_args['serialize_data'] ) { + return $name . '_' . $field['name']; + } + return $name . "[{$field['name']}]"; } @@ -539,6 +622,11 @@ public function get_field_value( $field, $search_data = null ) { $search_data = $this->_retrieved_data; } + // This is a single field. + if ( ! is_array( $search_data ) ) { + return $search_data; + } + $value = $search_data[ $field['name'] ] ?? null; // If empty use the default value. diff --git a/inc/fields.php b/inc/fields.php index a2f36cd..e6be830 100644 --- a/inc/fields.php +++ b/inc/fields.php @@ -1,4 +1,15 @@ true ), 'objects' ), + function ( $taxonomy ) use ( $news_post_types ) { + return ( $taxonomy->label && array_intersect( $news_post_types, $taxonomy->object_type ) ); + } +); + /** * Add the Alexa app ID. * Populates the types of skills that can be created @@ -241,15 +252,6 @@ function voicewp_fm_submenu_presave_data( $new_value, $old_value ) { } add_filter( 'pre_update_option_voicewp-settings', 'voicewp_fm_submenu_presave_data', 10, 2 ); -$news_post_types = voicewp_news_post_types(); -// All public taxonomies associated with news post types. Could be abstracted into a function. -$eligible_news_taxonomy_objects = array_filter( - get_taxonomies( array( 'public' => true ), 'objects' ), - function ( $taxonomy ) use ( $news_post_types ) { - return ( $taxonomy->label && array_intersect( $news_post_types, $taxonomy->object_type ) ); - } -); - /** * Add the Flash Breifing URL. */ @@ -267,11 +269,28 @@ function voicewp_briefing_category_url() { } add_action( 'voicewp-briefing-category_edit_form_fields', 'voicewp_briefing_category_url' ); +// Add Skill settings. +$post_settings = new VoiceWp\Settings( + 'post', + 'voicewp_skill', + __( 'Skill Settings', 'voicewp' ), + array( + 'app_id' => array( + 'label' => __( 'Alexa Application ID', 'voicewp' ), + 'description' => __( 'Add the application ID given by Amazon', 'voicewp' ), + ), + ), + array( + 'screen' => 'voicewp-skill', + 'serialize_data' => false, + ) +); + // Add Option settings. $option_settings = new VoiceWp\Settings( 'options', 'voicewp-settings', - __( 'Voice WP', 'voicewp' ), + __( 'VoiceWP', 'voicewp' ), array( 'skill_name' => array( 'label' => __( 'Skill name', 'voicewp' ), From ed7da497bf1b26a2268a7cd6e28a407432f6e0a9 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Thu, 1 Mar 2018 09:23:30 -0500 Subject: [PATCH 18/27] Add media field --- client/css/admin/settings.css | 7 ++- client/js/admin/settings-media.js | 77 +++++++++++++++++++++++++ inc/classes/class-settings.php | 93 +++++++++++++++++++++++++++--- inc/fields.php | 96 ++++++++++--------------------- 4 files changed, 200 insertions(+), 73 deletions(-) create mode 100644 client/js/admin/settings-media.js diff --git a/client/css/admin/settings.css b/client/css/admin/settings.css index 260bcac..c2d05cf 100644 --- a/client/css/admin/settings.css +++ b/client/css/admin/settings.css @@ -1,4 +1,9 @@ -.voicewp-wrapper { +a.voicewp-delete { + color: #bc0b0b; + text-decoration: none; +} + +.voicewp-group-wrapper { border: 1px solid #ddd; padding: 12px; background-color: #fff; diff --git a/client/js/admin/settings-media.js b/client/js/admin/settings-media.js new file mode 100644 index 0000000..2713db3 --- /dev/null +++ b/client/js/admin/settings-media.js @@ -0,0 +1,77 @@ +var voicewp_media_frame = []; +( function( $ ) { + + $( document ).on( 'click', '.voicewp-media-remove', function(e) { + e.preventDefault(); + $(this).parents( '.voicewp-wrapper' ).find( '.voicewp-media-id' ).val( 0 ); + $(this).parents( '.voicewp-wrapper' ).find( '.media-wrapper' ).html( '' ); + }); + + $( document ).on( 'click', '.media-wrapper a', function( event ){ + event.preventDefault(); + $(this).closest('.media-wrapper').siblings('.voicewp-media-button').click(); + } ); + + $( document ).on( 'click', '.voicewp-media-button', function( event ) { + var $el = $(this), + library = {}; + event.preventDefault(); + + // If the media frame already exists, reopen it. + if ( voicewp_media_frame[ $el.attr('id') ] ) { + voicewp_media_frame[ $el.attr('id') ].open(); + return; + } + + // Create the media frame. + voicewp_media_frame[ $el.attr('id') ] = wp.media({ + library: library, + }); + + // When an image is selected, run a callback. + voicewp_media_frame[ $el.attr('id') ].on( 'select', function() { + // Grab the selected attachment. + var attachment = voicewp_media_frame[ $el.attr('id') ].state().get('selection').first().attributes; + + props = { size: $el.data('preview-size') || 'thumbnail' }; + props = wp.media.string.props( props, attachment ); + props.align = 'none'; + props.link = 'custom'; + props.linkUrl = '#'; + props.caption = ''; + $el.parent().find('.voicewp-media-id').val( attachment.id ); + + if ( attachment.type == 'image' ) { + props.url = props.src; + var preview = 'Uploaded file:
'; + preview += wp.media.string.image( props ); + } else { + var preview = 'Uploaded file: '; + preview += wp.media.string.link( props ); + } + + preview += '
remove'; + var $wrapper = $el.parent().find( '.media-wrapper' ); + $wrapper.html( preview ); + }); + + // Select the attachment when the frame opens + voicewp_media_frame[ $el.attr('id') ].on( 'open', function() { + // Select the current attachment inside the frame + var selection = voicewp_media_frame[ $el.attr('id') ].state().get('selection'), + id = $el.parent().find('.voicewp-media-id').val(), + attachment; + + // If there is a saved attachment, use it + if ( '' !== id && -1 !== id && typeof wp.media.attachment !== "undefined" ) { + attachment = wp.media.attachment( id ); + attachment.fetch(); + } + + selection.reset( attachment ? [ attachment ] : [] ); + } ); + + voicewp_media_frame[ $el.attr('id') ].open(); + } ); + +} )( jQuery ); diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index 167fbf6..6b57b45 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -86,6 +86,7 @@ public function __construct( $context, $name, $title, $fields, $args = array() ) public function admin_enqueue_scripts() { wp_enqueue_style( 'voicewp-settings-css', VOICEWP_URL . '/client/css/admin/settings.css' ); wp_enqueue_script( 'voicewp-settings-js', VOICEWP_URL . '/client/js/admin/settings.js', [ 'jquery' ] ); + wp_enqueue_script( 'voicewp-settings-media-js', VOICEWP_URL . '/client/js/admin/settings-media.js', [ 'jquery' ] ); } /** @@ -250,8 +251,8 @@ function ( $post ) { $field_name = $this->get_field_name( $this->_name, $field ); // Render the field. - echo '
'; - $this->render_field_label( $field['label'], $field_name ); + echo '
'; + $this->render_field_label( $field_name, $field ); $this->render_field( $field_name, $field ); echo '
'; } @@ -296,6 +297,7 @@ public function sanitize_fields( array $fields ) { public function sanitize_field( string $name, array $field ) : array { $field = wp_parse_args( $field, array( 'name' => $name, + 'label' => '', 'type' => 'text', 'limit' => 1, 'add_more_label' => ( isset( $field['type'] ) && 'group' === $field['type'] ) ? __( 'Add group', 'voicewp' ) : __( 'Add field', 'voicewp' ), @@ -360,15 +362,17 @@ public function render_group_children( $name, $group, $value ) { } $child['value'] = $this->get_field_value( $child, $value ); + $child_name = $this->get_field_name( $name, $child ); - $this->render_field( $this->get_field_name( $name, $child ), $child ); + $this->render_field_label( $child_name ); + $this->render_field( $child_name, $child ); } if ( 0 === $group['limit'] || 1 < $group['limit'] ) { $repeater_html = $this->wrap_with_multi_tools( $group, ob_get_clean() ); } - echo '
'; + echo '
'; echo $repeater_html; // WPCS: XSS okay. echo '
'; } @@ -401,6 +405,17 @@ public function render_field( $name, $field ) { // Render the correct field type. switch ( $field['type'] ) { + case 'checkbox': + $field_html .= sprintf( + '

', + esc_attr( $name ), + esc_attr( $base_name ), + esc_attr( $field_value ), + ! empty( $field_value ) ? 'checked="checked"' : '', + ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '', // Escaped internally. + esc_html( $field['label'] ) + ); // WPCS XSS okay. + break; case 'checkboxes': if ( empty( $field['options'] ) ) { break; @@ -418,6 +433,65 @@ public function render_field( $name, $field ) { ); // WPCS XSS okay. } break; + case 'media': + // Generate the preview. + $preview = ''; + if ( is_numeric( $field_value ) && $field_value > 0 ) { + $attachment = get_post( $field_value ); + + if ( strpos( $attachment->post_mime_type, 'image/' ) === 0 ) { + $preview .= esc_html__( 'Uploaded image:', 'voicewp' ) . '
'; + $preview .= '' . wp_get_attachment_image( $field_value, 'thumbnail', false ) . ''; + } + + $preview .= sprintf( '
%s', esc_html__( 'remove', 'voicewp' ) ); + } + + $field_html .= sprintf( + ' + +
%5$s
', + esc_attr( $name ), + esc_attr( $base_name ), + esc_attr( $field_value ), + esc_attr__( 'Add Media', 'voicewp' ), + $preview, + ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '' // Escaped internally. + ); // WPCS XSS okay. + + break; + case 'select': + if ( empty( $field['options'] ) ) { + break; + } + + $field_html .= sprintf( + ''; + break; case 'textarea': $field_html .= sprintf( '', @@ -459,11 +533,16 @@ public function render_field( $name, $field ) { /** * Render the field label. * - * @param string $label The field label. * @param string $id The field ID. + * @param array $field The field. */ - public function render_field_label( $label, $id = '' ) { - echo '
'; + public function render_field_label( $id, $field ) { + // Do not render the label if this is a checkbox type. + if ( 'checkbox' === $field['type'] ) { + return; + } + + echo '
'; } /** diff --git a/inc/fields.php b/inc/fields.php index e6be830..e5b1fe3 100644 --- a/inc/fields.php +++ b/inc/fields.php @@ -10,71 +10,6 @@ function ( $taxonomy ) use ( $news_post_types ) { } ); -/** - * Add the Alexa app ID. - * Populates the types of skills that can be created - */ -function voicewp_fm_alexa_app_settings() { - - $post_id = ( isset( $_GET['post'] ) ) ? absint( $_GET['post'] ) : 0; - - $children = array( - new \Fieldmanager_Select( __( 'Skill Type', 'voicewp' ), array( - 'name' => 'type', - 'first_empty' => true, - 'options' => array( - // Key is class name - 'Quote' => __( 'Fact / Quote', 'voicewp' ), - ), - 'description' => __( 'What type of functionality is being added?', 'voicewp' ), - ) ), - new \Fieldmanager_Media( __( 'Default App Card Image', 'voicewp' ), array( - 'name' => 'default_image', - 'description' => __( 'Image to be used when no other is provided. App cards can be displayed within the Alexa app when she responds to a user request.', 'voicewp' ), - ) ), - new \Fieldmanager_Checkbox( array( - 'name' => 'is_standalone', - 'label' => __( 'This is a standalone skill', 'voicewp' ), - 'description' => __( 'Will this be its own skill or is this part of another skill?', 'voicewp' ), - ) ), - new \Fieldmanager_TextField( __( 'Alexa Application ID', 'voicewp' ), array( - 'name' => 'app_id', - 'description' => __( 'Add the application ID given by Amazon', 'voicewp' ), - 'display_if' => array( - 'src' => 'is_standalone', - 'value' => true, - ), - ) ), - ); - - // If there's a post ID, output the REST endpoint for use in the amazon developer portal - if ( $post_id ) { - $children['readonly_skill_url'] = new \Fieldmanager_TextField( array( - 'label' => __( 'This is the endpoint URL of your skill. Paste this within the configuration tab for your skill in the developer portal.', 'voicewp' ), - 'default_value' => home_url( '/wp-json/voicewp/v1/skill/' ) . $post_id, - 'skip_save' => true, - 'attributes' => array( - 'readonly' => 'readonly', - 'style' => 'width: 95%;', - ), - 'display_if' => array( - 'src' => 'is_standalone', - 'value' => true, - ), - ) ); - } - - $fm = new \Fieldmanager_Group( array( - 'name' => 'voicewp_skill', - 'serialize_data' => false, - // Needs to be name => field for compat with FM's validation routines. - 'children' => array_combine( wp_list_pluck( $children, 'name' ), $children ), - ) ); - $context = fm_get_context(); - return $fm->add_meta_box( __( 'Skill Settings', 'voicewp' ), $context[1], 'normal', 'high' ); -} -add_action( 'fm_post_voicewp-skill', 'voicewp_fm_alexa_app_settings' ); - /** * Fields for controlling flash briefing content. * @@ -269,16 +204,47 @@ function voicewp_briefing_category_url() { } add_action( 'voicewp-briefing-category_edit_form_fields', 'voicewp_briefing_category_url' ); +// Get the current post ID. +$post_id = ( isset( $_GET['post'] ) ) ? absint( $_GET['post'] ) : 0; + // Add Skill settings. $post_settings = new VoiceWp\Settings( 'post', 'voicewp_skill', __( 'Skill Settings', 'voicewp' ), array( + 'type' => array( + 'type' => 'select', + 'label' => __( 'Skill Type', 'voicewp' ), + 'first_empty' => true, + 'options' => array( + // Key is class name. + 'Quote' => __( 'Fact / Quote', 'voicewp' ), + ), + 'description' => __( 'What type of functionality is being added?', 'voicewp' ), + ), + 'default_image' => array( + 'type' => 'media', + 'label' => __( 'Default App Card Image', 'voicewp' ), + 'description' => __( 'Image to be used when no other is provided. App cards can be displayed within the Alexa app when she responds to a user request.', 'voicewp' ), + ), + 'is_standalone' => array( + 'type' => 'checkbox', + 'label' => __( 'This is a standalone skill', 'voicewp' ), + 'description' => __( 'Will this be its own skill or is this part of another skill?', 'voicewp' ), + ), 'app_id' => array( 'label' => __( 'Alexa Application ID', 'voicewp' ), 'description' => __( 'Add the application ID given by Amazon', 'voicewp' ), ), + 'readonly_skill_url' => array( + 'label' => __( 'This is the endpoint URL of your skill. Paste this within the configuration tab for your skill in the developer portal.', 'voicewp' ), + 'default_value' => home_url( '/wp-json/voicewp/v1/skill/' ) . $post_id, + 'attributes' => array( + 'readonly' => 'readonly', + 'style' => 'width: 95%;', + ), + ), ), array( 'screen' => 'voicewp-skill', From 5cc415845db774bd63942a1f01d3c0fdf30eae91 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Thu, 1 Mar 2018 10:11:09 -0500 Subject: [PATCH 19/27] add new add_to_prefix property --- client/js/admin/settings.js | 6 ++-- inc/classes/class-settings.php | 55 +++++++++++++++++++++---------- inc/fields.php | 60 +++++++++++++++++++--------------- 3 files changed, 74 insertions(+), 47 deletions(-) diff --git a/client/js/admin/settings.js b/client/js/admin/settings.js index 7ca20c9..20f2c29 100644 --- a/client/js/admin/settings.js +++ b/client/js/admin/settings.js @@ -1,7 +1,7 @@ voicewp_add_another = function( $element ) { const repeaterGroup = $element.parent().parent(); - const cloneElement = repeaterGroup.find( '.voicewp-wrapper' ).last().clone(); - const items = repeaterGroup.find('.voicewp-wrapper'); + const cloneElement = repeaterGroup.find( '.voicewp-group-wrapper' ).last().clone(); + const items = repeaterGroup.find('.voicewp-group-wrapper'); cloneElement.insertBefore( $element.parent() ); @@ -23,7 +23,7 @@ voicewp_remove = function( $element ) { voicewp_renumber = function( $element ) { const repeaterName = $element.data( 'repeater-name' ); - const items = $element.find( '.voicewp-wrapper' ); + const items = $element.find( '.voicewp-group-wrapper' ); // Update name and clear data. items.each( function( index, value ) { diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index 6b57b45..674d598 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -67,7 +67,10 @@ public function __construct( $context, $name, $title, $fields, $args = array() ) $this->_name = $name; $this->_title = $title; $this->_fields = $this->sanitize_fields( $fields ); - $this->_args = $args; + $this->_args = wp_parse_args( $args, array( + 'serialize_data' => true, + 'add_to_prefix' => true, + ) ); // Add scripts. add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) ); @@ -230,6 +233,24 @@ public function add_post_fields() { function ( $post ) { // Add the fields. foreach ( $this->_fields as $name => $field ) { + $prefix = $this->_name; + if ( ! $this->_args['add_to_prefix'] ) { + $prefix = ''; + } + + $field_name = $this->get_field_name( $prefix, $field ); + + // Serialized data. + if ( ! $this->_args['serialize_data'] ) { + $temp_field = $field; + if ( ! $this->_args['add_to_prefix'] ) { + $temp_field['name'] = $this->_name . $field['name']; + } else { + $temp_field['name'] = $this->_name . '_' . $field['name']; + } + + $field['value'] = $this->get_field_value( $temp_field ); + } // Group field. if ( @@ -237,19 +258,10 @@ function ( $post ) { && ! empty( $field['children'] ) && is_array( $field['children'] ) ) { + $this->render_group( $field_name, $field, $this->get_data() ); continue; } - // Serialized data. - if ( isset( $this->_args['serialize_data'] ) && ! $this->_args['serialize_data'] ) { - $temp_field = $field; - $temp_field['name'] = $this->_name . '_' . $field['name']; - - $field['value'] = $this->get_field_value( $temp_field ); - } - - $field_name = $this->get_field_name( $this->_name, $field ); - // Render the field. echo '
'; $this->render_field_label( $field_name, $field ); @@ -364,7 +376,7 @@ public function render_group_children( $name, $group, $value ) { $child['value'] = $this->get_field_value( $child, $value ); $child_name = $this->get_field_name( $name, $child ); - $this->render_field_label( $child_name ); + $this->render_field_label( $child_name, $child ); $this->render_field( $child_name, $child ); } @@ -448,7 +460,7 @@ public function render_field( $name, $field ) { } $field_html .= sprintf( - ' + '
%5$s
', esc_attr( $name ), @@ -649,9 +661,14 @@ public function get_data() { // Get the current post ID. $post_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; - if ( isset( $this->_args['serialize_data'] ) && ! $this->_args['serialize_data'] ) { + if ( ! $this->_args['serialize_data'] ) { foreach ( $this->_fields as $name => $field ) { - $field_name = $this->_name . '_' . $field['name']; + if ( ! $this->_args['add_to_prefix'] ) { + $field_name = $field['name']; + } else { + $field_name = $this->_name . '_' . $field['name']; + } + $this->_retrieved_data[ $field_name ] = get_post_meta( $post_id, $field_name, true ); } } else { @@ -677,8 +694,12 @@ public function get_field_name( $name, $field ) { } // The data is not serialized. - if ( isset( $this->_args['serialize_data'] ) && ! $this->_args['serialize_data'] ) { - return $name . '_' . $field['name']; + if ( ! $this->_args['serialize_data'] ) { + if ( $this->_args['add_to_prefix'] ) { + $name .= '_'; + } + + return $name . $field['name']; } return $name . "[{$field['name']}]"; diff --git a/inc/fields.php b/inc/fields.php index e5b1fe3..02333b8 100644 --- a/inc/fields.php +++ b/inc/fields.php @@ -135,33 +135,6 @@ function voicewp_fm_briefing_content() { } add_action( 'fm_post_voicewp-briefing', 'voicewp_fm_briefing_content' ); -/** - * Add facts or skills. - */ -function voicewp_fm_skill_fact_quote() { - $fm = new Fieldmanager_Group( array( - 'name' => 'facts_quotes', - 'limit' => 0, - 'extra_elements' => 0, - 'add_more_label' => __( 'Add another fact or quote', 'voicewp' ), - 'children' => array( - 'fact_quote' => new Fieldmanager_TextField( array( - 'label' => __( 'Fact / Quote', 'voicewp' ), - 'description' => __( 'Add a fact or quote', 'voicewp' ), - ) ), - 'attribution' => new Fieldmanager_TextField( array( - 'label' => __( 'Attribution', 'voicewp' ), - 'description' => __( 'Add attribution if applicable', 'voicewp' ), - ) ), - 'image' => new Fieldmanager_Media( array( - 'label' => __( 'Alexa App Card Image', 'voicewp' ), - ) ), - ), - ) ); - $fm->add_meta_box( __( 'Facts / Quotes', 'voicewp' ), array( 'voicewp-skill' ) ); -} -add_action( 'fm_post_voicewp-skill', 'voicewp_fm_skill_fact_quote' ); - /** * Creates option of user defined dictionary terms for replacement within * Alexa content. Uses the 'sub' element to specify pronunciations of words. @@ -252,6 +225,39 @@ function voicewp_briefing_category_url() { ) ); +// Add Skill settings. +$post_settings = new VoiceWp\Settings( + 'post', + 'facts_quotes', + __( 'Facts / Quotes', 'voicewp' ), + array( + 'facts_quotes' => array( + 'type' => 'group', + 'limit' => 0, + 'add_more_label' => __( 'Add another fact or quote', 'voicewp' ), + 'children' => array( + 'fact_quote' => array( + 'label' => __( 'Fact / Quote', 'voicewp' ), + 'description' => __( 'Add a fact or quote', 'voicewp' ), + ), + 'attribution' => array( + 'label' => __( 'Attribution', 'voicewp' ), + 'description' => __( 'Add attribution if applicable', 'voicewp' ), + ), + 'image' => array( + 'type' => 'media', + 'label' => __( 'Alexa App Card Image', 'voicewp' ), + ), + ), + ), + ), + array( + 'screen' => 'voicewp-skill', + 'serialize_data' => false, + 'add_to_prefix' => false, + ) +); + // Add Option settings. $option_settings = new VoiceWp\Settings( 'options', From 0a5f0ede8ec5dd617bf5bab2657e1292007579e4 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Thu, 1 Mar 2018 14:12:35 -0500 Subject: [PATCH 20/27] Fix post meta settings --- client/js/admin/settings.js | 1 + inc/classes/class-settings.php | 168 +++++++++++++++++++++++++++------ inc/fields.php | 1 - 3 files changed, 140 insertions(+), 30 deletions(-) diff --git a/client/js/admin/settings.js b/client/js/admin/settings.js index 20f2c29..4033bd9 100644 --- a/client/js/admin/settings.js +++ b/client/js/admin/settings.js @@ -9,6 +9,7 @@ voicewp_add_another = function( $element ) { const fields = cloneElement.find('.voicewp-item'); fields.each( function() { jQuery( this ).val( '' ); + jQuery( this ).siblings( '.media-wrapper' ).html( '' ); }); voicewp_renumber( $element.closest( '.voicewpjs-repeating-group' ) ); diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index 674d598..6fe8dec 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -66,7 +66,7 @@ public function __construct( $context, $name, $title, $fields, $args = array() ) $this->_context = $context; $this->_name = $name; $this->_title = $title; - $this->_fields = $this->sanitize_fields( $fields ); + $this->_fields = $this->validate_fields( $fields ); $this->_args = wp_parse_args( $args, array( 'serialize_data' => true, 'add_to_prefix' => true, @@ -80,9 +80,29 @@ public function __construct( $context, $name, $title, $fields, $args = array() ) add_action( 'admin_init', array( $this, 'add_options_fields' ) ); } elseif ( 'post' === $this->_context ) { add_action( 'add_meta_boxes', array( $this, 'add_post_fields' ) ); + add_action( 'save_post', array( $this, 'save_post_fields' ) ); + } + + if ( did_action( 'admin_print_scripts' ) ) { + $this->admin_print_scripts(); + } else { + add_action( 'admin_print_scripts', array( $this, 'admin_print_scripts' ) ); } } + /** + * Hook into admin_print_scripts action to enqueue the media for the current + * post + */ + public function admin_print_scripts() { + $post = get_post(); + $args = array(); + if ( ! empty( $post->ID ) ) { + $args['post'] = $post->ID; + } + wp_enqueue_media( $args ); // generally on post pages this will not have an impact. + } + /** * Add the scripts. */ @@ -231,23 +251,24 @@ public function add_post_fields() { $this->_name, $this->_title, function ( $post ) { + // Add nonce. + wp_nonce_field( 'voicewp_post_fields_' . $this->_name, 'voicewp_nonce_' . $this->_name ); + // Add the fields. foreach ( $this->_fields as $name => $field ) { - $prefix = $this->_name; + $field_name = $this->_name; + + // No prefix. if ( ! $this->_args['add_to_prefix'] ) { - $prefix = ''; + $field_name = $name; } - $field_name = $this->get_field_name( $prefix, $field ); - // Serialized data. if ( ! $this->_args['serialize_data'] ) { + $field_name .= '_' . $field['name']; + $temp_field = $field; - if ( ! $this->_args['add_to_prefix'] ) { - $temp_field['name'] = $this->_name . $field['name']; - } else { - $temp_field['name'] = $this->_name . '_' . $field['name']; - } + $temp_field['name'] = $field_name; $field['value'] = $this->get_field_value( $temp_field ); } @@ -274,25 +295,107 @@ function ( $post ) { } /** - * Santizie the fields. + * Save the post fields. + * + * @param int $post_id The post ID. + */ + public function save_post_fields( $post_id ) { + // Do not save meta fields for revisions or autosaves. + if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) { + return; + } + + // Validate nonce. + check_admin_referer( 'voicewp_post_fields_' . $this->_name, 'voicewp_nonce_' . $this->_name ); + + // Add the fields. + foreach ( $this->_fields as $name => $field ) { + $field_name = $this->_name; + + // No prefix. + if ( ! $this->_args['add_to_prefix'] ) { + $field_name = $name; + } + + // Serialized data. + if ( ! $this->_args['serialize_data'] ) { + $field_name .= '_' . $field['name']; + } + + if ( isset( $_POST[ $field_name ] ) ) { + update_post_meta( $post_id, $field_name, $this->sanitize_field( $field, $_POST[ $field_name ] ) ); // WPCS: Sanitization okay. + } else { + delete_post_meta( $post_id, $field_name ); + } + } + } + + /** + * Sanitize field. + * + * @param array $field The field. + * @param mixed $value The current field value. + * @return mixed $value The sanitized field value. + */ + public function sanitize_field( $field, $value ) { + $value = wp_unslash( $value ); + + // Data is an array. + if ( is_array( $value ) && $field['is_group'] ) { + if ( 0 === $field['limit'] || 1 < $field['limit'] ) { + foreach ( $value as &$item ) { + foreach ( $field['children'] as $child ) { + $item[ $child['name'] ] = $this->sanitize_field( $child, $item[ $child['name'] ] ); + } + } + } else { + foreach ( $field['children'] as $child ) { + $value[ $child['name'] ] = $this->sanitize_field( $child, $value[ $child['name'] ] ); + } + } + + return $value; + } + + switch ( $field['type'] ) { + case 'media': + $value = absint( $value ); + break; + case 'textarea': + $value = sanitize_textarea_field( $value ); + break; + case 'checkbox': + case 'checkboxes': + case 'select': + case 'text': + default: + $value = sanitize_text_field( $value ); + break; + } + + return $value; + } + + /** + * Validate the fields. * * @param array $fields The current fields. * @return array The sanitized fields. */ - public function sanitize_fields( array $fields ) { + public function validate_fields( array $fields ) { if ( empty( $fields ) || ! is_array( $fields ) ) { return []; } foreach ( $fields as $name => &$field ) { - $field = $this->sanitize_field( $name, $field ); + $field = $this->validate_field( $name, $field ); if ( 'group' === $field['type'] && ! empty( $field['children'] ) && is_array( $field['children'] ) ) { - $field['children'] = $this->sanitize_fields( $field['children'] ); + $field['children'] = $this->validate_fields( $field['children'] ); } } @@ -306,7 +409,7 @@ public function sanitize_fields( array $fields ) { * @param array $field The field array. * @return array The sanitized field array. */ - public function sanitize_field( string $name, array $field ) : array { + public function validate_field( string $name, array $field ) : array { $field = wp_parse_args( $field, array( 'name' => $name, 'label' => '', @@ -376,8 +479,10 @@ public function render_group_children( $name, $group, $value ) { $child['value'] = $this->get_field_value( $child, $value ); $child_name = $this->get_field_name( $name, $child ); + echo '
'; $this->render_field_label( $child_name, $child ); $this->render_field( $child_name, $child ); + echo '
'; } if ( 0 === $group['limit'] || 1 < $group['limit'] ) { @@ -419,10 +524,9 @@ public function render_field( $name, $field ) { switch ( $field['type'] ) { case 'checkbox': $field_html .= sprintf( - '

', + '

', esc_attr( $name ), esc_attr( $base_name ), - esc_attr( $field_value ), ! empty( $field_value ) ? 'checked="checked"' : '', ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '', // Escaped internally. esc_html( $field['label'] ) @@ -460,8 +564,8 @@ public function render_field( $name, $field ) { } $field_html .= sprintf( - ' - + ' +
%5$s
', esc_attr( $name ), esc_attr( $base_name ), @@ -661,23 +765,22 @@ public function get_data() { // Get the current post ID. $post_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; + $field_name = $this->_name; + if ( ! $this->_args['serialize_data'] ) { foreach ( $this->_fields as $name => $field ) { - if ( ! $this->_args['add_to_prefix'] ) { - $field_name = $field['name']; - } else { - $field_name = $this->_name . '_' . $field['name']; - } - - $this->_retrieved_data[ $field_name ] = get_post_meta( $post_id, $field_name, true ); + $child_name = $field_name . '_' . $field['name']; + $this->_retrieved_data[ $child_name ] = get_post_meta( $post_id, $child_name, true ); } } else { - $this->_retrieved_data = get_post_meta( $post_id, $this->_name, true ); + $this->_retrieved_data = get_post_meta( $post_id, $field_name, true ); } break; } } + + return $this->_retrieved_data; } /** @@ -698,8 +801,6 @@ public function get_field_name( $name, $field ) { if ( $this->_args['add_to_prefix'] ) { $name .= '_'; } - - return $name . $field['name']; } return $name . "[{$field['name']}]"; @@ -718,10 +819,19 @@ public function get_field_value( $field, $search_data = null ) { return null; } + // Use custom search data. if ( null === $search_data ) { $search_data = $this->_retrieved_data; } + // This is a repeater group. + if ( + is_array( $search_data ) + && array_keys( $search_data ) === range( 0, count( $search_data ) - 1 ) + ) { + return $search_data; + } + // This is a single field. if ( ! is_array( $search_data ) ) { return $search_data; diff --git a/inc/fields.php b/inc/fields.php index 02333b8..dc998bf 100644 --- a/inc/fields.php +++ b/inc/fields.php @@ -253,7 +253,6 @@ function voicewp_briefing_category_url() { ), array( 'screen' => 'voicewp-skill', - 'serialize_data' => false, 'add_to_prefix' => false, ) ); From 3cd76e11acc5ab2b51339094f11119fbe2ff96db Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Thu, 1 Mar 2018 14:39:29 -0500 Subject: [PATCH 21/27] Move briefing fields --- client/js/admin/settings-media.js | 30 +++++ inc/classes/class-settings.php | 37 ++++-- inc/fields.php | 196 +++++++++++------------------- 3 files changed, 126 insertions(+), 137 deletions(-) diff --git a/client/js/admin/settings-media.js b/client/js/admin/settings-media.js index 2713db3..00127fb 100644 --- a/client/js/admin/settings-media.js +++ b/client/js/admin/settings-media.js @@ -23,11 +23,41 @@ var voicewp_media_frame = []; return; } + // If mime type has been restricted, make sure the library only shows that + // type. + if ( $el.data( 'mime-type' ) && 'all' !== $el.data( 'mime-type' ) ) { + library.type = $el.data( 'mime-type' ); + } + // Create the media frame. voicewp_media_frame[ $el.attr('id') ] = wp.media({ library: library, }); + // If mime type has been restricted, make sure the library doesn't autoselect + // an uploaded file if it's the wrong mime type + if ( $el.data( 'mime-type' ) && 'all' !== $el.data( 'mime-type' ) ) { + // This event is only fired when a file is uploaded. + // @see {wp.media.controller.Library:uploading()} + voicewp_media_frame[ $el.attr('id') ].on( 'library:selection:add', function() { + // This event gets fired for every frame that has ever been created on + // the current page, which causes errors. We only care about the visible + // frame. Also, FM can change the ID of buttons, which means some older + // frames may no longer be valid. + if ( 'undefined' === typeof voicewp_media_frame[ $el.attr('id') ] || ! voicewp_media_frame[ $el.attr('id') ].$el.is(':visible') ) { + return; + } + + // Get the Selection object and the currently selected attachment. + var selection = voicewp_media_frame[ $el.attr('id') ].state().get('selection'), + attachment = selection.first(); + // If the mime type is wrong, deselect the file. + if ( attachment.attributes.type !== $el.data( 'mime-type' ) ) { + selection.remove(attachment); + } + }); + } + // When an image is selected, run a callback. voicewp_media_frame[ $el.attr('id') ].on( 'select', function() { // Grab the selected attachment. diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index 6fe8dec..1878fca 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -284,10 +284,7 @@ function ( $post ) { } // Render the field. - echo '
'; - $this->render_field_label( $field_name, $field ); - $this->render_field( $field_name, $field ); - echo '
'; + $this->render_complete_field( $field_name, $field ); } }, $this->_args['screen'] @@ -305,6 +302,14 @@ public function save_post_fields( $post_id ) { return; } + // Do not save fields for this post type. + if ( + ( is_string( $this->_args['screen'] ) && get_post_type( $post_id ) !== $this->_args['screen'] ) + || ( is_array( $this->_args['screen'] ) && in_array( get_post_type( $post_id ), $this->_args['screen'], true ) ) + ) { + return; + } + // Validate nonce. check_admin_referer( 'voicewp_post_fields_' . $this->_name, 'voicewp_nonce_' . $this->_name ); @@ -417,6 +422,7 @@ public function validate_field( string $name, array $field ) : array { 'limit' => 1, 'add_more_label' => ( isset( $field['type'] ) && 'group' === $field['type'] ) ? __( 'Add group', 'voicewp' ) : __( 'Add field', 'voicewp' ), 'is_group' => false, + 'mime_type' => 'all', ) ); if ( 'group' === $field['type'] ) { @@ -479,10 +485,7 @@ public function render_group_children( $name, $group, $value ) { $child['value'] = $this->get_field_value( $child, $value ); $child_name = $this->get_field_name( $name, $child ); - echo '
'; - $this->render_field_label( $child_name, $child ); - $this->render_field( $child_name, $child ); - echo '
'; + $this->render_complete_field( $child_name, $child ); } if ( 0 === $group['limit'] || 1 < $group['limit'] ) { @@ -494,6 +497,19 @@ public function render_group_children( $name, $group, $value ) { echo '
'; } + /** + * Render the complete field with a label. + * + * @param string $field_name The field name. + * @param array $field The field. + */ + public function render_complete_field( $field_name, $field ) { + echo '
'; + $this->render_field_label( $field_name, $field ); + $this->render_field( $field_name, $field ); + echo '
'; + } + /** * Renders the field. * @@ -564,13 +580,14 @@ public function render_field( $name, $field ) { } $field_html .= sprintf( - ' + ' -
%5$s
', +
%6$s
', esc_attr( $name ), esc_attr( $base_name ), esc_attr( $field_value ), esc_attr__( 'Add Media', 'voicewp' ), + esc_attr( $field['mime_type'] ), $preview, ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '' // Escaped internally. ); // WPCS XSS okay. diff --git a/inc/fields.php b/inc/fields.php index dc998bf..b8aa693 100644 --- a/inc/fields.php +++ b/inc/fields.php @@ -10,131 +10,6 @@ function ( $taxonomy ) use ( $news_post_types ) { } ); -/** - * Fields for controlling flash briefing content. - * - * @return \Fieldmanager_Context_Post Post context. - */ -function voicewp_fm_briefing_content() { - $post_id = ( isset( $_GET['post'] ) ) ? intval( $_GET['post'] ) : 0; - $allowed_formats = array( 'mp3' ); - - $children = array( - // Display-if control. - 'source' => new \Fieldmanager_Radios( __( 'Source', 'voicewp' ), array( - /** - * Allows for filtering the available sources that - * can be used for populating a flash briefing - * - * @since 1.1.0 - * - * @param array Flash briefing source options - */ - 'options' => apply_filters( 'voicewp_briefing_source_options', array( - 'content' => __( 'Text', 'voicewp' ), - 'audio_url' => __( 'HTTPS URL to an MP3', 'voicewp' ), - 'attachment_id' => __( 'Uploaded MP3', 'voicewp' ), - ) ), - /** - * String defining the default content source of a flash briefing - * - * @since 1.1.0 - * - * @param string default value - */ - 'default_value' => apply_filters( 'voicewp_default_briefing_source', 'content' ), - ) ), - 'content' => new \VoiceWP_Fieldmanager_Content_TextArea( __( 'Text', 'voicewp' ), array( - 'description' => __( 'Text should be under 4,500 characters.', 'voicewp' ), - 'attributes' => array( - 'style' => 'width: 100%; height: 400px', - 'maxlength' => 4500, - ), - 'display_if' => array( - 'src' => 'source', - 'value' => 'content', - ), - ) ), - 'audio_url' => new \Fieldmanager_Link( __( 'HTTPS URL to an MP3', 'voicewp' ), array( - 'attributes' => array( - 'style' => 'width: 100%;', - ), - 'display_if' => array( - 'src' => 'source', - /** - * Allow filtering of what sources an audio link is used with - * - * @since 1.1.0 - * - * @param string Comma separated list of source options to display the field for - */ - 'value' => apply_filters( 'voicewp_briefing_audio_url_display_if', 'audio_url' ), - ), - ) ), - 'attachment_id' => new \Fieldmanager_Media( __( 'Uploaded MP3', 'voicewp' ), array( - 'mime_type' => 'audio', - 'button_label' => __( 'Select a File', 'voicewp' ), - 'modal_button_label' => __( 'Select File', 'voicewp' ), - 'modal_title' => __( 'Select a File', 'voicewp' ), - 'display_if' => array( - 'src' => 'source', - 'value' => 'attachment_id', - ), - ) ), - 'uuid' => new \Fieldmanager_Hidden( array() ), - ); - - if ( ! get_post_meta( $post_id, 'voicewp_briefing_uuid', true ) ) { - $children['uuid']->default_value = voicewp_generate_uuid4(); - } - - /** - * Allow addition, removal, or modification of briefing fields - * - * @since 1.1.0 - * - * @param array $children The Fieldmanager fields used with a flash briefing - */ - $children = apply_filters( 'voicewp_briefing_fields', $children ); - - $fm = new \Fieldmanager_Group( array( - 'name' => 'voicewp_briefing', - 'serialize_data' => false, - // Needs to be name => field for compat with FM's validation routines. - 'children' => $children, - ) ); - - // Help text. - if ( $post_id ) { - $existing_audio_url = get_post_meta( $post_id, 'voicewp_briefing_audio_url', true ); - - if ( ! $existing_audio_url || ! in_array( pathinfo( parse_url( $existing_audio_url, PHP_URL_PATH ), PATHINFO_EXTENSION ), $allowed_formats, true ) ) { - $fm->children['audio_url']->description = __( 'Please make sure this is a URL to an MP3 file.', 'voicewp' ); - } - - $existing_attachment_id = get_post_meta( $post_id, 'voicewp_briefing_attachment_id', true ); - - if ( $existing_attachment_id ) { - $attachment_metadata = wp_get_attachment_metadata( $existing_attachment_id ); - $warnings = array(); - - if ( ! isset( $attachment_metadata['fileformat'] ) || ! in_array( $attachment_metadata['fileformat'], $allowed_formats, true ) ) { - $warnings[] = __( 'Please make sure this is an MP3 upload.', 'voicewp' ); - } - - if ( isset( $attachment_metadata['length'] ) && $attachment_metadata['length'] > ( 10 * MINUTE_IN_SECONDS ) ) { - $warnings[] = __( 'Audio should be under 10 minutes long.', 'voicewp' ); - } - - $fm->children['attachment_id']->description = implode( ' ', $warnings ); - } - } - - $context = fm_get_context(); - return $fm->add_meta_box( __( 'Briefing Content', 'voicewp' ), $context[1], 'normal', 'high' ); -} -add_action( 'fm_post_voicewp-briefing', 'voicewp_fm_briefing_content' ); - /** * Creates option of user defined dictionary terms for replacement within * Alexa content. Uses the 'sub' element to specify pronunciations of words. @@ -180,8 +55,75 @@ function voicewp_briefing_category_url() { // Get the current post ID. $post_id = ( isset( $_GET['post'] ) ) ? absint( $_GET['post'] ) : 0; +// Add Briefing settings. +$briefing_settings = new VoiceWp\Settings( + 'post', + 'voicewp_briefing', + __( 'Briefing Settings', 'voicewp' ), + array( + 'source' => array( + 'type' => 'select', + 'label' => __( 'Source', 'voicewp' ), + /** + * Allows for filtering the available sources that + * can be used for populating a flash briefing + * + * @since 1.1.0 + * + * @param array Flash briefing source options + */ + 'options' => apply_filters( 'voicewp_briefing_source_options', array( + 'content' => __( 'Text', 'voicewp' ), + 'audio_url' => __( 'HTTPS URL to an MP3', 'voicewp' ), + 'attachment_id' => __( 'Uploaded MP3', 'voicewp' ), + ) ), + /** + * String defining the default content source of a flash briefing + * + * @since 1.1.0 + * + * @param string default value + */ + 'default_value' => apply_filters( 'voicewp_default_briefing_source', 'content' ), + ), + 'content' => array( + 'type' => 'textarea', + 'label' => __( 'Text', 'voicewp' ), + 'description' => __( 'Text should be under 4,500 characters.', 'voicewp' ), + 'attributes' => array( + 'style' => 'width: 100%; height: 400px', + 'maxlength' => 4500, + ), + ), + 'audio_url' => array( + 'type' => 'text', + 'label' => __( 'HTTPS URL to an MP3', 'voicewp' ), + 'description' => __( 'Please make sure this is a URL to an MP3 file.', 'voicewp' ), + 'attributes' => array( + 'style' => 'width: 100%;', + ), + ), + 'attachment_id' => array( + 'type' => 'media', + 'label' => __( 'Uploaded MP3', 'voicewp' ), + 'mime_type' => 'audio', + ), + 'uuid' => array( + 'label' => __( 'UUID', 'voicewp' ), + 'default_value' => ! get_post_meta( $post_id, 'voicewp_briefing_uuid', true ) ?: voicewp_generate_uuid4(), + 'attributes' => array( + 'readonly' => 'readonly', + ), + ), + ), + array( + 'screen' => 'voicewp-briefing', + 'serialize_data' => false, + ) +); + // Add Skill settings. -$post_settings = new VoiceWp\Settings( +$skill_settings = new VoiceWp\Settings( 'post', 'voicewp_skill', __( 'Skill Settings', 'voicewp' ), @@ -226,7 +168,7 @@ function voicewp_briefing_category_url() { ); // Add Skill settings. -$post_settings = new VoiceWp\Settings( +$skill_fact_quote_settings = new VoiceWp\Settings( 'post', 'facts_quotes', __( 'Facts / Quotes', 'voicewp' ), From f1773f3d2ad54a4f173f9315e9183555fd6ef6bf Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Mon, 5 Mar 2018 12:16:26 -0500 Subject: [PATCH 22/27] Implement display if functionlaity --- client/js/admin/settings.js | 108 +++++++++++++++++++++++++++++++++ inc/classes/class-settings.php | 40 +++++++++--- inc/fields.php | 27 +++++++++ 3 files changed, 166 insertions(+), 9 deletions(-) diff --git a/client/js/admin/settings.js b/client/js/admin/settings.js index 4033bd9..7722404 100644 --- a/client/js/admin/settings.js +++ b/client/js/admin/settings.js @@ -53,4 +53,112 @@ jQuery( document ).ready( function ( $ ) { e.preventDefault(); voicewp_remove( $( this ) ); } ); + + // Initializes triggers to conditionally hide or show fields + voicewp_init_display_if = function() { + var val; + var src = $( this ).data( 'display-src' ); + var values = voicewpGetCompareValues( this ); + var wrapper = $( this ).closest('.voicewp-wrapper'); + // Wrapper divs sometimes receive .voicewp-element, but don't use them as + // triggers. Also don't use autocomplete inputs or a checkbox's hidden + // sibling as triggers, because the value is in their sibling fields + // (which this still matches). + var trigger = wrapper.siblings( '.voicewp-' + src + '-wrapper' ).find( '.voicewp-element' ).not( 'div, .voicewp-autocomplete, .voicewp-checkbox-hidden' ); + + // Sanity check before calling `val()` or `split()`. + if ( 0 === trigger.length ) { + return; + } + + if ( trigger.is( ':checkbox' ) ) { + if ( trigger.is( ':checked' ) ) { + val = true; + } else { + val = false; + } + } else if ( trigger.is( ':radio' ) ) { + if ( trigger.filter( ':checked' ).length ) { + val = trigger.filter( ':checked' ).val(); + } else { + // On load, there might not be any selected radio, in which case call the value blank. + val = ''; + } + } else { + val = trigger.val().split( ',' ); + } + trigger.addClass( 'display-trigger' ); + if ( ! voicewp_match_value( values, val ) ) { + wrapper.hide(); + } + }; + $( '.voicewp-display-if' ).each( voicewp_init_display_if ); + + // Controls the trigger to show or hide fields + voicewp_trigger_display_if = function() { + var val; + var $this = $( this ); + var name = $this.attr( 'name' ); + if ( $this.is( ':checkbox' ) ) { + if ( $this.is( ':checked' ) ) { + val = true; + } else { + val = false; + } + } else if ( $this.is( ':radio' ) ) { + val = $this.filter( ':checked' ).val(); + } else { + val = $this.val().split( ',' ); + } + + $( this ).closest( '.voicewp-wrapper' ).siblings().each( function() { + var element = $( this ).find('.voicewp-element'); + + if ( element.hasClass( 'voicewp-display-if' ) ) { + if ( name && name.match( element.data( 'display-src' ) ) != null ) { + if ( voicewp_match_value( voicewpGetCompareValues( element ), val ) ) { + $( this ).show(); + } else { + $( this ).hide(); + } + } + } + } ); + }; + $( document ).on( 'change', '.display-trigger', voicewp_trigger_display_if ); } ); + +/** + * Get data attribute display-value(s). + * + * Accounts for jQuery converting string to number automatically. + * + * @param HTMLDivElement el Wrapper with the data attribute. + * @return string|number|array Single string or number, or array if data attr contains CSV. + */ +var voicewpGetCompareValues = function( el ) { + var values = jQuery( el ).data( 'display-value' ); + try { + values = values.split( ',' ); + } catch( e ) { + // If jQuery already converted string to number. + values = [ values ]; + } + return values; +}; + +/** + * Matches the display if value. + * + * @param {array} values The list of values to match. + * @param {string} match_string The match string. + * @return {bool} True of false. + */ +var voicewp_match_value = function( values, match_string ) { + for ( var index in values ) { + if ( values[index] == match_string ) { + return true; + } + } + return false; +} diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index 1878fca..df49749 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -504,7 +504,7 @@ public function render_group_children( $name, $group, $value ) { * @param array $field The field. */ public function render_complete_field( $field_name, $field ) { - echo '
'; + echo '
'; $this->render_field_label( $field_name, $field ); $this->render_field( $field_name, $field ); echo '
'; @@ -535,14 +535,31 @@ public function render_field( $name, $field ) { } $field_html = ''; + $field_classes = [ + 'voicewp-item', + 'voicewp-element', + ]; + + // Checks to see if element has display_if data values, and inserts the data attributes if it does. + if ( ! empty( $field['display_if'] ) ) { + $field_classes[] = 'voicewp-display-if'; + + // For backwards compatibility. + $field['attributes']['data-display-src'] = $field['display_if']['src']; + $field['attributes']['data-display-value'] = $field['display_if']['value']; + } + + // Finalize the field classes. + $field_classes_attr = implode( ' ', array_map( 'sanitize_html_class', $field_classes ) ); // Render the correct field type. switch ( $field['type'] ) { case 'checkbox': $field_html .= sprintf( - '

', + '

', esc_attr( $name ), esc_attr( $base_name ), + esc_attr( $field_classes_attr ), ! empty( $field_value ) ? 'checked="checked"' : '', ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '', // Escaped internally. esc_html( $field['label'] ) @@ -555,9 +572,10 @@ public function render_field( $name, $field ) { foreach ( $field['options'] as $value => $label ) { $field_html .= sprintf( - '

', + '

', esc_attr( $name ), esc_attr( $base_name ), + esc_attr( $field_classes_attr ), esc_attr( $value ), ! empty( $field_value ) && in_array( $value, $field_value, true ) ? 'checked="checked"' : '', ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '', // Escaped internally. @@ -580,11 +598,12 @@ public function render_field( $name, $field ) { } $field_html .= sprintf( - ' - -
%6$s
', + ' + +
%7$s
', esc_attr( $name ), esc_attr( $base_name ), + esc_attr( $field_classes_attr ), esc_attr( $field_value ), esc_attr__( 'Add Media', 'voicewp' ), esc_attr( $field['mime_type'] ), @@ -599,9 +618,10 @@ public function render_field( $name, $field ) { } $field_html .= sprintf( - '', esc_attr( $name ), esc_attr( $base_name ), + esc_attr( $field_classes_attr ), ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '' // Escaped internally. ); // WPCS XSS okay. @@ -627,9 +647,10 @@ public function render_field( $name, $field ) { break; case 'textarea': $field_html .= sprintf( - '', + '', esc_attr( $name ), esc_attr( $base_name ), + esc_attr( $field_classes_attr ), esc_html( $field_value ), ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '' // Escaped internally. ); // WPCS XSS okay. @@ -637,9 +658,10 @@ public function render_field( $name, $field ) { case 'text': default: $field_html .= sprintf( - '', + '', esc_attr( $name ), esc_attr( $base_name ), + esc_attr( $field_classes_attr ), esc_attr( $field_value ), ! empty( $field['attributes'] ) ? $this->add_attributes( $field['attributes'] ) : '' // Escaped internally. ); // WPCS XSS okay. diff --git a/inc/fields.php b/inc/fields.php index b8aa693..e6c683b 100644 --- a/inc/fields.php +++ b/inc/fields.php @@ -94,6 +94,10 @@ function voicewp_briefing_category_url() { 'style' => 'width: 100%; height: 400px', 'maxlength' => 4500, ), + 'display_if' => array( + 'src' => 'voicewp_briefing_source', + 'value' => 'content', + ), ), 'audio_url' => array( 'type' => 'text', @@ -102,11 +106,26 @@ function voicewp_briefing_category_url() { 'attributes' => array( 'style' => 'width: 100%;', ), + 'display_if' => array( + 'src' => 'voicewp_briefing_source', + /** + * Allow filtering of what sources an audio link is used with + * + * @since 1.1.0 + * + * @param string Comma separated list of source options to display the field for + */ + 'value' => apply_filters( 'voicewp_briefing_audio_url_display_if', 'audio_url' ), + ), ), 'attachment_id' => array( 'type' => 'media', 'label' => __( 'Uploaded MP3', 'voicewp' ), 'mime_type' => 'audio', + 'display_if' => array( + 'src' => 'voicewp_briefing_source', + 'value' => 'attachment_id', + ), ), 'uuid' => array( 'label' => __( 'UUID', 'voicewp' ), @@ -151,6 +170,10 @@ function voicewp_briefing_category_url() { 'app_id' => array( 'label' => __( 'Alexa Application ID', 'voicewp' ), 'description' => __( 'Add the application ID given by Amazon', 'voicewp' ), + 'display_if' => array( + 'src' => 'voicewp_skill_is_standalone', + 'value' => true, + ), ), 'readonly_skill_url' => array( 'label' => __( 'This is the endpoint URL of your skill. Paste this within the configuration tab for your skill in the developer portal.', 'voicewp' ), @@ -159,6 +182,10 @@ function voicewp_briefing_category_url() { 'readonly' => 'readonly', 'style' => 'width: 95%;', ), + 'display_if' => array( + 'src' => 'voicewp_skill_is_standalone', + 'value' => true, + ), ), ), array( From e9ad067e7df6303e9c7ccc9f8a6cebe2af6abffa Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Mon, 5 Mar 2018 13:54:28 -0500 Subject: [PATCH 23/27] Add filters for briefing content --- inc/classes/class-settings.php | 40 ++- inc/fields.php | 56 ++++ vendor/autoload.php | 7 + vendor/composer/ClassLoader.php | 413 ++++++++++++++++++++++++ vendor/composer/LICENSE | 21 ++ vendor/composer/autoload_classmap.php | 9 + vendor/composer/autoload_namespaces.php | 9 + vendor/composer/autoload_psr4.php | 9 + vendor/composer/autoload_real.php | 52 +++ vendor/composer/autoload_static.php | 15 + 10 files changed, 628 insertions(+), 3 deletions(-) create mode 100644 vendor/autoload.php create mode 100644 vendor/composer/ClassLoader.php create mode 100644 vendor/composer/LICENSE create mode 100644 vendor/composer/autoload_classmap.php create mode 100644 vendor/composer/autoload_namespaces.php create mode 100644 vendor/composer/autoload_psr4.php create mode 100644 vendor/composer/autoload_real.php create mode 100644 vendor/composer/autoload_static.php diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index df49749..e4efc9c 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -327,9 +327,24 @@ public function save_post_fields( $post_id ) { $field_name .= '_' . $field['name']; } + $new_value = null; + if ( isset( $_POST[ $field_name ] ) ) { - update_post_meta( $post_id, $field_name, $this->sanitize_field( $field, $_POST[ $field_name ] ) ); // WPCS: Sanitization okay. + /** + * Filters the new value to be saved as post meta. + * + * @param mixed $new_value The new meta value. + * @param int $post_id The post ID. + * @param string $field_name The field name to be saved. + * @param Settings $settings_object The current settings object. + */ + $new_value = apply_filters( 'voicewp_update_post_meta', $this->sanitize_field( $field, $_POST[ $field_name ] ), $post_id, $field_name, $this ); // WPCS: Sanitization okay. + } + + if ( isset( $new_value ) ) { + update_post_meta( $post_id, $field_name, $new_value ); } else { + // Delete the meta value if no data was found in the form. delete_post_meta( $post_id, $field_name ); } } @@ -809,10 +824,10 @@ public function get_data() { if ( ! $this->_args['serialize_data'] ) { foreach ( $this->_fields as $name => $field ) { $child_name = $field_name . '_' . $field['name']; - $this->_retrieved_data[ $child_name ] = get_post_meta( $post_id, $child_name, true ); + $this->_retrieved_data[ $child_name ] = $this->get_post_meta_value( $post_id, $child_name ); } } else { - $this->_retrieved_data = get_post_meta( $post_id, $field_name, true ); + $this->_retrieved_data = $this->get_post_meta_value( $post_id, $field_name ); } break; @@ -822,6 +837,25 @@ public function get_data() { return $this->_retrieved_data; } + /** + * Get the post meta data value. + * + * @param int $post_id The post ID. + * @param string $field_name The meta key. + * @return mixed The meta value. + */ + public function get_post_meta_value( $post_id, $field_name ) { + /** + * Filters the post meta data retrieved. + * + * @param mixed $value The post meta value. + * @param int $post_id The post ID. + * @param string $field_name The meta key. + * @param Settings $settings_object The current settings object. + */ + return apply_filters( 'voicewp_get_post_meta', get_post_meta( $post_id, $field_name, true ), $post_id, $field_name, $this ); + } + /** * Get the field name. * diff --git a/inc/fields.php b/inc/fields.php index e6c683b..056d1fd 100644 --- a/inc/fields.php +++ b/inc/fields.php @@ -52,6 +52,62 @@ function voicewp_briefing_category_url() { } add_action( 'voicewp-briefing-category_edit_form_fields', 'voicewp_briefing_category_url' ); +/** + * Make sure the briefing content is saved to the post object as well as post meta. + * + * @param mixed $value The current meta value. + * @param int $post_id The post ID. + * @param string $name The meta name. + * @param Settings $settings_object The settings object. + * @return mixed $value The new meta value. + */ +function voicewp_save_briefing_content( $value, $post_id, $name, $settings_object ) { + if ( + 'voicewp-briefing' === get_post_type( $post_id ) + && 'voicewp_briefing_content' === $name + ) { + // Prevent infinite loops. + remove_action( 'save_post', array( $settings_object, 'save_post_fields' ) ); + + // Update the post's content. + wp_update_post( array( + 'ID' => $post_id, + 'post_content' => $value, + ) ); + + // Re-add the filter. + add_action( 'save_post', array( $settings_object, 'save_post_fields' ) ); + } + + return $value; +} +add_filter( 'voicewp_update_post_meta', 'voicewp_save_briefing_content', 10, 4 ); + +/** + * Get the briefing content value from the post object content. + * + * @param mixed $value The current meta value. + * @param int $post_id The post ID. + * @param string $name The meta name. + * @param Settings $settings_object The settings object. + * @return mixed $value The new meta value. + */ +function voicewp_get_briefing_content( $value, $post_id, $name, $settings_object ) { + if ( + 'voicewp-briefing' === get_post_type( $post_id ) + && 'voicewp_briefing_content' === $name + ) { + $post_object = get_post( $post_id ); + + if ( $post_object instanceof \WP_Post ) { + return $post_object->post_content; + } + } + + return $value; +} +add_filter( 'voicewp_get_post_meta', 'voicewp_get_briefing_content', 10, 4 ); + // Get the current post ID. $post_id = ( isset( $_GET['post'] ) ) ? absint( $_GET['post'] ) : 0; diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000..ac1a3c3 --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + + private $classMapAuthoritative = false; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 + if ('\\' == $class[0]) { + $class = substr($class, 1); + } + + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative) { + return false; + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if ($file === null && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if ($file === null) { + // Remember that this class does not exist. + return $this->classMap[$class] = false; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach ($this->prefixDirsPsr4[$prefix] as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000..1a28124 --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) 2016 Nils Adermann, Jordi Boggiano + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..7a91153 --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,9 @@ += 50600 && !defined('HHVM_VERSION'); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit6f4e260430e62133e311d68422fa94fd::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + return $loader; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 0000000..98b61c2 --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,15 @@ + Date: Mon, 5 Mar 2018 13:54:50 -0500 Subject: [PATCH 24/27] Remove vendor files --- vendor/autoload.php | 7 - vendor/composer/ClassLoader.php | 413 ------------------------ vendor/composer/LICENSE | 21 -- vendor/composer/autoload_classmap.php | 9 - vendor/composer/autoload_namespaces.php | 9 - vendor/composer/autoload_psr4.php | 9 - vendor/composer/autoload_real.php | 52 --- vendor/composer/autoload_static.php | 15 - 8 files changed, 535 deletions(-) delete mode 100644 vendor/autoload.php delete mode 100644 vendor/composer/ClassLoader.php delete mode 100644 vendor/composer/LICENSE delete mode 100644 vendor/composer/autoload_classmap.php delete mode 100644 vendor/composer/autoload_namespaces.php delete mode 100644 vendor/composer/autoload_psr4.php delete mode 100644 vendor/composer/autoload_real.php delete mode 100644 vendor/composer/autoload_static.php diff --git a/vendor/autoload.php b/vendor/autoload.php deleted file mode 100644 index ac1a3c3..0000000 --- a/vendor/autoload.php +++ /dev/null @@ -1,7 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Autoload; - -/** - * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. - * - * $loader = new \Composer\Autoload\ClassLoader(); - * - * // register classes with namespaces - * $loader->add('Symfony\Component', __DIR__.'/component'); - * $loader->add('Symfony', __DIR__.'/framework'); - * - * // activate the autoloader - * $loader->register(); - * - * // to enable searching the include path (eg. for PEAR packages) - * $loader->setUseIncludePath(true); - * - * In this example, if you try to use a class in the Symfony\Component - * namespace or one of its children (Symfony\Component\Console for instance), - * the autoloader will first look for the class under the component/ - * directory, and it will then fallback to the framework/ directory if not - * found before giving up. - * - * This class is loosely based on the Symfony UniversalClassLoader. - * - * @author Fabien Potencier - * @author Jordi Boggiano - * @see http://www.php-fig.org/psr/psr-0/ - * @see http://www.php-fig.org/psr/psr-4/ - */ -class ClassLoader -{ - // PSR-4 - private $prefixLengthsPsr4 = array(); - private $prefixDirsPsr4 = array(); - private $fallbackDirsPsr4 = array(); - - // PSR-0 - private $prefixesPsr0 = array(); - private $fallbackDirsPsr0 = array(); - - private $useIncludePath = false; - private $classMap = array(); - - private $classMapAuthoritative = false; - - public function getPrefixes() - { - if (!empty($this->prefixesPsr0)) { - return call_user_func_array('array_merge', $this->prefixesPsr0); - } - - return array(); - } - - public function getPrefixesPsr4() - { - return $this->prefixDirsPsr4; - } - - public function getFallbackDirs() - { - return $this->fallbackDirsPsr0; - } - - public function getFallbackDirsPsr4() - { - return $this->fallbackDirsPsr4; - } - - public function getClassMap() - { - return $this->classMap; - } - - /** - * @param array $classMap Class to filename map - */ - public function addClassMap(array $classMap) - { - if ($this->classMap) { - $this->classMap = array_merge($this->classMap, $classMap); - } else { - $this->classMap = $classMap; - } - } - - /** - * Registers a set of PSR-0 directories for a given prefix, either - * appending or prepending to the ones previously set for this prefix. - * - * @param string $prefix The prefix - * @param array|string $paths The PSR-0 root directories - * @param bool $prepend Whether to prepend the directories - */ - public function add($prefix, $paths, $prepend = false) - { - if (!$prefix) { - if ($prepend) { - $this->fallbackDirsPsr0 = array_merge( - (array) $paths, - $this->fallbackDirsPsr0 - ); - } else { - $this->fallbackDirsPsr0 = array_merge( - $this->fallbackDirsPsr0, - (array) $paths - ); - } - - return; - } - - $first = $prefix[0]; - if (!isset($this->prefixesPsr0[$first][$prefix])) { - $this->prefixesPsr0[$first][$prefix] = (array) $paths; - - return; - } - if ($prepend) { - $this->prefixesPsr0[$first][$prefix] = array_merge( - (array) $paths, - $this->prefixesPsr0[$first][$prefix] - ); - } else { - $this->prefixesPsr0[$first][$prefix] = array_merge( - $this->prefixesPsr0[$first][$prefix], - (array) $paths - ); - } - } - - /** - * Registers a set of PSR-4 directories for a given namespace, either - * appending or prepending to the ones previously set for this namespace. - * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param array|string $paths The PSR-4 base directories - * @param bool $prepend Whether to prepend the directories - * - * @throws \InvalidArgumentException - */ - public function addPsr4($prefix, $paths, $prepend = false) - { - if (!$prefix) { - // Register directories for the root namespace. - if ($prepend) { - $this->fallbackDirsPsr4 = array_merge( - (array) $paths, - $this->fallbackDirsPsr4 - ); - } else { - $this->fallbackDirsPsr4 = array_merge( - $this->fallbackDirsPsr4, - (array) $paths - ); - } - } elseif (!isset($this->prefixDirsPsr4[$prefix])) { - // Register directories for a new namespace. - $length = strlen($prefix); - if ('\\' !== $prefix[$length - 1]) { - throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); - } - $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = (array) $paths; - } elseif ($prepend) { - // Prepend directories for an already registered namespace. - $this->prefixDirsPsr4[$prefix] = array_merge( - (array) $paths, - $this->prefixDirsPsr4[$prefix] - ); - } else { - // Append directories for an already registered namespace. - $this->prefixDirsPsr4[$prefix] = array_merge( - $this->prefixDirsPsr4[$prefix], - (array) $paths - ); - } - } - - /** - * Registers a set of PSR-0 directories for a given prefix, - * replacing any others previously set for this prefix. - * - * @param string $prefix The prefix - * @param array|string $paths The PSR-0 base directories - */ - public function set($prefix, $paths) - { - if (!$prefix) { - $this->fallbackDirsPsr0 = (array) $paths; - } else { - $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; - } - } - - /** - * Registers a set of PSR-4 directories for a given namespace, - * replacing any others previously set for this namespace. - * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param array|string $paths The PSR-4 base directories - * - * @throws \InvalidArgumentException - */ - public function setPsr4($prefix, $paths) - { - if (!$prefix) { - $this->fallbackDirsPsr4 = (array) $paths; - } else { - $length = strlen($prefix); - if ('\\' !== $prefix[$length - 1]) { - throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); - } - $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = (array) $paths; - } - } - - /** - * Turns on searching the include path for class files. - * - * @param bool $useIncludePath - */ - public function setUseIncludePath($useIncludePath) - { - $this->useIncludePath = $useIncludePath; - } - - /** - * Can be used to check if the autoloader uses the include path to check - * for classes. - * - * @return bool - */ - public function getUseIncludePath() - { - return $this->useIncludePath; - } - - /** - * Turns off searching the prefix and fallback directories for classes - * that have not been registered with the class map. - * - * @param bool $classMapAuthoritative - */ - public function setClassMapAuthoritative($classMapAuthoritative) - { - $this->classMapAuthoritative = $classMapAuthoritative; - } - - /** - * Should class lookup fail if not found in the current class map? - * - * @return bool - */ - public function isClassMapAuthoritative() - { - return $this->classMapAuthoritative; - } - - /** - * Registers this instance as an autoloader. - * - * @param bool $prepend Whether to prepend the autoloader or not - */ - public function register($prepend = false) - { - spl_autoload_register(array($this, 'loadClass'), true, $prepend); - } - - /** - * Unregisters this instance as an autoloader. - */ - public function unregister() - { - spl_autoload_unregister(array($this, 'loadClass')); - } - - /** - * Loads the given class or interface. - * - * @param string $class The name of the class - * @return bool|null True if loaded, null otherwise - */ - public function loadClass($class) - { - if ($file = $this->findFile($class)) { - includeFile($file); - - return true; - } - } - - /** - * Finds the path to the file where the class is defined. - * - * @param string $class The name of the class - * - * @return string|false The path if found, false otherwise - */ - public function findFile($class) - { - // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 - if ('\\' == $class[0]) { - $class = substr($class, 1); - } - - // class map lookup - if (isset($this->classMap[$class])) { - return $this->classMap[$class]; - } - if ($this->classMapAuthoritative) { - return false; - } - - $file = $this->findFileWithExtension($class, '.php'); - - // Search for Hack files if we are running on HHVM - if ($file === null && defined('HHVM_VERSION')) { - $file = $this->findFileWithExtension($class, '.hh'); - } - - if ($file === null) { - // Remember that this class does not exist. - return $this->classMap[$class] = false; - } - - return $file; - } - - private function findFileWithExtension($class, $ext) - { - // PSR-4 lookup - $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; - - $first = $class[0]; - if (isset($this->prefixLengthsPsr4[$first])) { - foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { - if (0 === strpos($class, $prefix)) { - foreach ($this->prefixDirsPsr4[$prefix] as $dir) { - if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { - return $file; - } - } - } - } - } - - // PSR-4 fallback dirs - foreach ($this->fallbackDirsPsr4 as $dir) { - if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { - return $file; - } - } - - // PSR-0 lookup - if (false !== $pos = strrpos($class, '\\')) { - // namespaced class name - $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) - . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); - } else { - // PEAR-like class name - $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; - } - - if (isset($this->prefixesPsr0[$first])) { - foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { - if (0 === strpos($class, $prefix)) { - foreach ($dirs as $dir) { - if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { - return $file; - } - } - } - } - } - - // PSR-0 fallback dirs - foreach ($this->fallbackDirsPsr0 as $dir) { - if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { - return $file; - } - } - - // PSR-0 include paths. - if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { - return $file; - } - } -} - -/** - * Scope isolated include. - * - * Prevents access to $this/self from included files. - */ -function includeFile($file) -{ - include $file; -} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE deleted file mode 100644 index 1a28124..0000000 --- a/vendor/composer/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ - -Copyright (c) 2016 Nils Adermann, Jordi Boggiano - -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: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -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. - diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php deleted file mode 100644 index 7a91153..0000000 --- a/vendor/composer/autoload_classmap.php +++ /dev/null @@ -1,9 +0,0 @@ -= 50600 && !defined('HHVM_VERSION'); - if ($useStaticLoader) { - require_once __DIR__ . '/autoload_static.php'; - - call_user_func(\Composer\Autoload\ComposerStaticInit6f4e260430e62133e311d68422fa94fd::getInitializer($loader)); - } else { - $map = require __DIR__ . '/autoload_namespaces.php'; - foreach ($map as $namespace => $path) { - $loader->set($namespace, $path); - } - - $map = require __DIR__ . '/autoload_psr4.php'; - foreach ($map as $namespace => $path) { - $loader->setPsr4($namespace, $path); - } - - $classMap = require __DIR__ . '/autoload_classmap.php'; - if ($classMap) { - $loader->addClassMap($classMap); - } - } - - $loader->register(true); - - return $loader; - } -} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php deleted file mode 100644 index 98b61c2..0000000 --- a/vendor/composer/autoload_static.php +++ /dev/null @@ -1,15 +0,0 @@ - Date: Mon, 5 Mar 2018 13:57:17 -0500 Subject: [PATCH 25/27] Update comment --- inc/classes/class-settings.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inc/classes/class-settings.php b/inc/classes/class-settings.php index e4efc9c..12cdcdd 100644 --- a/inc/classes/class-settings.php +++ b/inc/classes/class-settings.php @@ -1,6 +1,6 @@ Date: Mon, 5 Mar 2018 14:01:53 -0500 Subject: [PATCH 26/27] Update minimum php version --- voicewp.php | 1 + 1 file changed, 1 insertion(+) diff --git a/voicewp.php b/voicewp.php index 6352333..724f454 100644 --- a/voicewp.php +++ b/voicewp.php @@ -6,6 +6,7 @@ * Author: TomHarrigan * Author URI: https://voicewp.com * Version: 1.0.0 + * Requires PHP: 5.4 * Text Domain: voicewp * License: MIT */ From 0089cf3195733d23b5b22d40b6b2a7d64fd1176b Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Mon, 5 Mar 2018 14:04:04 -0500 Subject: [PATCH 27/27] Update readme --- README.md | 4 +--- inc/class-voicewp-setup.php | 2 +- readme.txt | 12 +++++++----- voicewp.php | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 6908be6..7fbf0a0 100755 --- a/README.md +++ b/README.md @@ -18,14 +18,12 @@ For more on how Alexa skills work in general, [see here](https://developer.amazo ## Requirements -- [Fieldmanager](http://fieldmanager.org). - SSL certificate. -- Minimum version of PHP: 5.3. +- Minimum version of PHP: 5.4. - Minimum version of WordPress: 4.4. ## Installation -- Install and activate [Fieldmanager](https://github.com/alleyinteractive/wordpress-fieldmanager/archive/1.0.0.zip). - Download the .zip file of this repo and upload to your WordPress site by going to WP Admin and navigating to **Plugins -> Add New**. Select the 'Upload Plugin' button near the top of the top of the screen to upload the .zip file. ## Features diff --git a/inc/class-voicewp-setup.php b/inc/class-voicewp-setup.php index 9a0db46..76179d0 100644 --- a/inc/class-voicewp-setup.php +++ b/inc/class-voicewp-setup.php @@ -7,7 +7,7 @@ class Voicewp_Setup { * @var string * @access public */ - public static $version = '1.0.0'; + public static $version = '1.1.0'; protected static $instance; diff --git a/readme.txt b/readme.txt index aa4679d..5fba564 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: tomharrigan, dlh Tags: Alexa, Amazon, fieldmanager Requires at least: 4.4 Tested up to: 4.7.5 -Stable tag: 1.0.0 +Stable tag: 1.1.0 License: MIT WordPress + Amazon Alexa integration @@ -36,12 +36,14 @@ The endpoint for this will be at: `https://yourdomain.com/wp-json/voicewp/v1/ski == Installation == -1. Install the Fieldmanager plugin -2. Have a valid SSL certificate installed -3. Upload and/or activate the VoiceWP plugin +1. Have a valid SSL certificate installed +2. Upload and/or activate the VoiceWP plugin == Changelog == += 1.1.0 = +* Removed Fieldmanager dependency. + = 1.0.0 = * Renamed to VoiceWP * Renames all option, post type, meta prefixes with voicewp @@ -58,4 +60,4 @@ The endpoint for this will be at: `https://yourdomain.com/wp-json/voicewp/v1/ski * Support for PHP 5.3 = 0.1 = -* Initial commit \ No newline at end of file +* Initial commit diff --git a/voicewp.php b/voicewp.php index 724f454..af8a708 100644 --- a/voicewp.php +++ b/voicewp.php @@ -5,7 +5,7 @@ * Plugin URI: https://github.com/tomharrigan/ * Author: TomHarrigan * Author URI: https://voicewp.com - * Version: 1.0.0 + * Version: 1.1.0 * Requires PHP: 5.4 * Text Domain: voicewp * License: MIT