Changeset 3409533
- Timestamp:
- 12/03/2025 01:05:32 PM (3 months ago)
- Location:
- abtestkit/trunk
- Files:
-
- 5 edited
-
abtestkit.php (modified) (3 diffs)
-
assets/css/admin.css (modified) (1 diff)
-
assets/js/dashboard.js (modified) (1 diff)
-
assets/js/pt-wizard.js (modified) (18 diffs)
-
readme.txt (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
abtestkit/trunk/abtestkit.php
r3406331 r3409533 3 3 * Plugin Name: abtestkit 4 4 * Plugin URI: https://wordpress.org/plugins/abtestkit 5 * Description: Split testing for WooCommerce & WordPress, compatible with all page builders & caching plugins.6 * Version: 1.0. 45 * Description: Split testing for WooCommerce & WordPress, compatible with all page builders, themes & caching plugins. 6 * Version: 1.0.5 7 7 * Author: abtestkit 8 8 * License: GPL-2.0-or-later … … 2869 2869 } 2870 2870 2871 // Core WP React + components 2871 // Load the WordPress editor stack so we can embed classic editors in the wizard 2872 if ( function_exists( 'wp_enqueue_editor' ) ) { 2873 wp_enqueue_editor(); 2874 } else { 2875 wp_enqueue_script( 'wp-editor' ); 2876 } 2877 2878 // React + components 2872 2879 wp_enqueue_script( 'wp-element' ); 2873 2880 wp_enqueue_script( 'wp-components' ); … … 2882 2889 'abtestkit-pt-wizard', 2883 2890 plugins_url( 'assets/js/pt-wizard.js', __FILE__ ), 2884 [ 'wp-element', 'wp-components', 'wp-api-fetch' ],2891 [ 'wp-element', 'wp-components', 'wp-api-fetch', 'wp-editor' ], 2885 2892 '1.0.1', 2886 2893 true -
abtestkit/trunk/assets/css/admin.css
r3406331 r3409533 94 94 border-radius: 6px !important; 95 95 } 96 97 /* Ensure lists display correctly in Version A previews on the product test wizard */ 98 #abtestkit-pt-wizard-root .abtestkit-html-preview ul, 99 #abtestkit-pt-wizard-root .abtestkit-html-preview ol { 100 list-style: disc; 101 margin-left: 1.5em; 102 padding-left: 1.5em; 103 } 104 105 #abtestkit-pt-wizard-root .abtestkit-html-preview ol { 106 list-style: decimal; 107 } -
abtestkit/trunk/assets/js/dashboard.js
r3406331 r3409533 79 79 }); 80 80 81 const makeTab = (label, value) => 82 h( 83 'button', 84 { 85 type: 'button', 86 className: 87 'nav-tab' + (activeTab === value ? ' nav-tab-active' : ''), 88 onClick: () => setActiveTab(value), 89 style: { cursor: 'pointer' }, 90 }, 91 tabCounts[value] > 0 ? `${label} (${tabCounts[value]})` : label 92 ); 81 const makeTab = (label, value) => 82 h( 83 'button', 84 { 85 type: 'button', 86 className: 87 'nav-tab' + (activeTab === value ? ' nav-tab-active' : ''), 88 onClick: () => setActiveTab(value), 89 style: { 90 cursor: 'pointer', 91 flex: 1, 92 textAlign: 'center', 93 padding: '8px 16px', 94 }, 95 }, 96 tabCounts[value] > 0 ? `${label} (${tabCounts[value]})` : label 97 ); 98 93 99 94 100 return h( -
abtestkit/trunk/assets/js/pt-wizard.js
r3406331 r3409533 1 1 /* assets/js/pt-wizard.js */ 2 2 (function (wp) { 3 const { createElement: h, Fragment, useState, useEffect } = wp.element;3 const { createElement: h, Fragment, useState, useEffect, useRef } = wp.element; 4 4 const { 5 5 Button, … … 60 60 frame.open(); 61 61 }; 62 63 // TinyMCE-based classic editor field used for Version B product descriptions 64 const ClassicEditorField = ({ id, value, onChange, help, readOnly = false }) => { 65 const textareaId = id; 66 67 // Initialise editor once 68 useEffect(() => { 69 if (!window.wp || !wp.editor || !wp.editor.initialize) return; 70 71 const $ = window.jQuery || window.$; 72 if (!$) return; 73 74 // Set initial value in the textarea before turning it into an editor 75 if (typeof value === "string") { 76 $("#" + textareaId).val(value); 77 } 78 79 // Clean up any previous editor instance on this ID 80 if (wp.editor.remove) { 81 try { 82 wp.editor.remove(textareaId); 83 } catch (e) {} 84 } 85 86 const onInit = (event, editor) => { 87 if (!editor || editor.id !== textareaId) return; 88 editor.on("change keyup", () => { 89 const content = editor.getContent(); 90 if (typeof onChange === "function") { 91 onChange(content); 92 } 93 }); 94 }; 95 96 $(document).on( 97 "tinymce-editor-init.abtestkit-" + textareaId, 98 onInit 99 ); 100 101 wp.editor.initialize(textareaId, { 102 tinymce: { 103 wpautop: true, 104 toolbar1: readOnly ? false : "formatselect,bold,italic,bullist,numlist,link,unlink,blockquote,undo,redo", 105 toolbar2: "", 106 readonly: readOnly, 107 }, 108 quicktags: true, 109 }); 110 111 return () => { 112 $(document).off( 113 "tinymce-editor-init.abtestkit-" + textareaId, 114 onInit 115 ); 116 if (wp.editor.remove) { 117 try { 118 wp.editor.remove(textareaId); 119 } catch (e) {} 120 } 121 }; 122 }, [textareaId]); 123 124 // Keep programmatic value changes (like prefill from Version A) in sync 125 useEffect(() => { 126 if (!window.tinymce) return; 127 const ed = window.tinymce.get(textareaId); 128 if (!ed) return; 129 if (typeof value === "string" && value !== ed.getContent()) { 130 ed.setContent(value); 131 } 132 }, [textareaId, value]); 133 134 return h( 135 "div", 136 null, 137 [ 138 h("textarea", { 139 id: textareaId, 140 defaultValue: value || "", 141 style: { width: "100%", minHeight: 200 }, 142 }), 143 help 144 ? h( 145 "p", 146 { style: { marginTop: 4, color: "#6c7781", fontSize: 12 } }, 147 help 148 ) 149 : null, 150 ] 151 ); 152 }; 153 154 // Utility: strip HTML tags for plain-text placeholders, etc. 155 const stripHtml = (html) => 156 typeof html === "string" 157 ? html.replace(/<\/?[^>]+(>|$)/g, "").replace(/\s+/g, " ").trim() 158 : ""; 62 159 63 160 /* ────────────────────────────────────────────────────────────── … … 300 397 const hasPicks = Array.isArray(selected) && selected.length > 0; 301 398 const buttonLabel = picking 302 ? "Now click a target in the preview… (Esc to cancel)"399 ? "Now click your target in the preview above… (Esc to cancel)" 303 400 : hasPicks 304 401 ? "Select another" 305 : " Select target in the Preview";402 : "Begin selecting click targets"; 306 403 307 404 return h(Fragment, null, [ … … 423 520 ); 424 521 425 /* WordPress-style list table for selecting pages */522 /* WordPress-style list table for selecting pages (with simple pagination) */ 426 523 const PageTable = ({ pages, selectedId, onSelect, empty = "No pages found." }) => { 427 return h( 524 const [pageIndex, setPageIndex] = useState(0); 525 const pageSize = 25; 526 527 const list = Array.isArray(pages) ? pages : []; 528 const total = list.length; 529 const totalPages = total ? Math.ceil(total / pageSize) : 1; 530 531 // Clamp page index when results change (e.g. new search) 532 useEffect(() => { 533 if (pageIndex > 0 && pageIndex > totalPages - 1) { 534 setPageIndex(0); 535 } 536 }, [total]); 537 538 const start = pageIndex * pageSize; 539 const end = Math.min(start + pageSize, total); 540 const visible = total ? list.slice(start, end) : []; 541 542 const table = h( 428 543 "table", 429 544 { className: "wp-list-table widefat fixed striped", style: { marginTop: 12 } }, 430 545 [ 431 h("thead", null, 546 h( 547 "thead", 548 null, 432 549 h("tr", null, [ 433 550 h("th", { style: { width: 24 } }, ""), // radio column … … 441 558 "tbody", 442 559 null, 443 pages && pages.length444 ? pages.map((p) => {560 visible && visible.length 561 ? visible.map((p) => { 445 562 const isSel = String(selectedId || "") === String(p.id); 446 563 447 // New: mark pages/products that are already in a running test564 // Mark pages/products that are already in a running test 448 565 const isLocked = !!p.in_running_test; 449 566 const statusLabel = isLocked … … 464 581 }, 465 582 [ 466 h("td", null, 583 h( 584 "td", 585 null, 467 586 h("input", { 468 587 type: "radio", … … 474 593 }) 475 594 ), 476 h("td", null, 477 h("strong", null, p.title || "(no title)") 478 ), 595 h("td", null, h("strong", null, p.title || "(no title)")), 479 596 h("td", null, p.category || "—"), 480 597 h("td", null, statusLabel), … … 488 605 h( 489 606 "td", 490 { colSpan: 4, style: { padding: "12px 10px", color: "#6c7781" } }, 607 { 608 colSpan: 5, 609 style: { padding: "12px 10px", color: "#6c7781" }, 610 }, 491 611 empty 492 612 ) … … 495 615 ] 496 616 ); 617 618 const pager = 619 totalPages > 1 620 ? h( 621 "div", 622 { 623 style: { 624 marginTop: 8, 625 display: "flex", 626 justifyContent: "space-between", 627 alignItems: "center", 628 fontSize: 12, 629 color: "#6c7781", 630 }, 631 }, 632 [ 633 h( 634 "span", 635 null, 636 `Showing ${start + 1}–${end} of ${total}` 637 ), 638 h( 639 "div", 640 { style: { display: "flex", gap: 4 } }, 641 [ 642 h( 643 Button, 644 { 645 isSmall: true, 646 disabled: pageIndex === 0, 647 onClick: () => setPageIndex((i) => Math.max(0, i - 1)), 648 }, 649 "Previous" 650 ), 651 h( 652 "span", 653 { style: { padding: "2px 6px" } }, 654 `Page ${pageIndex + 1} of ${totalPages}` 655 ), 656 h( 657 Button, 658 { 659 isSmall: true, 660 disabled: pageIndex >= totalPages - 1, 661 onClick: () => 662 setPageIndex((i) => 663 Math.min(totalPages - 1, i + 1) 664 ), 665 }, 666 "Next" 667 ), 668 ] 669 ), 670 ] 671 ) 672 : null; 673 674 return h("div", null, [table, pager]); 497 675 }; 676 498 677 499 678 // Small iframe preview used on the "Review versions" step … … 590 769 const [showProductBImage, setShowProductBImage] = useState(false); 591 770 const [showProductBGallery, setShowProductBGallery] = useState(false); 771 772 // Track whether we've already pre-filled Version B descriptions from Version A 773 const shortHydratedRef = useRef(false); 774 const longHydratedRef = useRef(false); 592 775 593 776 // Fetch lists … … 1003 1186 null, 1004 1187 postType === "product" 1005 ? "Select a WooCommerce product to test (Version A / control). Start typing to search."1006 : "Select a page to test (Version A / control). Start typing to search."1188 ? "Select a WooCommerce product to test." 1189 : "Select a page to test." 1007 1190 ), 1008 1191 h(SearchControl, { … … 1108 1291 ? productMeta.gallery_urls.join(", ") 1109 1292 : ""; 1293 1294 // Plain-text versions for placeholders 1295 const productAShortPlain = stripHtml(productAShort); 1296 const productALongPlain = stripHtml(productALong); 1297 1298 // Prefill Version B editors with Version A content once (but let the user clear them) 1299 useEffect(() => { 1300 if ( 1301 postType === "product" && 1302 productAShort && 1303 !shortHydratedRef.current && 1304 !productBShortDesc 1305 ) { 1306 setProductBShortDesc(productAShort); 1307 shortHydratedRef.current = true; 1308 } 1309 }, [postType, productAShort, productBShortDesc]); 1310 1311 useEffect(() => { 1312 if ( 1313 postType === "product" && 1314 productALong && 1315 !longHydratedRef.current && 1316 !productBLongDesc 1317 ) { 1318 setProductBLongDesc(productALong); 1319 longHydratedRef.current = true; 1320 } 1321 }, [postType, productALong, productBLongDesc]); 1110 1322 1111 1323 /* Step 2 – Review versions */ … … 1119 1331 "p", 1120 1332 null, 1121 "Version A shows the current product fields ; Version B lets you override keyfields like title, price, descriptions and images."1333 "Version A shows the current product fields. Version B lets you override fields like title, price, descriptions and images." 1122 1334 ), 1123 1335 … … 1200 1412 [ 1201 1413 h("strong", null, "Short description"), 1202 h( 1203 "div", 1204 { 1205 style: { 1206 marginTop: 4, 1207 padding: "8px 10px", 1208 minHeight: 60, 1209 background: "#fff", 1210 border: "1px solid #dcdcde", 1211 borderRadius: 4, 1212 whiteSpace: "pre-wrap", 1213 }, 1414 h("div", { 1415 className: "abtestkit-html-preview", 1416 style: { 1417 marginTop: 4, 1418 padding: "8px 10px", 1419 minHeight: 60, 1420 background: "#fff", 1421 border: "1px solid #dcdcde", 1422 borderRadius: 4, 1214 1423 }, 1215 productAShort || "—" 1216 ), 1424 dangerouslySetInnerHTML: { 1425 __html: 1426 productAShort || 1427 "<span style='color:#6c7781'>—</span>", 1428 }, 1429 }), 1217 1430 ] 1218 1431 ), … … 1224 1437 [ 1225 1438 h("strong", null, "Description"), 1226 h( 1227 "div", 1228 { 1229 style: { 1230 marginTop: 4, 1231 padding: "8px 10px", 1232 minHeight: 80, 1233 maxHeight: 160, 1234 overflow: "auto", 1235 background: "#fff", 1236 border: "1px solid #dcdcde", 1237 borderRadius: 4, 1238 whiteSpace: "pre-wrap", 1239 }, 1439 h("div", { 1440 className: "abtestkit-html-preview", 1441 style: { 1442 marginTop: 4, 1443 padding: "8px 10px", 1444 minHeight: 80, 1445 maxHeight: 160, 1446 overflow: "auto", 1447 background: "#fff", 1448 border: "1px solid #dcdcde", 1449 borderRadius: 4, 1240 1450 }, 1241 productALong || "—" 1242 ), 1243 ] 1244 ), 1245 1246 // Main image 1247 h( 1248 "div", 1249 { style: { marginTop: 16 } }, 1250 [ 1251 h("strong", null, "Product image"), 1252 h( 1253 "div", 1254 { style: { marginTop: 4 } }, 1255 productAImageUrl 1256 ? h("img", { 1257 src: productAImageUrl, 1258 alt: "", 1259 style: { 1260 maxWidth: "100%", 1261 height: "auto", 1262 borderRadius: 4, 1263 border: "1px solid #dcdcde", 1264 }, 1265 }) 1266 : h( 1267 "span", 1268 { style: { color: "#6c7781" } }, 1269 "No image set" 1270 ) 1271 ), 1451 dangerouslySetInnerHTML: { 1452 __html: 1453 productALong || 1454 "<span style='color:#6c7781'>—</span>", 1455 }, 1456 }), 1272 1457 ] 1273 1458 ), … … 1373 1558 onChange: setProductBSalePrice, 1374 1559 placeholder: productASale || "e.g. 79.00", 1375 help:1376 "Leave blank to let WooCommerce use Version B’s regular price only.",1377 1560 }), 1378 1561 ] … … 1387 1570 [ 1388 1571 h("strong", null, "Short description"), 1389 h(TextareaControl, { 1572 h(ClassicEditorField, { 1573 id: "abtestkit-product-short-desc-b", 1390 1574 value: productBShortDesc, 1391 1575 onChange: setProductBShortDesc, 1392 rows: 4,1393 placeholder:1394 productAShort || "Version B short description…",1395 1576 }), 1396 1577 ] … … 1403 1584 [ 1404 1585 h("strong", null, "Description"), 1405 h(TextareaControl, { 1586 h(ClassicEditorField, { 1587 id: "abtestkit-product-long-desc-b", 1406 1588 value: productBLongDesc, 1407 1589 onChange: setProductBLongDesc, 1408 rows: 6,1409 placeholder:1410 productALong || "Version B full description…",1590 help: productALongPlain 1591 ? "Clear this field to reuse Version A’s full description." 1592 : "", 1411 1593 }), 1412 1594 ] -
abtestkit/trunk/readme.txt
r3406331 r3409533 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 1.0. 47 Stable tag: 1.0.5 8 8 License: GPL-2.0-or-later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 Split testing for WooCommerce & WordPress, compatible with all page builders & caching plugins.11 Split testing for WooCommerce & WordPress, compatible with all themes, page builders & caching plugins. 12 12 13 13 == Description == … … 15 15 = The simplest way to A/B test in WordPress = 16 16 17 **abtestkit** lets you run clean, fast, privacy-friendly AB tests without cod eor complicated interfaces.17 **abtestkit** lets you run clean, fast, privacy-friendly AB tests without coding or complicated interfaces. 18 18 Create full-page split tests in seconds, track performance automatically, and apply the winner with one click. 19 19 … … 62 62 Full-page testing works universally. 63 63 64 = What themes are supported? = 65 abtestkit works with all themes. 66 64 67 = How are winners decided? = 65 You don't need to analyse the results yourself.abtestkit uses a **Bayesian evaluation model** with a 95% confidence threshold, then automatically declares the winning variant. You can apply the winner with one click.68 abtestkit uses a **Bayesian evaluation model** with a 95% confidence threshold, then automatically declares the winning variant. You can apply the winner with one click. 66 69 67 70 = Where is data stored? = 68 All impression and click events are stored in your WordPress database (`wp_ab_test_events` table). Nothing is sent externally unless you explicitly opt into anonymous telemetry.71 All impression and click events are stored in your WordPress database (`wp_ab_test_events` table). 69 72 70 73 = Is this plugin free? = … … 72 75 73 76 == Changelog == 77 78 = 1.0.5 = 79 * Improved WooCommerce Product test creation UI 74 80 75 81 = 1.0.4 = … … 102 108 == Upgrade Notice == 103 109 110 = 1.0.5 = 111 Improved WooCommerce Product test creation UI 112 104 113 = 1.0.4 = 105 114 Major update with WooCommerce Product compatibility and off page click conversion goals.
Note: See TracChangeset
for help on using the changeset viewer.