The SEO Framework - Version 4.1.5

Version Description

This minor update adds support for Gutenberg 11.3.0 and fixes a few bugs.

Download this release

Release Info

Developer Cybr
Plugin Icon 128x128 The SEO Framework
Version 4.1.5
Comparing to
See all releases

Code changes from version 4.1.4 to 4.1.5

autodescription.php CHANGED
@@ -3,7 +3,7 @@
3
  * Plugin Name: The SEO Framework
4
  * Plugin URI: https://theseoframework.com/
5
  * Description: An automated, advanced, accessible, unbranded and extremely fast SEO solution for your WordPress website.
6
- * Version: 4.1.4
7
  * Author: The SEO Framework Team
8
  * Author URI: https://theseoframework.com/
9
  * License: GPLv3
@@ -46,7 +46,7 @@ defined( 'ABSPATH' ) or die;
46
  *
47
  * @since 2.3.5
48
  */
49
- define( 'THE_SEO_FRAMEWORK_VERSION', '4.1.4' );
50
 
51
  /**
52
  * The plugin Database version.
3
  * Plugin Name: The SEO Framework
4
  * Plugin URI: https://theseoframework.com/
5
  * Description: An automated, advanced, accessible, unbranded and extremely fast SEO solution for your WordPress website.
6
+ * Version: 4.1.5
7
  * Author: The SEO Framework Team
8
  * Author URI: https://theseoframework.com/
9
  * License: GPLv3
46
  *
47
  * @since 2.3.5
48
  */
49
+ define( 'THE_SEO_FRAMEWORK_VERSION', '4.1.5' );
50
 
51
  /**
52
  * The plugin Database version.
inc/classes/core.class.php CHANGED
@@ -229,6 +229,9 @@ class Core {
229
  * Gets view location.
230
  *
231
  * @since 3.1.0
 
 
 
232
  *
233
  * @param string $file The file name.
234
  * @return string The view location.
229
  * Gets view location.
230
  *
231
  * @since 3.1.0
232
+ * @access private
233
+ * @TODO add path traversal mitigation via realpath()?
234
+ * -> $file must always be dev-supplied, never user-.
235
  *
236
  * @param string $file The file name.
237
  * @return string The view location.
inc/classes/generate-description.class.php CHANGED
@@ -829,7 +829,7 @@ class Generate_Description extends Generate {
829
  * @since 4.0.5 : 1. Now decodes the excerpt input, improving accuracy, and so that HTML entities at
830
  * the end won't be transformed into gibberish.
831
  * @since 4.1.0 : 1. Now texturizes the excerpt input, improving accuracy with included closing & final punctuation support.
832
- * 2. Now performs even faster queries, in most situations. (0.2ms/0.02ms total (worst/best) @ PHP 7.3/PCRE 11 ).
833
  * Mind you, this method probably boots PCRE and wptexturize; so, it'll be slower than what we noted--it's
834
  * overhead that otherwise WP, the theme, or other plugin would cause anyway. So, deduct that.
835
  * 3. Now recognizes connector and final punctuations for preliminary sentence bounding.
@@ -840,37 +840,56 @@ class Generate_Description extends Generate {
840
  * 7. It will now stop counting trailing words towards new sentences when a connector, dash, mark, or ¡¿ is found.
841
  * 8. Now returns encoded entities once more. So that the return value can be treated the same as anything else
842
  * revolving around descriptions--preventing double transcoding like `& > & > &` instead of `&`.
 
 
 
 
 
843
  * @see https://secure.php.net/manual/en/regexp.reference.unicode.php
844
  *
845
  * We use `[^\P{Po}\'\"]` because WordPress texturizes ' and " to fall under `\P{Po}`.
846
  * This is perfect. Please have the courtesy to credit us when taking it. :)
847
  *
848
  * @param string $excerpt The untrimmed excerpt. Expected not to contain any HTML operators.
849
- * @param int $depr The current excerpt length. No longer needed. Deprecated.
 
850
  * @param int $max_char_length At what point to shave off the excerpt.
851
  * @return string The trimmed excerpt with encoded entities. Needs escaping prior printing.
852
  */
853
- public function trim_excerpt( $excerpt, $depr = 0, $max_char_length = 0 ) {
 
 
 
 
 
 
854
 
855
  // Decode to get a more accurate character length in Unicode.
856
  $excerpt = html_entity_decode( $excerpt, ENT_QUOTES, 'UTF-8' );
857
 
858
  // Find all words with $max_char_length, and trim when the last word boundary or punctuation is found.
859
- preg_match( sprintf( '/.{0,%d}([^\P{Po}\'\":]|[\p{Pc}\p{Pd}\p{Pf}\p{Z}]|$){1}/su', $max_char_length ), trim( $excerpt ), $matches );
860
  $excerpt = isset( $matches[0] ) ? ( $matches[0] ?: '' ) : '';
861
 
862
  $excerpt = trim( $excerpt );
863
 
864
- if ( ! $excerpt ) return '';
865
 
866
  // Texturize to recognize the sentence structure. Decode thereafter since we get HTML returned.
867
  $excerpt = htmlentities( $excerpt, ENT_QUOTES, 'UTF-8' );
868
  $excerpt = \wptexturize( $excerpt );
869
  $excerpt = html_entity_decode( $excerpt, ENT_QUOTES, 'UTF-8' );
870
  /**
871
- * Play with it here: https://regex101.com/r/u0DIgx/5/tests
 
 
872
  *
873
- * TODO fix `.*[\p{Pe}\p{Pf}]$`... it suffers from a backtracing issue. I still can't seem to anchor it...
 
 
 
 
 
874
  *
875
  * Critically optimized, so the $matches don't make much sense. Bear with me:
876
  *
@@ -884,7 +903,7 @@ class Generate_Description extends Generate {
884
  * }
885
  */
886
  preg_match(
887
- '/(?:^[\p{P}\p{Z}]*?)([\P{Po}\p{M}\xBF\xA1:\p{Z}]+[\p{Z}\w])(?:([^\P{Po}\p{M}\xBF\xA1:]$(*ACCEPT))|((?(?=.+?\p{Z}*(?:\w+[\p{Pc}\p{Pd}\p{Pf}\p{Z}]*){1,3}|[\p{Po}]$)(?:.*[\p{Pe}\p{Pf}]$|.*[^\P{Po}\p{M}\xBF\xA1:])|.*$(*ACCEPT)))(?>(.+?\p{Z}*(?:\w+[\p{Pc}\p{Pd}\p{Pf}\p{Z}]*){1,3})|[^\p{Pc}\p{Pd}\p{M}\xBF\xA1:])?)(.+)?/su',
888
  $excerpt,
889
  $matches
890
  );
@@ -900,6 +919,8 @@ class Generate_Description extends Generate {
900
  $excerpt = $matches[1];
901
  }
902
 
 
 
903
  /**
904
  * @param array $matches: {
905
  * 1 : Full match until leading punctuation.
@@ -923,6 +944,8 @@ class Generate_Description extends Generate {
923
  $excerpt = '';
924
  }
925
 
 
 
926
  return trim( htmlentities( $excerpt, ENT_QUOTES, 'UTF-8' ) );
927
  }
928
 
829
  * @since 4.0.5 : 1. Now decodes the excerpt input, improving accuracy, and so that HTML entities at
830
  * the end won't be transformed into gibberish.
831
  * @since 4.1.0 : 1. Now texturizes the excerpt input, improving accuracy with included closing & final punctuation support.
832
+ * 2. Now performs even faster queries, in most situations. (0.2ms/0.02ms total (worst/best) @ PHP 7.3/PCRE 11).
833
  * Mind you, this method probably boots PCRE and wptexturize; so, it'll be slower than what we noted--it's
834
  * overhead that otherwise WP, the theme, or other plugin would cause anyway. So, deduct that.
835
  * 3. Now recognizes connector and final punctuations for preliminary sentence bounding.
840
  * 7. It will now stop counting trailing words towards new sentences when a connector, dash, mark, or ¡¿ is found.
841
  * 8. Now returns encoded entities once more. So that the return value can be treated the same as anything else
842
  * revolving around descriptions--preventing double transcoding like `& > & > &` instead of `&`.
843
+ * @since 4.1.5 : 1. The second parameter now accepts values again. From "current description length" to minimum accepted char length.
844
+ * 2. Can now return an empty string when the input string doesn't satisfy the minimum character length.
845
+ * 3. The third parameter now defaults to 4096, so no longer unexpected results are created.
846
+ * 4. Resolved some backtracking issues.
847
+ * 5. Resolved an issue where a character followed by punctuation would cause the match to fail.
848
  * @see https://secure.php.net/manual/en/regexp.reference.unicode.php
849
  *
850
  * We use `[^\P{Po}\'\"]` because WordPress texturizes ' and " to fall under `\P{Po}`.
851
  * This is perfect. Please have the courtesy to credit us when taking it. :)
852
  *
853
  * @param string $excerpt The untrimmed excerpt. Expected not to contain any HTML operators.
854
+ * @param int $min_char_length The minimum character length. Set to 0 to ignore the requirement.
855
+ * This is read as a SUGGESTION. Multibyte characters will create inaccuracies.
856
  * @param int $max_char_length At what point to shave off the excerpt.
857
  * @return string The trimmed excerpt with encoded entities. Needs escaping prior printing.
858
  */
859
+ public function trim_excerpt( $excerpt, $min_char_length = 1, $max_char_length = 4096 ) {
860
+
861
+ // We should _actually_ use mb_strlen, but that's wasteful on resources for something benign.
862
+ // We'll rectify that later, somewhat, where characters are transformed.
863
+ // We could also use preg_match_all( '/./u' ); or count( preg_split( '/./u', $excerpt, $min_char_length ) );
864
+ // But, again, that'll eat CPU cycles.
865
+ if ( \strlen( $excerpt ) < $min_char_length ) return '';
866
 
867
  // Decode to get a more accurate character length in Unicode.
868
  $excerpt = html_entity_decode( $excerpt, ENT_QUOTES, 'UTF-8' );
869
 
870
  // Find all words with $max_char_length, and trim when the last word boundary or punctuation is found.
871
+ preg_match( sprintf( '/.{0,%d}([^\P{Po}\'\":]|[\p{Pc}\p{Pd}\p{Pf}\p{Z}]|\Z){1}/su', $max_char_length ), trim( $excerpt ), $matches );
872
  $excerpt = isset( $matches[0] ) ? ( $matches[0] ?: '' ) : '';
873
 
874
  $excerpt = trim( $excerpt );
875
 
876
+ if ( \strlen( $excerpt ) < $min_char_length ) return '';
877
 
878
  // Texturize to recognize the sentence structure. Decode thereafter since we get HTML returned.
879
  $excerpt = htmlentities( $excerpt, ENT_QUOTES, 'UTF-8' );
880
  $excerpt = \wptexturize( $excerpt );
881
  $excerpt = html_entity_decode( $excerpt, ENT_QUOTES, 'UTF-8' );
882
  /**
883
+ * Play with it here: https://regex101.com/r/u0DIgx/5/ (old) https://regex101.com/r/G92lUt/3 (new)
884
+ *
885
+ * TODO Group 4's match is repeated. However, referring to it as (4) will cause it to congeal into 3.
886
  *
887
+ * TODO .+[\p{Pe}\p{Pf}](*THEN)\Z still backtracks; it should just find \Z and see if one char is in front of it.
888
+ * -> [^\p{Pe}\p{Pf}]++.*?[\p{Pe}\p{Pf}]+?\Z would solve it... but I don't trust it; it's populating 4 and 5 in edge-cases.
889
+ *
890
+ * TODO we can futher optimize this by capturing the last 4 words and refer to that. Of thence more than 3 words
891
+ * found, we could simply end the query, mitigating all forms of backtracking. For now, backtracking cannot
892
+ * exceed step-count=($max_char_length*2+56) = 160*2+56 = 376, which is perfectly acceptable as a 'worst case'.
893
  *
894
  * Critically optimized, so the $matches don't make much sense. Bear with me:
895
  *
903
  * }
904
  */
905
  preg_match(
906
+ '/(?:\A[\p{P}\p{Z}]*?)?([\P{Po}\p{M}\xBF\xA1:\p{Z}]+[\p{Z}\w])(?:([^\P{Po}\p{M}\xBF\xA1:]\Z(*ACCEPT))|((?(?=.+(?:\w+[\p{Pc}\p{Pd}\p{Pf}\p{Z}]*){1,3}|[\p{Po}]\Z)(?:.+[\p{Pe}\p{Pf}](*THEN)\Z(*ACCEPT)|.*[^\P{Po}\p{M}\xBF\xA1:])|.*\Z(*ACCEPT)))(?>(.+?\p{Z}*(?:\w+[\p{Pc}\p{Pd}\p{Pf}\p{Z}]*){1,3})|[^\p{Pc}\p{Pd}\p{M}\xBF\xA1:])?)(.+)?/su',
907
  $excerpt,
908
  $matches
909
  );
919
  $excerpt = $matches[1];
920
  }
921
 
922
+ if ( \strlen( $excerpt ) < $min_char_length ) return '';
923
+
924
  /**
925
  * @param array $matches: {
926
  * 1 : Full match until leading punctuation.
944
  $excerpt = '';
945
  }
946
 
947
+ if ( \strlen( $excerpt ) < $min_char_length ) return '';
948
+
949
  return trim( htmlentities( $excerpt, ENT_QUOTES, 'UTF-8' ) );
950
  }
951
 
inc/classes/generate-url.class.php CHANGED
@@ -801,9 +801,8 @@ class Generate_Url extends Generate_Title {
801
  */
802
  public function _adjust_post_link_category( $term, $terms = null, $post = null ) {
803
 
804
- if ( null === $post ) {
805
  $post = \get_post( $this->get_the_real_ID() );
806
- }
807
 
808
  return $this->get_primary_term( $post->ID, $term->taxonomy ) ?: $term;
809
  }
801
  */
802
  public function _adjust_post_link_category( $term, $terms = null, $post = null ) {
803
 
804
+ if ( null === $post )
805
  $post = \get_post( $this->get_the_real_ID() );
 
806
 
807
  return $this->get_primary_term( $post->ID, $term->taxonomy ) ?: $term;
808
  }
inc/classes/post-data.class.php CHANGED
@@ -298,6 +298,7 @@ class Post_Data extends Detect {
298
  if ( $value || ( \is_string( $value ) && \strlen( $value ) ) ) {
299
  \update_post_meta( $post->ID, $field, $value );
300
  } else {
 
301
  // This is fine for as long as we merge the getter values with the defaults.
302
  \delete_post_meta( $post->ID, $field );
303
  }
@@ -557,10 +558,10 @@ class Post_Data extends Detect {
557
  ];
558
  }
559
 
560
- foreach ( $values as $t => $v ) {
561
  if ( ! isset( $_POST[ $v['name'] ] ) ) continue;
562
  if ( \wp_verify_nonce( $_POST[ $v['name'] ], $v['action'] ) ) { // Redundant. Fortified.
563
- $this->update_primary_term_id( $post->ID, $t, $v['value'] );
564
  }
565
  }
566
  }
@@ -820,20 +821,29 @@ class Post_Data extends Detect {
820
  * Returns the primary term for post.
821
  *
822
  * @since 3.0.0
 
 
823
  *
824
- * @param int|null $post_id The post ID.
825
- * @param string $taxonomy The taxonomy name.
826
  * @return \WP_Term|false The primary term. False if not set.
827
  */
828
- public function get_primary_term( $post_id = null, $taxonomy = '' ) {
 
 
829
 
830
- $primary_id = $this->get_primary_term_id( $post_id, $taxonomy );
 
831
 
832
- if ( ! $primary_id ) return false;
 
 
833
 
834
  // Users can alter the term list via quick/bulk edit, but cannot set a primary term that way.
835
  // Users can also delete a term from the site that was previously assigned as primary.
836
  // So, test if the term still exists for the post.
 
 
837
  $terms = \get_the_terms( $post_id, $taxonomy );
838
  $primary_term = false;
839
 
@@ -844,20 +854,23 @@ class Post_Data extends Detect {
844
  }
845
  }
846
 
847
- return $primary_term;
848
  }
849
 
850
  /**
851
  * Returns the primary term ID for post.
852
  *
853
  * @since 3.0.0
 
 
854
  *
855
- * @param int|null $post_id The post ID.
856
- * @param string $taxonomy The taxonomy name.
857
- * @return int The primary term ID. 0 if not set.
858
  */
859
- public function get_primary_term_id( $post_id = null, $taxonomy = '' ) {
860
- return (int) \get_post_meta( $post_id, '_primary_term_' . $taxonomy, true ) ?: 0;
 
861
  }
862
 
863
  /**
@@ -871,7 +884,7 @@ class Post_Data extends Detect {
871
  * @return bool True on success, false on failure.
872
  */
873
  public function update_primary_term_id( $post_id = null, $taxonomy = '', $value = 0 ) {
874
- if ( empty( $value ) ) {
875
  $success = \delete_post_meta( $post_id, '_primary_term_' . $taxonomy );
876
  } else {
877
  $success = \update_post_meta( $post_id, '_primary_term_' . $taxonomy, $value );
298
  if ( $value || ( \is_string( $value ) && \strlen( $value ) ) ) {
299
  \update_post_meta( $post->ID, $field, $value );
300
  } else {
301
+ // All empty values are deleted here, even if they never existed... is this the best way to handle this?
302
  // This is fine for as long as we merge the getter values with the defaults.
303
  \delete_post_meta( $post->ID, $field );
304
  }
558
  ];
559
  }
560
 
561
+ foreach ( $values as $_taxonomy => $v ) {
562
  if ( ! isset( $_POST[ $v['name'] ] ) ) continue;
563
  if ( \wp_verify_nonce( $_POST[ $v['name'] ], $v['action'] ) ) { // Redundant. Fortified.
564
+ $this->update_primary_term_id( $post->ID, $_taxonomy, $v['value'] );
565
  }
566
  }
567
  }
821
  * Returns the primary term for post.
822
  *
823
  * @since 3.0.0
824
+ * @since 4.1.5 1. Added memoization.
825
+ * 2. The first and second parameters are now required.
826
  *
827
+ * @param int $post_id The post ID.
828
+ * @param string $taxonomy The taxonomy name.
829
  * @return \WP_Term|false The primary term. False if not set.
830
  */
831
+ public function get_primary_term( $post_id, $taxonomy ) {
832
+
833
+ static $primary_terms = [];
834
 
835
+ if ( isset( $primary_terms[ $post_id ][ $taxonomy ] ) )
836
+ return $primary_terms[ $post_id ][ $taxonomy ];
837
 
838
+ $primary_id = \get_post_meta( $post_id, '_primary_term_' . $taxonomy, true ) ?: 0;
839
+
840
+ if ( ! $primary_id ) return $primary_terms[ $post_id ][ $taxonomy ] = false;
841
 
842
  // Users can alter the term list via quick/bulk edit, but cannot set a primary term that way.
843
  // Users can also delete a term from the site that was previously assigned as primary.
844
  // So, test if the term still exists for the post.
845
+ // Although 'get_the_terms()' is an expensive function, it memoizes, and
846
+ // is always called by WP before we fetch a primary term. So, 0 overhead here.
847
  $terms = \get_the_terms( $post_id, $taxonomy );
848
  $primary_term = false;
849
 
854
  }
855
  }
856
 
857
+ return $primary_terms[ $post_id ][ $taxonomy ] = $primary_term;
858
  }
859
 
860
  /**
861
  * Returns the primary term ID for post.
862
  *
863
  * @since 3.0.0
864
+ * @since 4.1.5 1. Now validates if the stored term ID's term exists (for the post or at all).
865
+ * 2. The first and second parameters are now required.
866
  *
867
+ * @param int $post_id The post ID.
868
+ * @param string $taxonomy The taxonomy name.
869
+ * @return int The primary term ID. 0 if not found.
870
  */
871
+ public function get_primary_term_id( $post_id, $taxonomy ) {
872
+ $primary_term = $this->get_primary_term( $post_id, $taxonomy );
873
+ return isset( $primary_term->term_id ) ? $primary_term->term_id : 0;
874
  }
875
 
876
  /**
884
  * @return bool True on success, false on failure.
885
  */
886
  public function update_primary_term_id( $post_id = null, $taxonomy = '', $value = 0 ) {
887
+ if ( ! $value ) {
888
  $success = \delete_post_meta( $post_id, '_primary_term_' . $taxonomy );
889
  } else {
890
  $success = \update_post_meta( $post_id, '_primary_term_' . $taxonomy, $value );
lib/js/gbc.js CHANGED
@@ -329,9 +329,11 @@ window.tsfGBC = function( $ ) {
329
  */
330
  const _initCompat = () => {
331
 
332
- wp.data.subscribe( debounce( sidebarDispatcher, 500 ) );
333
- wp.data.subscribe( debounce( assessData, 300 ) );
334
- wp.data.subscribe( saveDispatcher );
 
 
335
 
336
  // Set all values prior debouncing.
337
  setTimeout( () => {
329
  */
330
  const _initCompat = () => {
331
 
332
+ const { subscribe } = wp.data;
333
+
334
+ subscribe( debounce( sidebarDispatcher, 500 ) );
335
+ subscribe( debounce( assessData, 300 ) );
336
+ subscribe( saveDispatcher );
337
 
338
  // Set all values prior debouncing.
339
  setTimeout( () => {
lib/js/gbc.min.js CHANGED
@@ -1 +1 @@
1
- 'use strict';window.tsfGBC=function(a){function b(a){return k.getEditedPostAttribute(a)}function c(){n={title:b("title"),link:k.getPermalink(),content:b("content"),excerpt:b("excerpt"),visibility:k.getEditedPostVisibility()}}function d(a){return n[a]||null}function e(){let a=n;c(),a.title!==n.title&&o("title"),a.link!==n.link&&o("link"),a.content!==n.content&&o("content"),a.excerpt!==n.excerpt&&o("excerpt"),a.visibility!==n.visibility&&o("visibility")}function f(){p?k.didPostSaveRequestSucceed()?(s(),r()&&r().cancel(),g()):r():k.isSavingPost()&&(k.isPreviewingPost()?(p=!0,q="preview"):k.isAutosavingPost()?(p=!0,q="autosave"):(p=!0,q="save"))}function g(){p=!1}function h(){if(k.isPostSavingLocked())3>++t?s():(s()&&s().cancel(),t=0);else{t=0;let a=!k.hasChangedContent();"preview"===q?document.dispatchEvent(new CustomEvent("tsf-gutenberg-onpreview")):"autosave"===q?document.dispatchEvent(new CustomEvent("tsf-gutenberg-onautosave")):"save"===q?a=!0:void 0;a&&document.dispatchEvent(new CustomEvent("tsf-gutenberg-onsave"))&&document.dispatchEvent(new CustomEvent("tsf-gutenberg-onsave-completed")),document.dispatchEvent(new CustomEvent("tsf-gutenberg-saved-document",{detail:{savedType:q}})),q=""}}function i(){l.isEditorSidebarOpened()?!u.opened&&(u.opened=!0,document.dispatchEvent(new CustomEvent("tsf-gutenberg-sidebar-opened"))):u.opened&&(u.opened=!1,document.dispatchEvent(new CustomEvent("tsf-gutenberg-sidebar-closed")))}const j="undefined"!=typeof tsfGBCL10n&&tsfGBCL10n,k=wp.data.select("core/editor"),l=wp.data.select("core/edit-post"),{debounce:m}=lodash;let n;const o=b=>{a(document).trigger("tsf-updated-gutenberg-"+b,[d(b)])};let p=!1,q="";const r=m(g,7e3),s=m(h,500);let t=0,u={opened:!1};const v=()=>{wp.data.subscribe(m(i,500)),wp.data.subscribe(m(e,300)),wp.data.subscribe(f),setTimeout(()=>{c(),o("title"),o("link"),o("content"),o("excerpt"),o("visibility"),document.dispatchEvent(new CustomEvent("tsf-subscribed-to-gutenberg"))})};return Object.assign({load:()=>{document.body.addEventListener("tsf-onload",v)}},{triggerUpdate:o},{l10n:j})}(jQuery),window.tsfGBC.load();
1
+ 'use strict';window.tsfGBC=function(a){function b(a){return k.getEditedPostAttribute(a)}function c(){n={title:b("title"),link:k.getPermalink(),content:b("content"),excerpt:b("excerpt"),visibility:k.getEditedPostVisibility()}}function d(a){return n[a]||null}function e(){let a=n;c(),a.title!==n.title&&o("title"),a.link!==n.link&&o("link"),a.content!==n.content&&o("content"),a.excerpt!==n.excerpt&&o("excerpt"),a.visibility!==n.visibility&&o("visibility")}function f(){p?k.didPostSaveRequestSucceed()?(s(),r()&&r().cancel(),g()):r():k.isSavingPost()&&(k.isPreviewingPost()?(p=!0,q="preview"):k.isAutosavingPost()?(p=!0,q="autosave"):(p=!0,q="save"))}function g(){p=!1}function h(){if(k.isPostSavingLocked())3>++t?s():(s()&&s().cancel(),t=0);else{t=0;let a=!k.hasChangedContent();"preview"===q?document.dispatchEvent(new CustomEvent("tsf-gutenberg-onpreview")):"autosave"===q?document.dispatchEvent(new CustomEvent("tsf-gutenberg-onautosave")):"save"===q?a=!0:void 0;a&&document.dispatchEvent(new CustomEvent("tsf-gutenberg-onsave"))&&document.dispatchEvent(new CustomEvent("tsf-gutenberg-onsave-completed")),document.dispatchEvent(new CustomEvent("tsf-gutenberg-saved-document",{detail:{savedType:q}})),q=""}}function i(){l.isEditorSidebarOpened()?!u.opened&&(u.opened=!0,document.dispatchEvent(new CustomEvent("tsf-gutenberg-sidebar-opened"))):u.opened&&(u.opened=!1,document.dispatchEvent(new CustomEvent("tsf-gutenberg-sidebar-closed")))}const j="undefined"!=typeof tsfGBCL10n&&tsfGBCL10n,k=wp.data.select("core/editor"),l=wp.data.select("core/edit-post"),{debounce:m}=lodash;let n;const o=b=>{a(document).trigger("tsf-updated-gutenberg-"+b,[d(b)])};let p=!1,q="";const r=m(g,7e3),s=m(h,500);let t=0,u={opened:!1};const v=()=>{const{subscribe:a}=wp.data;a(m(i,500)),a(m(e,300)),a(f),setTimeout(()=>{c(),o("title"),o("link"),o("content"),o("excerpt"),o("visibility"),document.dispatchEvent(new CustomEvent("tsf-subscribed-to-gutenberg"))})};return Object.assign({load:()=>{document.body.addEventListener("tsf-onload",v)}},{triggerUpdate:o},{l10n:j})}(jQuery),window.tsfGBC.load();
lib/js/pt-gb.js CHANGED
@@ -30,6 +30,7 @@
30
  * This is a self-constructed function assigned as an object.
31
  *
32
  * @since 3.2.0
 
33
  *
34
  * @constructor
35
  * @param {!jQuery} $ jQuery object.
@@ -46,18 +47,30 @@ window.tsfPTGB = function( $ ) {
46
  const l10n = 'undefined' !== typeof tsfPTL10n && tsfPTL10n;
47
 
48
  /**
49
- * @since 3.2.0
 
 
 
 
 
 
50
  * @access private
51
  */
52
- const { addFilter } = wp.hooks;
53
- const { createElement, Fragment } = wp.element;
54
  const { SelectControl } = wp.components;
55
- const { addQueryArgs } = wp.url;
56
- const apiFetch = wp.apiFetch;
57
- const { invoke, unescape } = lodash;
58
 
59
  /**
60
- * @since 3.2.0
 
 
 
 
 
 
 
 
61
  * @access private
62
  */
63
  const DEFAULT_QUERY = {
@@ -68,287 +81,215 @@ window.tsfPTGB = function( $ ) {
68
  };
69
 
70
  /**
71
- * Initializes primary term selection for gutenberg.
72
- *
73
- * @since 3.2.0
74
  * @access private
75
- *
76
- * @function
77
- * @return {undefined}
78
  */
79
- const _initPrimaryTerm = () => {
80
 
81
- if ( ! Object.keys( l10n.taxonomies ).length )
82
- return;
 
 
 
 
 
 
83
 
84
- let taxonomies = l10n.taxonomies,
85
- inputTemplate = wp.template( 'tsf-primary-term-selector' ),
86
- registeredFields = {};
87
 
88
- const geti18n = ( taxonomy, what ) => what in taxonomies[ taxonomy ].i18n && taxonomies[ taxonomy ].i18n[ what ] || '';
 
89
 
90
- const getPrimaryTermHolder = taxonomy => document.getElementById( `autodescription[_primary_term_${taxonomy}]` );
91
- const getPrimaryTermID = taxonomy => +getPrimaryTermHolder( taxonomy ).value;
92
- const setPrimaryTermID = ( taxonomy, id ) => +( getPrimaryTermHolder( taxonomy ).value = +id );
93
 
94
- const addDataInput = ( taxonomy ) => {
95
- let wrap = document.getElementById( 'tsf-gutenberg-data-holder' );
96
- if ( ! wrap ) return registeredFields[ taxonomy ] = false;
97
 
98
- let template = inputTemplate( { 'taxonomy' : taxonomies[ taxonomy ] } );
99
- return registeredFields[ taxonomy ] = !! $( template ).appendTo( wrap );
100
- }
101
- const hasDataInput = ( taxonomy ) => registeredFields[ taxonomy ];
102
-
103
- const revalidatePrimaryTerm = ( taxonomy, terms ) => {
104
- if ( terms.indexOf( getPrimaryTermID( taxonomy ) ) < 0 ) {
105
- // Set to first found term, or empty the value if no term is selected.
106
- if ( 0 in terms ) {
107
- setPrimaryTermID( taxonomy, terms[0] );
108
- } else {
109
- setPrimaryTermID( taxonomy, 0 );
110
- }
111
- }
112
  }
113
 
114
- var dataStores = {};
115
- class DataStore {
116
- constructor( slug ) {
117
- this.slug = slug;
118
- this.reset();
119
- }
120
 
121
- registeredData() {
122
- return !! Object.keys( this.data ).length;
123
- }
 
124
 
125
- reset() {
126
- this.data = {};
127
- }
128
 
129
- read() {
130
- return this.data;
131
- }
 
 
 
 
 
 
 
132
 
133
- get( what ) {
134
- return what in this.data && this.data[ what ] || null;
135
- }
136
 
137
- set( what, value ) {
138
- return this.data[ what ] = value;
139
- }
140
- }
141
- const createStore = slug => dataStores[ slug ] = new DataStore( slug );
142
- const getStore = slug => dataStores[ slug ] || createStore( slug );
143
-
144
- class PrimaryTermSelectorHandler extends React.Component {
145
- componentDidMount() {
146
- // Mounted for the first time.
147
- if ( ! this.dsAccess().registeredData() )
148
- this.registerData();
149
-
150
- if ( this.dsAccess().registeredData() ) {
151
- // Remounted thanks to adding a new term (from 1 selected, now 2).
152
- if ( this.hasNewTerms() ) {
153
- this.fetchTerms();
154
- } else {
155
- this.setState( {
156
- loading: false,
157
- } );
158
- }
159
- }
160
- }
161
 
162
- componentWillUnmount() {
163
- invoke( this.fetchRequest, [ 'abort' ] );
164
- }
165
 
166
- componentDidUpdate( prevProps, prevState ) {
167
- // No noteworthy update occurred. Probably, only the state updated.
168
- if ( prevProps.terms === this.props.terms ) return;
169
 
170
- if ( this.props.terms.length > 1 ) {
171
- if ( ! this.dsAccess().get( 'availableTerms' ) || this.hasNewTerms() ) {
172
- this.fetchTerms();
173
- }
174
- }
175
- }
 
 
 
 
176
 
177
- registerData() {
178
- this.dsAccess().set( 'availableTerms', [] );
179
- this.fetchTerms();
180
- }
181
-
182
- dsAccess() {
183
- return getStore( this.props.slug );
184
- }
185
-
186
- hasNewTerms() {
187
- let availableTermsIds = ( this.dsAccess().get( 'availableTerms' ) || [] ).map( x => x.id );
188
- return ! this.props.terms.every( id => availableTermsIds.includes( id ) );
189
- }
190
- isTermAvailable( id ) {
191
- return this.dsAccess().get( 'availableTerms' ).some( term => term.id === id );
192
- }
193
 
194
- fetchTerms() {
195
- const { taxonomy } = this.props;
196
- if ( ! taxonomy ) return;
 
 
 
 
 
 
197
 
198
- this.setState( {
199
- loading: true,
200
- } );
 
 
 
 
 
 
 
 
 
 
201
 
202
- // Abort previous, if any? WP doesn't do that either...
203
- this.fetchRequest = apiFetch( {
204
- path: addQueryArgs(
205
- `/wp/v2/${ taxonomy.rest_base }`,
206
- DEFAULT_QUERY
207
- ),
208
- } );
209
- this.fetchRequest.then(
210
- ( terms ) => { // resolve
211
- this.fetchRequest = null;
212
- this.setState( {
213
- loading: false,
214
- } );
215
- this.dsAccess().set( 'availableTerms', terms );
216
- this.forceUpdate();
217
- },
218
- ( xhr ) => { // reject
219
- if ( xhr.statusText === 'abort' ) {
220
- return;
 
 
 
 
 
 
 
221
  }
222
- this.fetchRequest = null;
223
- this.setState( {
224
- loading: false,
225
- } );
226
- // Users may see empty select fields now... Strip them? See getTermName instead?
227
- // if ( ! this.dsAccess().get( 'availableTerms' ).length ) {}
228
  }
229
- );
230
- }
231
- }
232
-
233
- class TermSelector extends PrimaryTermSelectorHandler {
234
- constructor() {
235
- super( ...arguments );
236
- this.onChange = this.onChange.bind( this );
237
- this.state = {
238
- loading: true,
239
- }
240
- }
241
-
242
- getTermName( id ) {
243
- let availableTerms = this.dsAccess().get( 'availableTerms' );
244
 
245
- if ( ! Array.isArray( availableTerms ) ) return '';
246
-
247
- let term = availableTerms.find( term => term.id === id );
248
- return term && term.name || '';
249
- }
250
-
251
- getSelectOptions() {
252
- // Terms might not've been registered (yet).
253
- if ( ! Array.isArray( this.props.terms ) ) return '';
254
 
255
- return this.props.terms.sort().map( id => {
256
- return {
257
- value: id,
258
- // unescape is also in tsf.js. Prevents double-escape, since React re-escapes.
259
- label: unescape( this.getTermName( id ) )
260
- };
261
- } );
262
  }
263
 
264
- onChange( value ) {
265
- this.setState( {
266
- options: this.getSelectOptions(),
267
- value: setPrimaryTermID( this.props.slug, value )
268
- } );
269
  'tsfAys' in window && tsfAys.registerChange();
270
- }
271
-
272
- isDisabled() {
273
- // Terms might not've been registered (yet).
274
- if ( this.state.loading || ! Array.isArray( this.props.terms ) )
275
- return true;
276
-
277
- let availableTerms = this.dsAccess().get( 'availableTerms' );
278
- return ! Array.isArray( availableTerms ) || ! availableTerms.length;
279
- }
280
-
281
- render() {
282
- // React causes this to loop back to render() once because of debugRenderPhaseSideEffectsForStrictMode...
283
- return createElement(
284
- SelectControl,
285
- {
286
- label: geti18n( this.props.slug, 'selectPrimary' ),
287
- value: getPrimaryTermID( this.props.slug ),
288
- className: 'tsf-pt-gb-selector',
289
- onChange: this.onChange,
290
- options: this.getSelectOptions(),
291
- disabled: this.isDisabled(),
292
- },
293
- );
294
- }
 
 
295
  }
296
 
297
- const primaryTermSelectorFilter = PostTaxonomyType => class extends React.Component {
298
- initSelectors() {
299
- const { slug, terms } = this.props;
300
- if ( hasDataInput( slug ) ) {
301
- revalidatePrimaryTerm( slug, terms );
302
- if ( terms.length > 1 ) {
303
- return createElement(
304
- Fragment,
305
- {},
306
- createElement(
307
- TermSelector,
308
- this.props,
309
- )
310
- );
311
- }
312
- }
313
- return null;
314
- }
315
-
316
  render() {
317
- if ( ! ( this.props.slug in taxonomies ) ) {
318
- // Return original component.
319
- return createElement(
320
- PostTaxonomyType,
321
- this.props,
322
- );
323
- }
324
-
325
- // React causes this to loop back to render() once because of debugRenderPhaseSideEffectsForStrictMode...
326
  return createElement(
327
  Fragment,
328
- {},
329
  createElement(
330
- PostTaxonomyType,
331
- this.props,
332
  ),
333
- // If we can access PostTaxonomyType from here, we can bypass the API fetch and use its results from state instead.
334
- this.initSelectors()
 
 
 
 
 
335
  );
336
  }
337
  }
338
 
339
- const _init = () => {
340
- for ( let taxonomy in taxonomies ) {
341
- addDataInput( taxonomy );
342
- }
343
 
344
- addFilter(
345
- 'editor.PostTaxonomyType',
346
- 'tsf/pt',
347
- primaryTermSelectorFilter,
348
- 20
349
- );
350
- }
351
- _init();
352
  }
353
 
354
  return Object.assign( {
30
  * This is a self-constructed function assigned as an object.
31
  *
32
  * @since 3.2.0
33
+ * @since 4.1.5 Rewritten to support Gutenberg 11.3.0+ / WP 5.9+
34
  *
35
  * @constructor
36
  * @param {!jQuery} $ jQuery object.
47
  const l10n = 'undefined' !== typeof tsfPTL10n && tsfPTL10n;
48
 
49
  /**
50
+ * @since 4.1.5
51
+ * @access private
52
+ */
53
+ const supportedTaxonomies = l10n?.taxonomies;
54
+
55
+ /**
56
+ * @since 4.1.5
57
  * @access private
58
  */
59
+ const { createElement, Fragment, Component, useState, useEffect } = wp.element;
 
60
  const { SelectControl } = wp.components;
61
+ const { useSelect } = wp.data;
62
+ const { unescape } = lodash;
 
63
 
64
  /**
65
+ * Arrays are unique objects, meaning [] !== [].
66
+ * Let's store one that's immutable, so we have [] === [], preventing state changes.
67
+ * @since 4.1.5
68
+ * @access private
69
+ */
70
+ const EMPTY_ARRAY = [];
71
+
72
+ /**
73
+ * @since 4.1.5
74
  * @access private
75
  */
76
  const DEFAULT_QUERY = {
81
  };
82
 
83
  /**
84
+ * @since 4.1.5
 
 
85
  * @access private
86
+ * @param {String} taxonomySlug
87
+ * @param {String} what The i18n to get.
 
88
  */
89
+ const _geti18n = ( taxonomySlug, what ) => supportedTaxonomies[ taxonomySlug ].i18n[ what ] || '';
90
 
91
+ let _registeredFields = {}; // memo. TODO Make Map()? Meh, this gets called like 3 to 5x per page.
92
+ /**
93
+ * @since 4.1.5
94
+ * @access private
95
+ * @param {String} taxonomySlug
96
+ * @return {(<Object<String,Function>)}
97
+ */
98
+ const _primaryTerm = taxonomySlug => {
99
 
100
+ const _dataHolder = () => document.getElementById( `autodescription[_primary_term_${taxonomySlug}]` );
 
 
101
 
102
+ const get = () => +_dataHolder().value;
103
+ const set = id => +( _dataHolder().value = +id );
104
 
105
+ const revalidate = selectedTerms => {
106
+ let primaryTerm = get();
 
107
 
108
+ if ( selectedTerms.includes( primaryTerm ) )
109
+ return primaryTerm;
 
110
 
111
+ return set( selectedTerms?.[0] || 0 );
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  }
113
 
114
+ const register = () => {
115
+ let wrap = document.getElementById( 'tsf-gutenberg-data-holder' );
116
+ if ( ! wrap ) return _registeredFields[ taxonomySlug ] = false;
 
 
 
117
 
118
+ let template = wp.template( 'tsf-primary-term-selector' )( { taxonomy: supportedTaxonomies[ taxonomySlug ] } );
119
+ return _registeredFields[ taxonomySlug ] = !! $( template ).appendTo( wrap );
120
+ }
121
+ const isRegistered = () => _registeredFields[ taxonomySlug ] || false;
122
 
123
+ return { get, set, revalidate, register, isRegistered };
124
+ };
 
125
 
126
+ /**
127
+ * Initializes primary term selection for Gutenberg.
128
+ *
129
+ * @since 3.2.0
130
+ * @access private
131
+ *
132
+ * @function
133
+ * @return {undefined}
134
+ */
135
+ const _initPrimaryTerm = () => {
136
 
137
+ if ( ! Object.keys( supportedTaxonomies ).length ) return;
 
 
138
 
139
+ function primaryTermSelector( { taxonomySlug, _legacySelectedTerms } ) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
+ const primaryTerm = _primaryTerm( taxonomySlug );
142
+ const [ selection, setSelection ] = useState( primaryTerm.get() );
 
143
 
144
+ // Legacy Gutenberg (<11.3.0) support
145
+ const [ _legacyAvailableTerms, _setLegacyAvailableTerms ] = useState( false );
146
+ const [ _legacyIsResolving, _setLegacyIsResolving ] = useState( false );
147
 
148
+ // Ref: <https://github.com/WordPress/gutenberg/pull/33418#issuecomment-903686737>
149
+ const {
150
+ selectedTerms,
151
+ loading,
152
+ availableTerms,
153
+ _taxonomy,
154
+ } = useSelect(
155
+ select => {
156
+ const { getTaxonomy, getEntityRecords, isResolving } = select( 'core' );
157
+ const { getEditedPostAttribute } = select( 'core/editor' );
158
 
159
+ const _taxonomy = getTaxonomy( taxonomySlug );
160
+ const _query = [ 'taxonomy', taxonomySlug, DEFAULT_QUERY ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
+ return {
163
+ selectedTerms: getEditedPostAttribute( _taxonomy?.rest_base ) || EMPTY_ARRAY, // GB bug: causes 70 useSelect-calls on-load.
164
+ loading: _legacyIsResolving || isResolving( 'getEntityRecords', _query ),
165
+ availableTerms: _legacyAvailableTerms || getEntityRecords( ..._query ) || EMPTY_ARRAY,
166
+ _taxonomy,
167
+ };
168
+ },
169
+ [ taxonomySlug, _legacyIsResolving, _legacyAvailableTerms ]
170
+ );
171
 
172
+ // Forward data to our store based on mutability of "selection".
173
+ // TODO add test for availableTerms.includes( +selection ) -> User deletes terms while in Gutenberg?
174
+ // -> `! availableTerms.map( term => term.id ).includes( id )`
175
+ // = Mega edge case Gutenberg doesn't handle (properly) either.
176
+ useEffect(
177
+ () => {
178
+ if ( ! selectedTerms.includes( +selection ) || primaryTerm.get() !== +selection ) {
179
+ primaryTerm.revalidate( selectedTerms );
180
+ setSelection( primaryTerm.get() );
181
+ }
182
+ },
183
+ [ selectedTerms ]
184
+ );
185
 
186
+ // This effect depends on the mutable state of _legacySelectedTerms.
187
+ useEffect(
188
+ () => {
189
+ // Legacy Gutenberg (<11.3.0) support.
190
+ if ( _taxonomy?.rest_base && _legacySelectedTerms?.length && ! _legacyIsResolving ) {
191
+ if ( ! _legacyAvailableTerms?.length
192
+ // Find differences in stored/selected ids.
193
+ || _legacySelectedTerms.filter( id => ! availableTerms.map( term => term.id ).includes( id ) ).length
194
+ ) {
195
+ _setLegacyIsResolving( true );
196
+ wp.apiFetch(
197
+ {
198
+ path: wp.url?.addQueryArgs(
199
+ `/wp/v2/${ _taxonomy.rest_base }`,
200
+ DEFAULT_QUERY
201
+ ),
202
+ }
203
+ ).then(
204
+ terms => {
205
+ _setLegacyAvailableTerms( terms );
206
+ },
207
+ ).finally(
208
+ () => {
209
+ _setLegacyIsResolving( false );
210
+ }
211
+ );
212
  }
 
 
 
 
 
 
213
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
+ return () => { _setLegacyIsResolving( false ); };
216
+ },
217
+ [ _legacySelectedTerms ]
218
+ );
 
 
 
 
 
219
 
220
+ if ( selectedTerms?.length < 2 ) {
221
+ // Delete entry.
222
+ primaryTerm.set( 0 );
223
+ // Hide selector. Halt function.
224
+ return null;
 
 
225
  }
226
 
227
+ const onChange = termId => {
228
+ if ( ! selectedTerms.includes( +termId ) ) return;
229
+ primaryTerm.set( termId );
230
+ setSelection( primaryTerm.get() );
 
231
  'tsfAys' in window && tsfAys.registerChange();
232
+ };
233
+
234
+ const getSelectOptions = () => {
235
+ return availableTerms.map( term =>
236
+ selectedTerms.includes( term?.id )
237
+ && {
238
+ value: term.id,
239
+ label: unescape( term?.name )
240
+ }
241
+ ).filter( Boolean ) || '';
242
+ };
243
+
244
+ const isDisabled = () => ! ( selectedTerms.length && availableTerms.length && ! loading );
245
+
246
+ return createElement(
247
+ SelectControl,
248
+ {
249
+ label: _geti18n( taxonomySlug, 'selectPrimary' ),
250
+ value: selection,
251
+ className: 'tsf-pt-gb-selector',
252
+ onChange: onChange,
253
+ options: getSelectOptions(),
254
+ disabled: isDisabled(),
255
+ // Yes, it's neater. No, we shouldn't. Not our bug.
256
+ // style: { lineHeight: 'unset' }, // <https://github.com/WordPress/gutenberg/issues/27194>
257
+ },
258
+ );
259
  }
260
 
261
+ const PrimaryTermSelectorFilter = OriginalComponent => class extends Component {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  render() {
263
+ // If we cannot store the primary term for this taxonomy, bail.
264
+ if ( ! _primaryTerm( this.props?.slug ).isRegistered() )
265
+ return createElement( OriginalComponent, { ...this.props } );
266
+
 
 
 
 
 
267
  return createElement(
268
  Fragment,
269
+ null,
270
  createElement(
271
+ OriginalComponent,
272
+ { ...this.props },
273
  ),
274
+ createElement(
275
+ primaryTermSelector,
276
+ {
277
+ taxonomySlug: this.props?.slug,
278
+ _legacySelectedTerms: this.props?.terms,
279
+ },
280
+ )
281
  );
282
  }
283
  }
284
 
285
+ for ( let taxonomySlug in supportedTaxonomies )
286
+ _primaryTerm( taxonomySlug ).register();
 
 
287
 
288
+ wp.hooks.addFilter(
289
+ 'editor.PostTaxonomyType',
290
+ 'tsf/pt',
291
+ PrimaryTermSelectorFilter
292
+ );
 
 
 
293
  }
294
 
295
  return Object.assign( {
lib/js/pt-gb.min.js CHANGED
@@ -1 +1 @@
1
- 'use strict';window.tsfPTGB=function(a){const b="undefined"!=typeof tsfPTL10n&&tsfPTL10n,{addFilter:c}=wp.hooks,{createElement:d,Fragment:e}=wp.element,{SelectControl:f}=wp.components,{addQueryArgs:g}=wp.url,h=wp.apiFetch,{invoke:i,unescape:j}=lodash,k={per_page:-1,orderby:"id",order:"asc",_fields:"id,name"},l=()=>{if(!Object.keys(b.taxonomies).length)return;let l=b.taxonomies,m=wp.template("tsf-primary-term-selector"),n={};const o=(a,b)=>b in l[a].i18n&&l[a].i18n[b]||"",p=a=>document.getElementById(`autodescription[_primary_term_${a}]`),q=a=>+p(a).value,r=(a,b)=>+(p(a).value=+b),s=b=>{let c=document.getElementById("tsf-gutenberg-data-holder");if(!c)return n[b]=!1;let d=m({taxonomy:l[b]});return n[b]=!!a(d).appendTo(c)},t=a=>n[a],u=(a,b)=>{0>b.indexOf(q(a))&&(0 in b?r(a,b[0]):r(a,0))};var v={};class w{constructor(a){this.slug=a,this.reset()}registeredData(){return!!Object.keys(this.data).length}reset(){this.data={}}read(){return this.data}get(a){return a in this.data&&this.data[a]||null}set(a,b){return this.data[a]=b}}const x=a=>v[a]=new w(a),y=a=>v[a]||x(a);class z extends React.Component{componentDidMount(){this.dsAccess().registeredData()||this.registerData(),this.dsAccess().registeredData()&&(this.hasNewTerms()?this.fetchTerms():this.setState({loading:!1}))}componentWillUnmount(){i(this.fetchRequest,["abort"])}componentDidUpdate(a){a.terms===this.props.terms||1<this.props.terms.length&&(!this.dsAccess().get("availableTerms")||this.hasNewTerms())&&this.fetchTerms()}registerData(){this.dsAccess().set("availableTerms",[]),this.fetchTerms()}dsAccess(){return y(this.props.slug)}hasNewTerms(){let a=(this.dsAccess().get("availableTerms")||[]).map(a=>a.id);return!this.props.terms.every(b=>a.includes(b))}isTermAvailable(a){return this.dsAccess().get("availableTerms").some(b=>b.id===a)}fetchTerms(){const{taxonomy:a}=this.props;a&&(this.setState({loading:!0}),this.fetchRequest=h({path:g(`/wp/v2/${a.rest_base}`,k)}),this.fetchRequest.then(a=>{this.fetchRequest=null,this.setState({loading:!1}),this.dsAccess().set("availableTerms",a),this.forceUpdate()},a=>{"abort"===a.statusText||(this.fetchRequest=null,this.setState({loading:!1}))}))}}class A extends z{constructor(){super(...arguments),this.onChange=this.onChange.bind(this),this.state={loading:!0}}getTermName(a){let b=this.dsAccess().get("availableTerms");if(!Array.isArray(b))return"";let c=b.find(b=>b.id===a);return c&&c.name||""}getSelectOptions(){return Array.isArray(this.props.terms)?this.props.terms.sort().map(a=>({value:a,label:j(this.getTermName(a))})):""}onChange(a){this.setState({options:this.getSelectOptions(),value:r(this.props.slug,a)}),"tsfAys"in window&&tsfAys.registerChange()}isDisabled(){if(this.state.loading||!Array.isArray(this.props.terms))return!0;let a=this.dsAccess().get("availableTerms");return!Array.isArray(a)||!a.length}render(){return d(f,{label:o(this.props.slug,"selectPrimary"),value:q(this.props.slug),className:"tsf-pt-gb-selector",onChange:this.onChange,options:this.getSelectOptions(),disabled:this.isDisabled()})}}const B=a=>class extends React.Component{initSelectors(){const{slug:a,terms:b}=this.props;return t(a)&&(u(a,b),1<b.length)?d(e,{},d(A,this.props)):null}render(){return this.props.slug in l?d(e,{},d(a,this.props),this.initSelectors()):d(a,this.props)}};(()=>{for(let a in l)s(a);c("editor.PostTaxonomyType","tsf/pt",B,20)})()};return Object.assign({load:()=>{document.body.addEventListener("tsf-onload",l)}},{},{l10n:b})}(jQuery),window.tsfPTGB.load();
1
+ 'use strict';window.tsfPTGB=function(a){const b="undefined"!=typeof tsfPTL10n&&tsfPTL10n,c=null===b||void 0===b?void 0:b.taxonomies,{createElement:d,Fragment:e,Component:f,useState:g,useEffect:h}=wp.element,{SelectControl:i}=wp.components,{useSelect:j}=wp.data,{unescape:k}=lodash,l=[],m={per_page:-1,orderby:"id",order:"asc",_fields:"id,name"},n=(a,b)=>c[a].i18n[b]||"";let o={};const p=b=>{const d=()=>document.getElementById(`autodescription[_primary_term_${b}]`),e=()=>+d().value,f=a=>+(d().value=+a);return{get:e,set:f,revalidate:a=>{let b=e();return a.includes(b)?b:f((null===a||void 0===a?void 0:a[0])||0)},register:()=>{let d=document.getElementById("tsf-gutenberg-data-holder");if(!d)return o[b]=!1;let e=wp.template("tsf-primary-term-selector")({taxonomy:c[b]});return o[b]=!!a(e).appendTo(d)},isRegistered:()=>o[b]||!1}},q=()=>{function a({taxonomySlug:a,_legacySelectedTerms:b}){const c=p(a),[e,f]=g(c.get()),[o,q]=g(!1),[r,s]=g(!1),{selectedTerms:t,loading:u,availableTerms:v,_taxonomy:w}=j(b=>{const{getTaxonomy:c,getEntityRecords:d,isResolving:e}=b("core"),{getEditedPostAttribute:f}=b("core/editor"),g=c(a),h=["taxonomy",a,m];return{selectedTerms:f(null===g||void 0===g?void 0:g.rest_base)||l,loading:r||e("getEntityRecords",h),availableTerms:o||d(...h)||l,_taxonomy:g}},[a,r,o]);if(h(()=>{t.includes(+e)&&c.get()===+e||(c.revalidate(t),f(c.get()))},[t]),h(()=>{if((null===w||void 0===w?void 0:w.rest_base)&&(null===b||void 0===b?void 0:b.length)&&!r&&(null===o||void 0===o||!o.length||b.filter(a=>!v.map(a=>a.id).includes(a)).length)){var a;s(!0),wp.apiFetch({path:null===(a=wp.url)||void 0===a?void 0:a.addQueryArgs(`/wp/v2/${w.rest_base}`,m)}).then(a=>{q(a)}).finally(()=>{s(!1)})}return()=>{s(!1)}},[b]),2>(null===t||void 0===t?void 0:t.length))return c.set(0),null;const x=a=>{t.includes(+a)&&(c.set(a),f(c.get()),"tsfAys"in window&&tsfAys.registerChange())};return d(i,{label:n(a,"selectPrimary"),value:e,className:"tsf-pt-gb-selector",onChange:x,options:(()=>v.map(a=>t.includes(null===a||void 0===a?void 0:a.id)&&{value:a.id,label:k(null===a||void 0===a?void 0:a.name)}).filter(Boolean)||"")(),disabled:(()=>!(t.length&&v.length&&!u))()})}if(Object.keys(c).length){for(let a in c)p(a).register();wp.hooks.addFilter("editor.PostTaxonomyType","tsf/pt",b=>class extends f{render(){var c,f,g;return p(null===(c=this.props)||void 0===c?void 0:c.slug).isRegistered()?d(e,null,d(b,{...this.props}),d(a,{taxonomySlug:null===(f=this.props)||void 0===f?void 0:f.slug,_legacySelectedTerms:null===(g=this.props)||void 0===g?void 0:g.terms})):d(b,{...this.props})}})}};return Object.assign({load:()=>{document.body.addEventListener("tsf-onload",q)}},{},{l10n:b})}(jQuery),window.tsfPTGB.load();
readme.txt CHANGED
@@ -5,7 +5,7 @@ Tags: seo, xml sitemap, google search, open graph, schema.org, twitter card, per
5
  Requires at least: 5.1.0
6
  Tested up to: 5.8
7
  Requires PHP: 5.6.0
8
- Stable tag: 4.1.4
9
  License: GPLv3
10
  License URI: http://www.gnu.org/licenses/gpl-3.0.html
11
 
@@ -247,6 +247,10 @@ If you wish to display breadcrumbs, then your theme should provide this. Alterna
247
 
248
  == Changelog ==
249
 
 
 
 
 
250
  = 4.1.4 =
251
 
252
  This minor update packs a major punch. TSF now supports [headless mode](https://kb.theseoframework.com/?p=136), cementing itself as a turnkey solution. We defenestrated the pernicious object caching mechanism, and we updated some options' defaults effective only on new sites. We improved performance iterably, fixed about 12 bugs, and enjoyed the weather. Lastly, we introduced a new API for user meta handling, among other things --- developers that wrote software interfacing with TSF are employed well reading the [detailed changelog](https://theseoframework.com/?p=3727#detailed).
5
  Requires at least: 5.1.0
6
  Tested up to: 5.8
7
  Requires PHP: 5.6.0
8
+ Stable tag: 4.1.5
9
  License: GPLv3
10
  License URI: http://www.gnu.org/licenses/gpl-3.0.html
11
 
247
 
248
  == Changelog ==
249
 
250
+ = 4.1.5 =
251
+
252
+ This minor update adds support for Gutenberg 11.3.0 and [fixes a few bugs](https://theseoframework.com/?p=3756#detailed).
253
+
254
  = 4.1.4 =
255
 
256
  This minor update packs a major punch. TSF now supports [headless mode](https://kb.theseoframework.com/?p=136), cementing itself as a turnkey solution. We defenestrated the pernicious object caching mechanism, and we updated some options' defaults effective only on new sites. We improved performance iterably, fixed about 12 bugs, and enjoyed the weather. Lastly, we introduced a new API for user meta handling, among other things --- developers that wrote software interfacing with TSF are employed well reading the [detailed changelog](https://theseoframework.com/?p=3727#detailed).