diff --git a/.distignore b/.distignore
index d307c4b..0a301e0 100644
--- a/.distignore
+++ b/.distignore
@@ -35,3 +35,7 @@ contributing.md
docs
release.sh
release
+src/**/*.js
+src/**/*.jsx
+src/**/*.scss
+webpack.config.js
diff --git a/.gitignore b/.gitignore
index bda5e68..ea83f92 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ node_modules/
release/
vendor
.phpunit.result.cache
+dist/
diff --git a/assets/widget.js b/assets/widget.js
index 84c4b97..a7c120f 100644
--- a/assets/widget.js
+++ b/assets/widget.js
@@ -96,6 +96,14 @@ function show_modal( $modal, $close ) {
initTabSwitching();
+ // Bind copy button via data attribute (replaces inline onclick).
+ var $copyBtn = $modal.find( '[data-copy-active]' );
+ $copyBtn.off( 'click.republish' ).on( 'click.republish', function() {
+ if ( window.ClipboardUtils ) {
+ ClipboardUtils.copyFromElement( getActiveTextarea(), this );
+ }
+ } );
+
trapFocus( $modal );
$close.focus();
}
diff --git a/includes/class-republish-button-block.php b/includes/class-republish-button-block.php
new file mode 100644
index 0000000..555fc7f
--- /dev/null
+++ b/includes/class-republish-button-block.php
@@ -0,0 +1,193 @@
+ [ __CLASS__, 'render_block' ],
+ ];
+
+ if ( function_exists( 'wp_is_block_theme' ) && ! wp_is_block_theme() ) {
+ $args['supports'] = [
+ 'inserter' => false,
+ ];
+ }
+
+ register_block_type_from_metadata(
+ REPUBLICATION_TRACKER_TOOL_PATH . 'src/blocks/republish-button',
+ $args
+ );
+ }
+
+ /**
+ * Render the block on the frontend.
+ *
+ * @param array $attrs Block attributes.
+ * @return string Rendered block HTML.
+ */
+ public static function render_block( $attrs ) {
+ global $post;
+
+ // Guard: only render on singular views.
+ if ( ! is_singular() || ! $post instanceof \WP_Post ) {
+ return '';
+ }
+
+ // Guard: check allowed post types.
+ $allowed_post_types = (array) apply_filters( 'republication_tracker_tool_post_types', [ 'post' ] );
+ if ( ! in_array( get_post_type( $post ), $allowed_post_types, true ) ) {
+ return '';
+ }
+
+ // Guard: check if widget is hidden for this post (same filter as the widget).
+ $hide = apply_filters(
+ 'hide_republication_widget',
+ get_post_meta( $post->ID, 'republication-tracker-tool-hide-widget', true ),
+ $post
+ );
+ if ( true == $hide ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
+ return '';
+ }
+
+ // Translated defaults (block.json defaults are not translatable).
+ $default_attrs = [
+ 'buttonText' => __( 'Republish This Story', 'republication-tracker-tool' ),
+ 'message' => __( 'Republish our articles for free, online or in print, under a Creative Commons license.', 'republication-tracker-tool' ),
+ 'displayMode' => 'modal',
+ ];
+ $attrs = wp_parse_args( $attrs, $default_attrs );
+
+ // Validate displayMode against allowed values.
+ if ( ! in_array( $attrs['displayMode'], [ 'modal', 'page' ], true ) ) {
+ $attrs['displayMode'] = 'modal';
+ }
+
+ // Fall back to translated default when attribute is empty string.
+ $button_text = '' === trim( (string) $attrs['buttonText'] ) ? $default_attrs['buttonText'] : $attrs['buttonText'];
+ $message_text = '' === trim( (string) $attrs['message'] ) ? $default_attrs['message'] : $attrs['message'];
+ $display_mode = $attrs['displayMode'];
+
+ // License badge.
+ $license_key = get_option( 'republication_tracker_tool_license', REPUBLICATION_TRACKER_TOOL_DEFAULT_LICENSE );
+ $using_license = isset( REPUBLICATION_TRACKER_TOOL_LICENSES[ $license_key ] );
+
+ // Block wrapper attributes go on the outer div (anchor, alignment, layout, block supports).
+ $wrapper_attributes = get_block_wrapper_attributes();
+
+ // Start building output.
+ $html = '
';
+
+ // Message.
+ $html .= '
' . wp_kses_post( $message_text ) . '
';
+
+ // Button or link (inherits colors from wrapper via CSS).
+ $button_class = 'wp-block-republication-tracker-tool-republish-button__button';
+ if ( 'page' === $display_mode ) {
+ $endpoint = apply_filters( 'republication_tracker_tool_endpoint', 'republish' );
+ $republish_url = home_url( '/' . $endpoint . wp_make_link_relative( get_permalink( $post->ID ) ) );
+ $html .= '
' . esc_html( $button_text ) . ' ';
+ } else {
+ $html .= '
' . esc_html( $button_text ) . ' ';
+ }
+
+ // License badge.
+ if ( $using_license ) {
+ $html .= sprintf(
+ '
',
+ esc_url( REPUBLICATION_TRACKER_TOOL_LICENSES[ $license_key ]['url'] ),
+ esc_html__( 'Creative Commons License', 'republication-tracker-tool' ),
+ esc_url( plugin_dir_url( __DIR__ ) . 'assets/img/' . $license_key . '.png' )
+ );
+ }
+
+ $html .= '
';
+
+ // Modal (only for modal mode, only once per page).
+ if ( 'modal' === $display_mode ) {
+ self::enqueue_modal_assets();
+
+ if ( ! Republication_Tracker_Tool::$modal_rendered ) {
+ Republication_Tracker_Tool::$modal_rendered = true;
+
+ $is_amp = false; // Block themes do not support AMP.
+ $modal_content_path = REPUBLICATION_TRACKER_TOOL_PATH . 'includes/shareable-content.php';
+
+ ob_start();
+ ?>
+
+
+
+ [],
+ 'version' => REPUBLICATION_TRACKER_TOOL_VERSION,
+ ];
+
+ wp_enqueue_script(
+ 'republication-tracker-tool-republish-button-view',
+ REPUBLICATION_TRACKER_TOOL_URL . 'dist/republish-button-view.js',
+ array_merge( $asset['dependencies'], [ 'republication-tracker-tool-clipboard-utils' ] ),
+ $asset['version'],
+ true
+ );
+ }
+}
+
+Republication_Tracker_Tool_Republish_Button_Block::init();
diff --git a/includes/class-widget.php b/includes/class-widget.php
index 90056cb..1517bd9 100644
--- a/includes/class-widget.php
+++ b/includes/class-widget.php
@@ -13,13 +13,6 @@
*/
class Republication_Tracker_Tool_Widget extends WP_Widget {
- /**
- * Whether the widget has been instantiated.
- *
- * @var bool
- */
- public $has_instance = false;
-
/**
* Sets up the widgets name etc.
*/
@@ -108,25 +101,27 @@ public function widget( $args, $instance ) {
echo wp_kses_post( $args['after_widget'] );
- // if has_instance is false, we can continue with displaying the modal.
- if ( isset( $this->has_instance ) && false === $this->has_instance ) {
+ // If the modal has not been rendered yet, include it.
+ if ( ! Republication_Tracker_Tool::$modal_rendered ) {
- // update has_instance so the next time the widget is created on the same page, it does not create a second modal.
- $this->has_instance = true;
+ // Mark the modal as rendered.
+ Republication_Tracker_Tool::$modal_rendered = true;
// define our path to grab file content from.
$modal_content_path = plugin_dir_path( __FILE__ ) . 'shareable-content.php';
+ $is_amp = self::is_amp();
+
if ( $is_amp ) {
?>
-
+
-
+
-
+
=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.58.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": {
+ "version": "8.58.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz",
+ "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.58.0",
+ "@typescript-eslint/typescript-estree": "8.58.0",
+ "@typescript-eslint/utils": "8.58.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/@typescript-eslint/parser": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz",
- "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
+ "version": "8.58.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz",
+ "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.46.2",
- "@typescript-eslint/types": "8.46.2",
- "@typescript-eslint/typescript-estree": "8.46.2",
- "@typescript-eslint/visitor-keys": "8.46.2",
- "debug": "^4.3.4"
+ "@typescript-eslint/scope-manager": "8.58.0",
+ "@typescript-eslint/types": "8.58.0",
+ "@typescript-eslint/typescript-estree": "8.58.0",
+ "@typescript-eslint/visitor-keys": "8.58.0",
+ "debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -7339,20 +7404,20 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz",
- "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==",
+ "version": "8.58.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz",
+ "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.46.2",
- "@typescript-eslint/types": "^8.46.2",
- "debug": "^4.3.4"
+ "@typescript-eslint/tsconfig-utils": "^8.58.0",
+ "@typescript-eslint/types": "^8.58.0",
+ "debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -7362,18 +7427,18 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <6.0.0"
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz",
- "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==",
+ "version": "8.58.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz",
+ "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.46.2",
- "@typescript-eslint/visitor-keys": "8.46.2"
+ "@typescript-eslint/types": "8.58.0",
+ "@typescript-eslint/visitor-keys": "8.58.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -7384,9 +7449,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz",
- "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==",
+ "version": "8.58.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz",
+ "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -7397,7 +7462,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <6.0.0"
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
@@ -7606,9 +7671,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz",
- "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==",
+ "version": "8.58.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz",
+ "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==",
"dev": true,
"license": "MIT",
"engines": {
@@ -7620,22 +7685,21 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz",
- "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==",
+ "version": "8.58.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz",
+ "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.46.2",
- "@typescript-eslint/tsconfig-utils": "8.46.2",
- "@typescript-eslint/types": "8.46.2",
- "@typescript-eslint/visitor-keys": "8.46.2",
- "debug": "^4.3.4",
- "fast-glob": "^3.3.2",
- "is-glob": "^4.0.3",
- "minimatch": "^9.0.4",
- "semver": "^7.6.0",
- "ts-api-utils": "^2.1.0"
+ "@typescript-eslint/project-service": "8.58.0",
+ "@typescript-eslint/tsconfig-utils": "8.58.0",
+ "@typescript-eslint/types": "8.58.0",
+ "@typescript-eslint/visitor-keys": "8.58.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -7645,13 +7709,52 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <6.0.0"
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -7662,16 +7765,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz",
- "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==",
+ "version": "8.58.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz",
+ "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/eslint-utils": "^4.7.0",
- "@typescript-eslint/scope-manager": "8.46.2",
- "@typescript-eslint/types": "8.46.2",
- "@typescript-eslint/typescript-estree": "8.46.2"
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.58.0",
+ "@typescript-eslint/types": "8.58.0",
+ "@typescript-eslint/typescript-estree": "8.58.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -7681,19 +7784,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.46.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz",
- "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==",
+ "version": "8.58.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz",
+ "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.46.2",
- "eslint-visitor-keys": "^4.2.1"
+ "@typescript-eslint/types": "8.58.0",
+ "eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -15190,13 +15293,13 @@
}
},
"node_modules/eslint-visitor-keys": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
- "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
@@ -32665,9 +32768,9 @@
}
},
"node_modules/ts-api-utils": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
- "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/package.json b/package.json
index d021fac..142bde4 100644
--- a/package.json
+++ b/package.json
@@ -4,14 +4,17 @@
"main": "Gruntfile.js",
"author": "Automattic",
"scripts": {
- "start": "npm ci",
+ "start": "npm ci && npm run watch",
"cm": "newspack-scripts commit",
- "build": "echo 'No build in this repository.'",
+ "build": "npm run clean && newspack-scripts wp-scripts build",
+ "watch": "npm run clean && newspack-scripts wp-scripts start",
+ "clean": "rm -rf dist",
"i18n": "grunt i18n",
- "lint:js": "echo 'No JS to lint in this repository.'",
+ "lint:js": "newspack-scripts wp-scripts lint-js 'src/**/*.{js,jsx}'",
+ "lint:scss": "newspack-scripts wp-scripts lint-style 'src/**/*.scss'",
"readme": "grunt readme",
"semantic-release": "newspack-scripts release --files=republication-tracker-tool.php",
- "release:archive": "rm -rf release && mkdir -p release && rsync -r . ./release/republication-tracker-tool --exclude-from='./.distignore' && cd release && zip -r republication-tracker-tool.zip republication-tracker-tool",
+ "release:archive": "npm run build && rm -rf release && mkdir -p release && rsync -r . ./release/republication-tracker-tool --exclude-from='./.distignore' && cd release && zip -r republication-tracker-tool.zip republication-tracker-tool",
"release": "npm run semantic-release"
},
"repository": {
@@ -23,6 +26,7 @@
"url": "https://github.com/Automattic/republication-tracker-tool/issues"
},
"devDependencies": {
+ "@typescript-eslint/eslint-plugin": "^8.58.0",
"grunt": "~1.6.1",
"grunt-wp-i18n": "~1.0.3",
"grunt-wp-readme-to-markdown": "~2.1.0",
diff --git a/readme.txt b/readme.txt
index b4e4152..1d847ed 100644
--- a/readme.txt
+++ b/readme.txt
@@ -2,9 +2,9 @@
Contributors: innlabs
Donate link: https://inn.org/donate
Tags: publishers, news
-Requires at least: 5.3
+Requires at least: 6.0
Requires PHP: 7.4
-Tested up to: 6.4.3
+Tested up to: 7.0
Stable tag: trunk
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
diff --git a/republication-tracker-tool.php b/republication-tracker-tool.php
index 2266dde..824a70c 100644
--- a/republication-tracker-tool.php
+++ b/republication-tracker-tool.php
@@ -6,6 +6,7 @@
* Author URI: https://labs.inn.org
* Text Domain: republication-tracker-tool
* Domain Path: /languages
+ * Requires at least: 6.0
* Version: 2.8.1
*
* @package Republication_Tracker_Tool
@@ -32,6 +33,7 @@ function_exists( 'get_plugin_data' ) || require_once ABSPATH . 'wp-admin/include
require plugin_dir_path( __FILE__ ) . 'includes/class-widget.php';
require plugin_dir_path( __FILE__ ) . 'includes/compatibility-co-authors-plus.php';
require plugin_dir_path( __FILE__ ) . 'includes/class-republication-rewrite.php';
+require plugin_dir_path( __FILE__ ) . 'includes/class-republish-button-block.php';
/**
* Main initiation class.
@@ -72,6 +74,13 @@ final class Republication_Tracker_Tool {
*/
protected static $single_instance = null;
+ /**
+ * Whether the modal has been rendered on this page.
+ *
+ * @var bool
+ */
+ public static $modal_rendered = false;
+
/**
* Instance of Republication_Tracker_Tool_Settings
*
diff --git a/src/blocks/republish-button/block.json b/src/blocks/republish-button/block.json
new file mode 100644
index 0000000..b644c49
--- /dev/null
+++ b/src/blocks/republish-button/block.json
@@ -0,0 +1,61 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "republication-tracker-tool/republish-button",
+ "title": "Republish Button",
+ "category": "widgets",
+ "description": "A button that allows readers to republish your content under a Creative Commons license.",
+ "keywords": [ "republish", "creative commons", "syndication" ],
+ "textdomain": "republication-tracker-tool",
+ "attributes": {
+ "buttonText": {
+ "type": "string",
+ "default": "Republish This Story"
+ },
+ "message": {
+ "type": "string",
+ "default": "Republish our articles for free, online or in print, under a Creative Commons license."
+ },
+ "displayMode": {
+ "type": "string",
+ "default": "modal",
+ "enum": [ "modal", "page" ]
+ }
+ },
+ "supports": {
+ "anchor": true,
+ "color": {
+ "background": true,
+ "text": true
+ },
+ "typography": {
+ "fontSize": true,
+ "lineHeight": true,
+ "__experimentalFontFamily": true,
+ "__experimentalFontWeight": true,
+ "__experimentalFontStyle": true,
+ "__experimentalTextTransform": true,
+ "__experimentalTextDecoration": true,
+ "__experimentalLetterSpacing": true
+ },
+ "spacing": {
+ "padding": true
+ },
+ "__experimentalBorder": {
+ "radius": true,
+ "color": true,
+ "style": true,
+ "width": true
+ },
+ "shadow": true
+ },
+ "editorScript": "file:../../../dist/republish-button.js",
+ "style": "file:../../../dist/republish-button.css",
+ "example": {
+ "attributes": {
+ "buttonText": "Republish This Story",
+ "message": "Republish our articles for free, online or in print, under a Creative Commons license.",
+ "displayMode": "modal"
+ }
+ }
+}
diff --git a/src/blocks/republish-button/edit.js b/src/blocks/republish-button/edit.js
new file mode 100644
index 0000000..5ccb4bd
--- /dev/null
+++ b/src/blocks/republish-button/edit.js
@@ -0,0 +1,92 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ InspectorControls,
+ RichText,
+ useBlockProps,
+} from '@wordpress/block-editor';
+import { PanelBody, SelectControl } from '@wordpress/components';
+
+function RepublishButtonEdit( { attributes, setAttributes } ) {
+ const { buttonText, message, displayMode } = attributes;
+
+ // Block supports (color, typography, spacing, border, shadow) are applied
+ // to the wrapper div automatically by useBlockProps(). The inner button
+ // inherits colors via CSS.
+ const blockProps = useBlockProps();
+
+ return (
+ <>
+
+
+
+ setAttributes( { displayMode: val } )
+ }
+ />
+
+
+
+ setAttributes( { message: val } ) }
+ placeholder={ __(
+ 'Add a description of the republish featureā¦',
+ 'republication-tracker-tool'
+ ) }
+ withoutInteractiveFormatting
+ />
+
+
+ setAttributes( { buttonText: val } )
+ }
+ placeholder={ __(
+ 'Republish This Story',
+ 'republication-tracker-tool'
+ ) }
+ allowedFormats={ [] }
+ aria-label={ __(
+ 'Button text',
+ 'republication-tracker-tool'
+ ) }
+ />
+
+
+ >
+ );
+}
+
+export default RepublishButtonEdit;
diff --git a/src/blocks/republish-button/index.js b/src/blocks/republish-button/index.js
new file mode 100644
index 0000000..213009b
--- /dev/null
+++ b/src/blocks/republish-button/index.js
@@ -0,0 +1,16 @@
+/**
+ * WordPress dependencies
+ */
+import { registerBlockType } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import metadata from './block.json';
+import Edit from './edit';
+import './style.scss';
+
+registerBlockType( metadata, {
+ edit: Edit,
+ save: () => null,
+} );
diff --git a/src/blocks/republish-button/style.scss b/src/blocks/republish-button/style.scss
new file mode 100644
index 0000000..7eb96c3
--- /dev/null
+++ b/src/blocks/republish-button/style.scss
@@ -0,0 +1,18 @@
+.wp-block-republication-tracker-tool-republish-button {
+
+ &__button {
+ display: inline-block;
+ cursor: pointer;
+ text-decoration: none;
+ border: none;
+ padding: 0;
+ background: none;
+ color: inherit;
+ font: inherit;
+ line-height: inherit;
+ }
+
+ &__message {
+ margin-bottom: 0.5em;
+ }
+}
diff --git a/src/blocks/republish-button/view.js b/src/blocks/republish-button/view.js
new file mode 100644
index 0000000..8509959
--- /dev/null
+++ b/src/blocks/republish-button/view.js
@@ -0,0 +1,270 @@
+/* global ClipboardUtils, DOMParser */
+
+/**
+ * WordPress dependencies
+ */
+import domReady from '@wordpress/dom-ready';
+
+let initialized = false;
+let currentTrigger = null;
+
+/**
+ * Get the textarea for the currently active tab.
+ *
+ * @return {HTMLElement|null} The active textarea element.
+ */
+function getActiveTextarea() {
+ const activeTextarea = document.querySelector(
+ '.republish-content.republish-content--active textarea'
+ );
+ if ( activeTextarea ) {
+ return activeTextarea;
+ }
+ // Fallback to the original textarea if no tabs are present.
+ return document.querySelector(
+ '#republication-tracker-tool-shareable-content'
+ );
+}
+
+/**
+ * Initialize tab switching between HTML and Plain Text formats.
+ *
+ * @param {HTMLElement} modal The modal element.
+ */
+function initTabSwitching( modal ) {
+ const tabButtons = modal.querySelectorAll(
+ '.republish-format-tabs__button'
+ );
+ const tabContents = modal.querySelectorAll( '.republish-content' );
+ const mainCopyButton = modal.querySelector(
+ '.republication-tracker-tool__copy-button--main'
+ );
+
+ tabButtons.forEach( ( button ) => {
+ button.addEventListener( 'click', ( e ) => {
+ e.preventDefault();
+ const targetTab = button.getAttribute( 'data-tab' );
+
+ // Update active states.
+ tabButtons.forEach( ( btn ) =>
+ btn.classList.remove( 'republish-format-tabs__button--active' )
+ );
+ tabContents.forEach( ( content ) =>
+ content.classList.remove( 'republish-content--active' )
+ );
+
+ button.classList.add( 'republish-format-tabs__button--active' );
+ const targetContent = modal.querySelector(
+ `[data-tab-content="${ targetTab }"]`
+ );
+ if ( targetContent ) {
+ targetContent.classList.add( 'republish-content--active' );
+ }
+
+ // Show/hide main copy button based on active tab.
+ if ( mainCopyButton ) {
+ if ( targetTab === 'html' ) {
+ mainCopyButton.classList.add( 'show-for-html' );
+ } else {
+ mainCopyButton.classList.remove( 'show-for-html' );
+ }
+ }
+ } );
+ } );
+
+ // Initialize copy buttons for individual plain text fields.
+ modal.querySelectorAll( '.plain-text-field__button' ).forEach( ( btn ) => {
+ btn.addEventListener( 'click', ( e ) => {
+ e.preventDefault();
+ const target = btn.getAttribute( 'data-target' );
+ if ( window.ClipboardUtils && target ) {
+ ClipboardUtils.copyFromElement( target, btn );
+ }
+ } );
+ } );
+
+ // Bind main copy button via data attribute.
+ const copyActiveBtn = modal.querySelector( '[data-copy-active]' );
+ if ( copyActiveBtn ) {
+ copyActiveBtn.addEventListener( 'click', ( e ) => {
+ e.preventDefault();
+ if ( window.ClipboardUtils ) {
+ ClipboardUtils.copyFromElement(
+ getActiveTextarea(),
+ copyActiveBtn
+ );
+ }
+ } );
+ }
+}
+
+/**
+ * Strip captions from shareable content.
+ *
+ * @param {HTMLElement} modal The modal element.
+ */
+function stripCaptions( modal ) {
+ const shareable = modal.querySelector(
+ '#republication-tracker-tool-shareable-content'
+ );
+ if ( ! shareable ) {
+ return;
+ }
+ const html = shareable.textContent;
+ const parser = new DOMParser();
+ const doc = parser.parseFromString( html, 'text/html' );
+ doc.querySelectorAll( '.wp-caption' ).forEach( ( el ) => el.remove() );
+ shareable.innerHTML = doc.body.innerHTML;
+}
+
+/**
+ * Trap focus within the modal for accessibility.
+ *
+ * @param {HTMLElement} modal The modal element.
+ */
+function trapFocus( modal ) {
+ const focusableSelector =
+ 'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), select:not([disabled])';
+ const focusableEls = modal.querySelectorAll( focusableSelector );
+ if ( ! focusableEls.length ) {
+ return;
+ }
+ const firstFocusable = focusableEls[ 0 ];
+ const lastFocusable = focusableEls[ focusableEls.length - 1 ];
+
+ modal.addEventListener( 'keydown', ( e ) => {
+ if ( e.key !== 'Tab' ) {
+ return;
+ }
+ const active = modal.ownerDocument.activeElement;
+ if ( e.shiftKey ) {
+ if ( active === firstFocusable ) {
+ lastFocusable.focus();
+ e.preventDefault();
+ }
+ } else if ( active === lastFocusable ) {
+ firstFocusable.focus();
+ e.preventDefault();
+ }
+ } );
+}
+
+/**
+ * Close the modal and return focus to the trigger button.
+ *
+ * @param {HTMLElement} modal The modal element.
+ */
+function closeModal( modal ) {
+ document.body.classList.remove( 'modal-open-disallow-scrolling' );
+ modal.style.display = 'none';
+ if ( currentTrigger ) {
+ currentTrigger.focus();
+ }
+ currentTrigger = null;
+}
+
+/**
+ * Initialize modal event listeners once.
+ *
+ * @param {HTMLElement} modal The modal element.
+ */
+function initModal( modal ) {
+ if ( initialized ) {
+ return;
+ }
+ initialized = true;
+
+ const modalContent = modal.querySelector(
+ '#republication-tracker-tool-modal-content'
+ );
+ const closeBtn = modal.querySelector( '.republication-tracker-tool-close' );
+
+ // Move modal to body once.
+ if ( modal.parentNode !== document.body ) {
+ document.body.appendChild( modal );
+ }
+
+ // Strip captions once (not per-open).
+ stripCaptions( modal );
+
+ // Tab switching (bind once).
+ initTabSwitching( modal );
+
+ // Focus trap (bind once).
+ trapFocus( modal );
+
+ // Prevent clicks inside modal content from closing modal.
+ if ( modalContent ) {
+ modalContent.addEventListener( 'click', ( e ) => e.stopPropagation() );
+ }
+
+ // Click outside modal content closes modal.
+ modal.addEventListener( 'click', () => closeModal( modal ) );
+
+ // Close button.
+ if ( closeBtn ) {
+ closeBtn.addEventListener( 'click', ( e ) => {
+ e.stopPropagation();
+ closeModal( modal );
+ } );
+ }
+
+ // Escape key.
+ document.addEventListener( 'keydown', ( e ) => {
+ if ( e.key === 'Escape' && modal.style.display !== 'none' ) {
+ closeModal( modal );
+ }
+ } );
+}
+
+/**
+ * Show the modal.
+ *
+ * @param {HTMLElement} modal The modal element.
+ * @param {HTMLElement} triggerButton The button that triggered the modal.
+ */
+function showModal( modal, triggerButton ) {
+ initModal( modal );
+ currentTrigger = triggerButton;
+
+ const modalContent = modal.querySelector(
+ '#republication-tracker-tool-modal-content'
+ );
+
+ modal.style.display = '';
+ if ( modalContent ) {
+ modalContent.style.display = '';
+ }
+ document.body.classList.add( 'modal-open-disallow-scrolling' );
+
+ // Focus close button.
+ const closeBtn = modal.querySelector( '.republication-tracker-tool-close' );
+ if ( closeBtn ) {
+ closeBtn.focus();
+ }
+}
+
+domReady( () => {
+ const modal = document.getElementById( 'republication-tracker-tool-modal' );
+ if ( ! modal ) {
+ return;
+ }
+
+ // Find all block trigger buttons.
+ document
+ .querySelectorAll( '[data-modal-trigger="republish"]' )
+ .forEach( ( button ) => {
+ button.addEventListener( 'click', ( e ) => {
+ e.preventDefault();
+ showModal( modal, button );
+ } );
+ } );
+
+ // Auto-open via URL hash.
+ if ( window.location.hash === '#show-republish' ) {
+ const firstTrigger = document.querySelector(
+ '[data-modal-trigger="republish"]'
+ );
+ showModal( modal, firstTrigger );
+ }
+} );
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 123187e..deca9e2 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -5,6 +5,9 @@
* @package Creative_Commons_Sharing
*/
+// Load the composer autoloader.
+require_once __DIR__ . '/../vendor/autoload.php';
+
$_tests_dir = getenv( 'WP_TESTS_DIR' );
if ( ! $_tests_dir ) {
$_tests_dir = '/tmp/wordpress-tests-lib';
diff --git a/tests/test-republish-button-block.php b/tests/test-republish-button-block.php
new file mode 100644
index 0000000..d1f7e3b
--- /dev/null
+++ b/tests/test-republish-button-block.php
@@ -0,0 +1,209 @@
+is_registered( 'republication-tracker-tool/republish-button' ) ) {
+ register_block_type_from_metadata(
+ REPUBLICATION_TRACKER_TOOL_PATH . 'src/blocks/republish-button',
+ [
+ 'render_callback' => [ 'Republication_Tracker_Tool_Republish_Button_Block', 'render_block' ],
+ ]
+ );
+ }
+
+ $this->test_post = $this->factory->post->create_and_get(
+ [
+ 'post_title' => 'Test Post for Block',
+ 'post_content' => 'Test content for block display.
',
+ 'post_status' => 'publish',
+ ]
+ );
+ }
+
+ /**
+ * Clean up after tests.
+ */
+ public function tear_down() {
+ wp_delete_post( $this->test_post->ID, true );
+ Republication_Tracker_Tool::$modal_rendered = false;
+ parent::tear_down();
+ }
+
+ /**
+ * Helper to set up a singular post context.
+ */
+ private function set_singular_context() {
+ global $post, $wp_query;
+ $post = $this->test_post;
+ $wp_query->is_singular = true;
+ $wp_query->is_single = true;
+ $wp_query->queried_object = $this->test_post;
+ $wp_query->queried_object_id = $this->test_post->ID;
+ }
+
+ /**
+ * Helper to render the block through the standard pipeline so
+ * WP_Block_Supports context is set correctly.
+ *
+ * @param array $attrs Block attributes.
+ * @return string Rendered HTML.
+ */
+ private function render_block( $attrs = [] ) {
+ $json = empty( $attrs ) ? '' : ' ' . wp_json_encode( (object) $attrs );
+ $serialized = '';
+ return do_blocks( $serialized );
+ }
+
+ /**
+ * Test block renders on singular post view.
+ */
+ public function test_block_renders_on_singular_post() {
+ $this->set_singular_context();
+
+ $output = $this->render_block();
+
+ $this->assertStringContainsString( 'wp-block-republication-tracker-tool-republish-button', $output );
+ $this->assertStringContainsString( 'Republish This Story', $output );
+ $this->assertStringContainsString( 'data-modal-trigger="republish"', $output );
+ }
+
+ /**
+ * Test block returns empty on non-singular views.
+ */
+ public function test_block_empty_on_non_singular() {
+ global $wp_query;
+ $wp_query->is_singular = false;
+ $wp_query->is_single = false;
+
+ $output = Republication_Tracker_Tool_Republish_Button_Block::render_block( [] );
+
+ $this->assertEmpty( $output );
+ }
+
+ /**
+ * Test block respects post type filter.
+ */
+ public function test_block_respects_post_type_filter() {
+ $this->set_singular_context();
+
+ add_filter(
+ 'republication_tracker_tool_post_types',
+ function () {
+ return [ 'page' ];
+ }
+ );
+
+ $output = Republication_Tracker_Tool_Republish_Button_Block::render_block( [] );
+
+ $this->assertEmpty( $output );
+
+ remove_all_filters( 'republication_tracker_tool_post_types' );
+ }
+
+ /**
+ * Test block respects hide widget meta.
+ */
+ public function test_block_respects_hide_meta() {
+ $this->set_singular_context();
+ update_post_meta( $this->test_post->ID, 'republication-tracker-tool-hide-widget', true );
+
+ $output = Republication_Tracker_Tool_Republish_Button_Block::render_block( [] );
+
+ $this->assertEmpty( $output );
+
+ delete_post_meta( $this->test_post->ID, 'republication-tracker-tool-hide-widget' );
+ }
+
+ /**
+ * Test block respects hide_republication_widget filter.
+ */
+ public function test_block_respects_hide_filter() {
+ $this->set_singular_context();
+ add_filter( 'hide_republication_widget', '__return_true' );
+
+ $output = Republication_Tracker_Tool_Republish_Button_Block::render_block( [] );
+
+ $this->assertEmpty( $output );
+
+ remove_filter( 'hide_republication_widget', '__return_true' );
+ }
+
+ /**
+ * Test modal is only rendered once across multiple blocks.
+ */
+ public function test_single_modal_across_multiple_blocks() {
+ $this->set_singular_context();
+
+ $output1 = $this->render_block();
+ $output2 = $this->render_block();
+
+ $combined = $output1 . $output2;
+
+ $this->assertEquals( 1, substr_count( $combined, 'id="republication-tracker-tool-modal"' ), 'Single modal in combined output.' );
+ $this->assertEquals( 2, substr_count( $combined, 'data-modal-trigger="republish"' ), 'Two trigger buttons in combined output.' );
+ }
+
+ /**
+ * Test page mode renders a link instead of a button.
+ */
+ public function test_page_mode_renders_link() {
+ $this->set_singular_context();
+
+ $output = $this->render_block( [ 'displayMode' => 'page' ] );
+
+ $this->assertStringContainsString( 'assertStringNotContainsString( 'data-modal-trigger', $output );
+ $this->assertStringNotContainsString( 'id="republication-tracker-tool-modal"', $output );
+ }
+
+ /**
+ * Test empty attributes fall back to translated defaults.
+ */
+ public function test_empty_attributes_fallback() {
+ $this->set_singular_context();
+
+ $output = $this->render_block(
+ [
+ 'buttonText' => '',
+ 'message' => '',
+ ]
+ );
+
+ $this->assertStringContainsString( 'Republish This Story', $output );
+ $this->assertStringContainsString( 'Republish our articles for free', $output );
+ }
+
+ /**
+ * Test invalid displayMode falls back to modal.
+ */
+ public function test_invalid_display_mode_fallback() {
+ $this->set_singular_context();
+
+ $output = $this->render_block( [ 'displayMode' => 'invalid' ] );
+
+ $this->assertStringContainsString( 'data-modal-trigger="republish"', $output );
+ }
+}
diff --git a/tests/test-widget.php b/tests/test-widget.php
index 043d61b..b0053b5 100644
--- a/tests/test-widget.php
+++ b/tests/test-widget.php
@@ -46,6 +46,7 @@ public function set_up() {
*/
public function tear_down() {
wp_delete_post( $this->test_post->ID, true );
+ Republication_Tracker_Tool::$modal_rendered = false;
parent::tear_down();
}
@@ -156,11 +157,11 @@ public function test_multiple_widget_instances() {
$this->widget->widget( $args, $instance );
$this->widget->widget( $args, $instance );
$output = ob_get_clean();
- $this->assertStringContainsString( 'republication-tracker-tool-modal', $output );
+ $this->assertStringContainsString( 'id="republication-tracker-tool-modal"', $output );
$this->assertStringContainsString( 'republication-tracker-tool-button', $output );
- // Check only one modal is present.
- $this->assertEquals( substr_count( $output, 'republication-tracker-tool-modal' ), 1, 'Single modal found in output.' );
- $this->assertEquals( substr_count( $output, 'republication-tracker-tool-button' ), 3, 'Multiple buttons found in output.' );
+ // Check only one modal wrapper is present.
+ $this->assertEquals( 1, substr_count( $output, 'id="republication-tracker-tool-modal"' ), 'Single modal found in output.' );
+ $this->assertEquals( 3, substr_count( $output, 'republication-tracker-tool-button' ), 'Multiple buttons found in output.' );
}
}
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..ff5770b
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,12 @@
+/**
+ * External dependencies
+ */
+const path = require( 'path' );
+const getBaseWebpackConfig = require( 'newspack-scripts/config/getWebpackConfig' );
+
+const entry = {
+ 'republish-button': path.join( __dirname, 'src', 'blocks', 'republish-button', 'index.js' ),
+ 'republish-button-view': path.join( __dirname, 'src', 'blocks', 'republish-button', 'view.js' ),
+};
+
+module.exports = getBaseWebpackConfig( { entry } );