Changeset 3679318
- Timestamp:
- 09/03/2026 08:19:55 AM (7 weeks ago)
- Location:
- navrail-toc-for-elementor
- Files:
-
- 10 edited
- 1 copied
-
tags/3.0.0 (copied) (copied from navrail-toc-for-elementor/trunk )
-
tags/3.0.0/assets/css/navrail-nav.css (modified) (4 diffs)
-
tags/3.0.0/assets/css/navrail-toc.css (modified) (3 diffs)
-
tags/3.0.0/assets/js/navrail-core.js (modified) (10 diffs)
-
tags/3.0.0/navrail-toc-for-elementor.php (modified) (9 diffs)
-
tags/3.0.0/readme.txt (modified) (3 diffs)
-
trunk/assets/css/navrail-nav.css (modified) (4 diffs)
-
trunk/assets/css/navrail-toc.css (modified) (3 diffs)
-
trunk/assets/js/navrail-core.js (modified) (10 diffs)
-
trunk/navrail-toc-for-elementor.php (modified) (9 diffs)
-
trunk/readme.txt (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
navrail-toc-for-elementor/tags/3.0.0/assets/css/navrail-nav.css
r3671253 r3679318 21 21 --navrail-nav-media-direction: row; 22 22 --navrail-nav-item-height: 0px; 23 --navrail-nav-items-gap: 24px; 24 --navrail-nav-item-padding-block: 8px; 25 --navrail-nav-item-padding-inline: 16px; 23 26 24 27 display: flex; … … 50 53 flex-wrap: nowrap; 51 54 align-items: center; 55 gap: var(--navrail-nav-items-gap); 52 56 list-style: none; 53 57 margin: 0; … … 80 84 gap: var(--navrail-nav-title-spacing); 81 85 min-height: var(--navrail-nav-item-height); 86 padding-block: var(--navrail-nav-item-padding-block); 87 padding-inline: var(--navrail-nav-item-padding-inline); 82 88 max-width: 100%; 83 89 white-space: nowrap; … … 128 134 the link's accessible name instead of being removed from the DOM. */ 129 135 .navrail-nav__title--visually-hidden { 136 position: absolute; 137 width: 1px; 138 height: 1px; 139 padding: 0; 140 margin: -1px; 141 overflow: hidden; 142 clip: rect(0, 0, 0, 0); 143 white-space: nowrap; 144 border: 0; 145 } 146 147 /* Used by the Gutenberg "Navrail Navigation" block only: since its Items 148 are static, individually-saved child blocks that can't know the parent's 149 "Show Titles" setting at save time (block context isn't available inside 150 a static block's save()), the parent's own wrapper carries this class 151 instead and hides every descendant title with one rule — functionally 152 identical to the per-item class above that Elementor's PHP applies. */ 153 .navrail-nav--hide-titles .navrail-nav__title { 130 154 position: absolute; 131 155 width: 1px; -
navrail-toc-for-elementor/tags/3.0.0/assets/css/navrail-toc.css
r3661787 r3679318 21 21 --htoc-separator-width: 1px; 22 22 --htoc-separator-height: 16px; 23 --htoc-items-gap: 24px; 24 --htoc-item-padding-block: 8px; 25 --htoc-item-padding-inline: 16px; 23 26 24 27 display: flex; … … 67 70 flex-wrap: nowrap; 68 71 align-items: center; 72 gap: var(--htoc-items-gap); 69 73 list-style: none; 70 74 margin: 0; … … 95 99 display: inline-flex; 96 100 align-items: center; 101 padding-block: var(--htoc-item-padding-block); 102 padding-inline: var(--htoc-item-padding-inline); 97 103 max-width: 100%; 98 104 white-space: nowrap; -
navrail-toc-for-elementor/tags/3.0.0/assets/js/navrail-core.js
r3671253 r3679318 21 21 var NavrailCore = {}; 22 22 23 var _rtlUsesNegativeScrollLeft = null; 24 25 /** 26 * One-time, cached, purely behavioral detection of which `scrollLeft` 27 * convention this browser engine uses for RTL scroll containers. 28 * Every evergreen engine today follows the CSSOM View spec (0 at 29 * reading-start, running down to `-max` at reading-end), but two 30 * older conventions existed historically: a positive-only scheme 31 * (`max` at reading-start, down to `0` at reading-end) and a scheme 32 * that left `scrollLeft` completely unadjusted for direction 33 * (numerically identical to LTR — `0` to `+max`). Both older schemes 34 * share the trait this test relies on: neither accepts a negative 35 * `scrollLeft` value, so assigning one gets silently clamped back to 36 * `0`. Only the modern convention actually allows it to go negative. 37 * A single assignment-and-read-back on a disposable probe element 38 * distinguishes the two cases without depending on any element's 39 * live scroll position — unlike inferring the convention from the 40 * sign of a real track's current `scrollLeft`, which is ambiguous 41 * exactly at the boundary where that value is `0`. 42 * 43 * @return {boolean} True if this engine uses the modern (negative) 44 * convention. 45 */ 46 function rtlUsesNegativeScrollLeft() { 47 if ( null !== _rtlUsesNegativeScrollLeft ) { 48 return _rtlUsesNegativeScrollLeft; 49 } 50 51 if ( 'undefined' === typeof document || ! document.body ) { 52 // No DOM available to probe. Assume the modern spec — true 53 // for every current evergreen engine. 54 return true; 55 } 56 57 var probe = document.createElement( 'div' ); 58 probe.setAttribute( 'dir', 'rtl' ); 59 probe.style.cssText = 'position:absolute;visibility:hidden;top:-9999px;left:-9999px;width:2px;height:2px;overflow:scroll;'; 60 61 var inner = document.createElement( 'div' ); 62 inner.style.cssText = 'width:6px;height:1px;'; 63 probe.appendChild( inner ); 64 65 document.body.appendChild( probe ); 66 probe.scrollLeft = -1; 67 _rtlUsesNegativeScrollLeft = ( probe.scrollLeft < 0 ); 68 document.body.removeChild( probe ); 69 70 return _rtlUsesNegativeScrollLeft; 71 } 72 23 73 /** 24 74 * @param {Function} fn … … 64 114 this.resizeObserver = null; 65 115 this._fadeRaf = null; 116 this._isProgrammaticScroll = false; 66 117 67 118 this.onClick = this.onClick.bind( this ); … … 228 279 229 280 var activeItem = this.getItemById( id ); 230 if ( activeItem ) {281 if ( activeItem && ! this._isProgrammaticScroll ) { 231 282 this.scrollItemIntoView( activeItem.linkEl ); 232 283 } … … 234 285 235 286 /** 236 * Brings the active item into view inside the horizontally scrollable 237 * track using the native `scrollIntoView` `inline` alignment option. 238 * This is deliberately used instead of manually computing `scrollLeft`, 239 * because browsers have historically disagreed on how `scrollLeft` 240 * behaves in RTL contexts; `scrollIntoView`'s `inline` option is 241 * handled internally by the browser per the current CSSOM View spec 242 * and is consistent across engines regardless of that discrepancy. 243 * `block: 'nearest'` prevents any accidental vertical page scroll. 287 * Brings the active item into view by setting `this.track`'s own 288 * `scrollLeft` directly — never `Element.scrollIntoView()`. 289 * 290 * `scrollIntoView()` was used originally because manually computing 291 * `scrollLeft` in RTL is awkward (see `getNormalizedScrollLeft`). 292 * But `scrollIntoView()` also walks up and scrolls every *other* 293 * ancestor scroll container needed to bring the element into view — 294 * including the page itself — via its `block` option. `block: 295 * 'nearest'` only minimizes that vertical scroll, it does not 296 * eliminate it: once this widget's own bar has scrolled out of the 297 * vertical viewport (routine during organic scrolling past earlier 298 * headings, since the bar itself lives at a fixed point on the page), 299 * "nearest" for the *link* is no longer "no scroll" — the browser 300 * still has to move the page to make an off-screen link visible at 301 * all, which fights whatever scroll the reader is already doing. 302 * 303 * Setting `scrollLeft` on `this.track` touches only that element. 304 * There is no `block` option and no ancestor involved, so this is 305 * structurally incapable of moving the page vertically — regardless 306 * of whether the track happens to be on-screen at the moment. 244 307 * 245 308 * @param {HTMLElement} linkEl 246 309 */ 247 310 Base.prototype.scrollItemIntoView = function ( linkEl ) { 311 if ( ! this.track ) { 312 return; 313 } 314 315 var max = this.track.scrollWidth - this.track.clientWidth; 316 317 if ( max <= 0 ) { 318 return; 319 } 320 248 321 var align = this.getResponsiveValue( this.config.align ) || 'nearest'; 322 var viewportWidth = this.track.clientWidth; 323 var edges = this.getContentEdges( linkEl, max ); 324 var current = this.getNormalizedScrollLeft( this.track, max ); 325 var target; 326 327 if ( 'start' === align ) { 328 target = edges.start; 329 } else if ( 'end' === align ) { 330 target = edges.end - viewportWidth; 331 } else if ( 'center' === align ) { 332 target = ( edges.start + edges.end ) / 2 - ( viewportWidth / 2 ); 333 } else if ( edges.start < current ) { 334 // 'nearest': item's start edge has scrolled past the 335 // viewport's start edge — pull it back into view there. 336 target = edges.start; 337 } else if ( edges.end > current + viewportWidth ) { 338 // Item's end edge is past the viewport's end edge. 339 target = edges.end - viewportWidth; 340 } else { 341 // Already fully visible; 'nearest' means "do nothing". 342 return; 343 } 344 345 target = Math.max( 0, Math.min( max, target ) ); 346 249 347 var reducedMotion = !! ( window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches ); 250 251 try { 252 linkEl.scrollIntoView( { 253 behavior: reducedMotion ? 'auto' : 'smooth', 254 block: 'nearest', 255 inline: align 256 } ); 257 } catch ( e ) { 258 linkEl.scrollIntoView(); 348 var track = this.track; 349 var previousBehavior = track.style.scrollBehavior; 350 351 // The stylesheet sets `scroll-behavior: auto` on the track so 352 // drag/wheel scrolling stays instant; switching to `smooth` only 353 // for this JS-driven move, then reverting once it's had time to 354 // finish, keeps that native drag/wheel feel intact. 355 track.style.scrollBehavior = reducedMotion ? 'auto' : 'smooth'; 356 this.setNormalizedScrollLeft( track, max, target ); 357 358 if ( ! reducedMotion ) { 359 setTimeout( function () { 360 track.style.scrollBehavior = previousBehavior; 361 }, 500 ); 362 } else { 363 track.style.scrollBehavior = previousBehavior; 259 364 } 260 365 }; … … 429 534 } 430 535 536 // While a programmatic scroll to a target is in flight, the 537 // scroll-spy observer below still fires as the viewport passes 538 // other sections along the way — that's fine for updating which 539 // link shows as active, but setActive() must not also snap the 540 // horizontal track to whichever link that is. This widget's own 541 // bar commonly scrolls out of the vertical viewport during a 542 // scroll to a distant target, and scrollIntoView({block:'nearest'}) 543 // on a now off-screen link pulls the page back to reveal the bar, 544 // directly fighting the scroll this function just started — which 545 // is exactly why clicking any link could end up settling back near 546 // wherever the bar itself lives instead of the intended target. 547 this._isProgrammaticScroll = true; 548 431 549 var lastTarget = computeTarget(); 432 550 … … 437 555 438 556 if ( reducedMotion ) { 557 setTimeout( function () { 558 self._isProgrammaticScroll = false; 559 }, 100 ); 439 560 return; 440 561 } … … 455 576 if ( attempts < maxAttempts ) { 456 577 setTimeout( correct, 120 ); 578 } else { 579 // A little extra margin past the last correction for the 580 // browser's own smooth-scroll animation to fully settle. 581 setTimeout( function () { 582 self._isProgrammaticScroll = false; 583 }, 200 ); 457 584 } 458 585 }() ); … … 477 604 /** 478 605 * Normalizes `scrollLeft` in RTL to a value that always runs from 0 479 * (reading-start) to `max` (reading-end), regardless of which of the 480 * three historical browser conventions for RTL `scrollLeft` is in 481 * effect (0 -> -max, 0 -> +max, or +max -> 0). The convention is 482 * detected at read-time from the sign of the live value rather than by 483 * sniffing the browser, which keeps this correct across engines and 484 * future browser versions. 606 * (reading-start) to `max` (reading-end), regardless of which RTL 607 * `scrollLeft` convention this engine uses (see 608 * `rtlUsesNegativeScrollLeft()`). 485 609 * 486 610 * @param {HTMLElement} el … … 489 613 */ 490 614 Base.prototype.getNormalizedScrollLeft = function ( el, max ) { 491 var scrollLeft = el.scrollLeft;492 493 615 if ( ! this.isRTL ) { 494 return scrollLeft; 495 } 496 497 return scrollLeft > 0 ? ( max - scrollLeft ) : ( -scrollLeft ); 616 return el.scrollLeft; 617 } 618 619 return rtlUsesNegativeScrollLeft() ? ( -el.scrollLeft ) : ( max - el.scrollLeft ); 620 }; 621 622 /** 623 * The write-side inverse of `getNormalizedScrollLeft()`: takes a 624 * normalized value (0 = reading-start, `max` = reading-end) and 625 * assigns the correctly-converted raw `scrollLeft` for whichever RTL 626 * convention this engine uses. 627 * 628 * @param {HTMLElement} el 629 * @param {number} max 630 * @param {number} value 631 */ 632 Base.prototype.setNormalizedScrollLeft = function ( el, max, value ) { 633 if ( ! this.isRTL ) { 634 el.scrollLeft = value; 635 return; 636 } 637 638 el.scrollLeft = rtlUsesNegativeScrollLeft() ? ( -value ) : ( max - value ); 639 }; 640 641 /** 642 * Computes `linkEl`'s horizontal position as two scroll-invariant 643 * offsets — `start` and `end` — expressed in the same 0-to-`max` 644 * normalized coordinate space as `getNormalizedScrollLeft()` 645 * (distance along reading order from the track's reading-start edge). 646 * Being scroll-invariant is what makes these directly comparable 647 * against a current or target normalized `scrollLeft`: the values 648 * don't change no matter what the track's scroll position was at the 649 * moment this is called. 650 * 651 * Built from `getBoundingClientRect()` (physical viewport pixels, 652 * always left-to-right regardless of `dir`) plus the track's current 653 * normalized scroll offset, rather than `offsetLeft`, so this doesn't 654 * depend on assumptions about which ancestor happens to be the 655 * element's `offsetParent`. 656 * 657 * @param {HTMLElement} linkEl 658 * @param {number} max 659 * @return {{start: number, end: number}} 660 */ 661 Base.prototype.getContentEdges = function ( linkEl, max ) { 662 var trackRect = this.track.getBoundingClientRect(); 663 var linkRect = linkEl.getBoundingClientRect(); 664 var current = this.getNormalizedScrollLeft( this.track, max ); 665 var physLeft = linkRect.left - trackRect.left; 666 var physRight = linkRect.right - trackRect.left; 667 668 if ( ! this.isRTL ) { 669 return { start: physLeft + current, end: physRight + current }; 670 } 671 672 var viewportWidth = this.track.clientWidth; 673 674 // Reading order runs right-to-left, so the link's reading-order 675 // "start" edge is its physical *right* edge, and its "end" edge 676 // is its physical *left* edge. 677 return { 678 start: viewportWidth + current - physRight, 679 end: viewportWidth + current - physLeft 680 }; 498 681 }; 499 682 … … 641 824 642 825 /** 643 * Elementor's own frontend script can, depending on the site's 644 * asset loading configuration (e.g. deferred/module script 645 * loading), finish initializing either before or after this file 646 * executes — the order scripts appear in the HTML does not 647 * guarantee execution order once defer/module scripts are 648 * involved. Guard both directions: register immediately if 649 * `elementorFrontend` is already ready, otherwise wait for the 650 * `elementor/frontend/init` event that Elementor fires exactly 651 * once, globally, the moment it finishes initializing — this is 652 * the documented, timing-safe hook point for third-party scripts. 826 * Catch-all for every matching element Elementor's own lifecycle will 827 * never reach — chiefly this widget's markup rendered by a Gutenberg 828 * block, but really anything not inside Elementor-managed content. 829 * This runs unconditionally, even on a site where Elementor happens 830 * to be active for OTHER content on the same page (its frontend 831 * script commonly loads site-wide), since that alone doesn't mean 832 * *this* element is one of its widgets. 833 * 834 * Elementor-rendered instances are deliberately skipped here (even 835 * if Elementor hasn't reached them yet) so each is only ever 836 * initialized once, via the frontend/element_ready hook above — 837 * which, unlike this one-time scan, also fires on every live-preview 838 * re-render inside the Elementor editor. 653 839 */ 840 function scanForUninitialized() { 841 var nodes = document.querySelectorAll( rootSelector ); 842 Array.prototype.forEach.call( nodes, function ( el ) { 843 if ( el[ instanceProp ] || el.closest( '.elementor' ) ) { 844 return; 845 } 846 initWidget( el ); 847 } ); 848 } 849 654 850 if ( window.elementorFrontend && window.elementorFrontend.hooks ) { 655 851 registerElementReadyHook(); 656 852 } else if ( window.jQuery ) { 657 853 window.jQuery( window ).on( 'elementor/frontend/init', registerElementReadyHook ); 854 } 855 856 if ( 'loading' === document.readyState ) { 857 document.addEventListener( 'DOMContentLoaded', scanForUninitialized ); 658 858 } else { 659 document.addEventListener( 'DOMContentLoaded', function () { 660 if ( window.elementorFrontend && window.elementorFrontend.hooks ) { 661 registerElementReadyHook(); 662 } 663 } ); 859 scanForUninitialized(); 664 860 } 665 861 }; -
navrail-toc-for-elementor/tags/3.0.0/navrail-toc-for-elementor.php
r3671253 r3679318 2 2 /** 3 3 * Plugin Name: Navrail – Horizontal Navigation & Table of Contents 4 * Description: Horizontal, auto-scrolling navigation bars — built automatically from headings (Navrail TOC) or manually, item by item (Navrail Navigation). Currently available as an Elementor widget.5 * Version: 2.0.04 * Description: Horizontal, auto-scrolling navigation bars — built automatically from headings (Navrail TOC) or manually, item by item (Navrail Navigation). Available as Elementor widgets and Gutenberg blocks. 5 * Version: 3.0.0 6 6 * Requires at least: 6.3 7 7 * Requires PHP: 7.4 … … 20 20 } 21 21 22 define( 'NAVRAIL_VERSION', ' 2.0.0' );22 define( 'NAVRAIL_VERSION', '3.0.0' ); 23 23 define( 'NAVRAIL_PLUGIN_FILE', __FILE__ ); 24 24 define( 'NAVRAIL_PATH', plugin_dir_path( __FILE__ ) ); … … 28 28 * Main plugin bootstrap class. 29 29 * 30 * Kept as a single, small bootstrap class on purpose: this plugin registers 31 * exactly one widget, so a larger loader/service-container architecture would 32 * add indirection without adding value. 30 * Registers, depending on what's active: Gutenberg blocks (always, since 31 * the block editor is WordPress core) and Elementor widgets (only when 32 * Elementor is installed and active). Both integrations share the same 33 * underlying JS/CSS — see register_scripts()/register_styles(). 33 34 */ 34 35 final class NAVRAIL_Plugin { … … 73 74 } 74 75 76 // Scripts, styles, and Gutenberg blocks are registered regardless of 77 // Elementor: the block editor is a WordPress core feature, and the 78 // same shared assets now serve both integrations (see 79 // register_scripts() for how the two are told apart at runtime). 80 add_action( 'init', array( $this, 'register_scripts' ) ); 81 add_action( 'init', array( $this, 'register_styles' ) ); 82 add_action( 'init', array( $this, 'register_blocks' ) ); 83 75 84 if ( ! did_action( 'elementor/loaded' ) ) { 76 85 add_action( 'admin_notices', array( $this, 'admin_notice_missing_elementor' ) ); … … 85 94 add_action( 'elementor/widgets/register', array( $this, 'register_widgets' ) ); 86 95 add_action( 'elementor/elements/categories_registered', array( $this, 'register_category' ) ); 87 add_action( 'elementor/frontend/before_register_scripts', array( $this, 'register_scripts' ) ); 88 add_action( 'elementor/frontend/before_register_styles', array( $this, 'register_styles' ) ); 89 } 90 91 /** 92 * Registers script/style handles with Elementor's asset manager without 93 * enqueueing them. Elementor only prints/enqueues these when a widget 94 * whose get_script_depends()/get_style_depends() references them is 95 * actually present on the rendered page (frontend or editor preview). 96 } 97 98 /** 99 * Per-file cache-busting version: the file's own `filemtime()` when 100 * it can be read, falling back to `NAVRAIL_VERSION` (e.g. the file 101 * is missing, or the filesystem call is unavailable/restricted). 102 * Using the real modification time — rather than one shared plugin 103 * version for every asset — means a browser is guaranteed to fetch 104 * the new copy of exactly the file that changed the moment it 105 * changes, with no reliance on remembering to bump a version string. 106 * 107 * @param string $relative_path Path relative to the plugin root, e.g. 'assets/js/navrail-core.js'. 108 * @return string 109 */ 110 private function get_asset_version( $relative_path ) { 111 $absolute_path = NAVRAIL_PATH . $relative_path; 112 $mtime = @filemtime( $absolute_path ); 113 114 return $mtime ? (string) $mtime : NAVRAIL_VERSION; 115 } 116 117 /** 118 * Registers script handles on `init` — early enough for the Gutenberg 119 * blocks' block.json `viewScript`/`style` references (bare handle 120 * names, not `file:` paths) to resolve correctly, and equally usable by 121 * Elementor's widgets via get_script_depends(), which doesn't care when 122 * or how a handle was registered, only that it exists by print time. 96 123 */ 97 124 public function register_scripts() { 125 $elementor_active = (bool) did_action( 'elementor/loaded' ); 126 127 $core_deps = array( 'jquery' ); 128 if ( $elementor_active ) { 129 $core_deps[] = 'elementor-frontend'; 130 } 131 98 132 wp_register_script( 99 133 'navrail-core', 100 134 NAVRAIL_URL . 'assets/js/navrail-core.js', 101 array( 'jquery', 'elementor-frontend' ),102 NAVRAIL_VERSION,135 $core_deps, 136 $this->get_asset_version( 'assets/js/navrail-core.js' ), 103 137 true 104 138 ); … … 108 142 NAVRAIL_URL . 'assets/js/navrail-toc.js', 109 143 array( 'navrail-core' ), 110 NAVRAIL_VERSION,144 $this->get_asset_version( 'assets/js/navrail-toc.js' ), 111 145 true 112 146 ); … … 116 150 NAVRAIL_URL . 'assets/js/navrail-nav.js', 117 151 array( 'navrail-core' ), 118 NAVRAIL_VERSION,152 $this->get_asset_version( 'assets/js/navrail-nav.js' ), 119 153 true 120 154 ); … … 134 168 NAVRAIL_URL . 'assets/css/navrail-toc.css', 135 169 array(), 136 NAVRAIL_VERSION170 $this->get_asset_version( 'assets/css/navrail-toc.css' ) 137 171 ); 138 172 … … 141 175 NAVRAIL_URL . 'assets/css/navrail-nav.css', 142 176 array(), 143 NAVRAIL_VERSION 144 ); 177 $this->get_asset_version( 'assets/css/navrail-nav.css' ) 178 ); 179 } 180 181 /** 182 * Registers the Gutenberg blocks from their compiled build/ directories. 183 * Each block.json's `viewScript`/`style` fields reference the same 184 * handles registered in register_scripts()/register_styles() above by 185 * name, so the exact same frontend JS/CSS shipped for the Elementor 186 * widgets also runs these blocks — nothing is duplicated. 187 */ 188 public function register_blocks() { 189 foreach ( array( 'toc', 'navigation', 'navigation-item' ) as $block ) { 190 $path = NAVRAIL_PATH . 'build/' . $block; 191 192 if ( file_exists( $path . '/block.json' ) ) { 193 register_block_type( $path ); 194 } 195 } 145 196 } 146 197 -
navrail-toc-for-elementor/tags/3.0.0/readme.txt
r3671253 r3679318 1 1 === Navrail – Horizontal Navigation & Table of Contents === 2 2 Contributors: hadiomid 3 Tags: elementor, table of contents, toc, navigation3 Tags: elementor, gutenberg, table of contents, toc, navigation 4 4 Requires at least: 6.3 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 2.0.07 Stable tag: 3.0.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 Create smooth horizontal navigation from your headings, or build custom navigation with icons and images. Made for Elementor.11 Turn your headings into a smooth horizontal navigation bar automatically, or build one by hand. Works with Elementor or Gutenberg. 12 12 13 13 == Description == 14 14 15 Navrail adds two widgets for horizontal, auto-scrolling navigation bars, the same pattern used by modern landing pages, documentation sites, and long-form pages to help visitors jump straight to the section they care about. Currently available as Elementor widgets.15 Navrail adds a horizontal, auto-scrolling navigation bar to your page. This is the same style of navigation used on many landing pages, documentation sites, and long articles, so visitors can jump straight to the part they want. It works with Elementor and with the WordPress block editor (Gutenberg). Elementor is optional. 16 16 17 **Navrail TOC** builds the navigation for you. Pick which heading levels to track, and it detects your sections automatically, smooth-scrolls to them on click, highlights the active one as visitors scroll, and keeps it in view. No manual list-building.17 **Navrail TOC** builds the navigation for you. Choose which heading levels to track (H2, H3, and so on), and it finds your sections automatically. It smooth-scrolls to a section when clicked, and highlights the current section as visitors scroll. No manual list-building needed. 18 18 19 **Navrail Navigation** builds the navigation from a list you configure by hand: one item at a time, each with its own title, optional icon or image, and a link, whether to a section on the page (with the same smooth scroll and active-item tracking as Navrail TOC) or to any normal URL, `tel:`, or `mailto:` link.19 **Navrail Navigation** lets you build the navigation by hand. Add items one by one, each with a title, an optional icon or image, and a link. A link can point to a section on the same page (with the same smooth scroll and highlighting as Navrail TOC), or to any normal URL, phone number, or email address. 20 20 21 Either widget works wellfor:21 Good for: 22 22 23 * Landing page sectionnavigation24 * Long -formsales pages23 * Landing page navigation 24 * Long sales pages 25 25 * Documentation 26 26 * Service and product pages 27 27 * One-page websites 28 * Long articles built with Elementor28 * Long articles 29 29 30 **Navrail TOC **30 **Navrail TOC features** 31 31 32 * Works with any Elementor page structure: headings, sections, anything with a heading tag 33 * Include/Exclude selectors for precise control over which headings are picked up 34 * Optional item separators and hierarchical numbering (1, 1.1, 1.2, 2, ...) 35 * Optional leading text with Dynamic Tags support 36 * Optional title truncation, by width, character count, or word count 32 * Works with any page: headings, sections, anything with a heading tag 33 * Choose which parts of the page to include or skip 34 * Optional numbers before each item (1, 1.1, 1.2, 2, ...) 35 * Optional line separators between items 36 * Optional fixed label before the list 37 * Shortens long titles automatically (by width, character count, or word count) 38 * Optional "stick to top/bottom" behavior 37 39 38 **Navrail Navigation **40 **Navrail Navigation features** 39 41 40 * Manually-configured items via a repeater: title, optional icon or image,link41 * Icon/image position (before, after, above, or below the title), set independently per breakpoint42 * Min Height control to keep items a consistent heightwhen some have an icon/image and others don't43 * Independent Normal/Hover/Active styling for both the item and its title42 * Add items by hand: title, optional icon or image, and a link 43 * Place the icon/image before, after, above, or below the title 44 * Keep all items the same height, even when some have an icon/image and others don't 45 * Separate colors and styles for Normal, Hover, and Active states 44 46 45 ** Shared by both widgets**47 **Works the same in Elementor and the block editor** 46 48 47 * Smooth scroll with active-item highlighting 48 * Auto-scrolling, swipeable navigation bar with edge fade 49 * Adjustable width, alignment, and full style control: colors, borders, spacing, independent Normal/Hover/Active states 50 * Sticky positioning support 51 * RTL-ready 52 * Lightweight, no external libraries, loads only where the widget is used 49 * Smooth scroll with highlighting of the current item 50 * Auto-scrolling, swipeable bar with a soft fade at each edge 51 * Right-to-left (RTL) language support 52 * Lightweight: no external libraries, and only loads on pages where it's used 53 * Elementor also lets you set separate values for Desktop, Tablet, and Mobile; the block editor uses one value for all screen sizes 53 54 54 55 == Installation == … … 56 57 1. Upload the plugin ZIP via Plugins → Add New → Upload Plugin, or extract it into `wp-content/plugins/`. 57 58 2. Activate "Navrail". 58 3. Edit any page with Elementor and drag either the "Navrail TOC" widget (auto-detects headings) or the "Navrail Navigation" widget (manually-configured items) — both are in the "Navrail" category — onto the page. 59 3. **Elementor**: edit a page with Elementor and drag in the "Navrail TOC" widget (finds headings automatically) or the "Navrail Navigation" widget (build the list yourself). Both are in the "Navrail" category. 60 **Block editor**: open the block inserter and search "Navrail". Add the "Navrail TOC" block, or the "Navrail Navigation" block (add "Navigation Item" blocks inside it for each link). 59 61 60 62 == Frequently Asked Questions == 61 63 64 = Is Elementor required? = 65 66 No. The blocks work in the standard WordPress block editor with no other plugin installed. Elementor is only needed if you want to use the Elementor widgets instead. 67 62 68 = What's the difference between Navrail TOC and Navrail Navigation? = 63 69 64 Navrail TOC builds its list automatically from headings already on the page. Navrail Navigation is fully manual: you add each item, its label, an optional icon or image, and a link yourself, whether or not it points at this page.70 Navrail TOC builds its list automatically from the headings already on your page. Navrail Navigation is fully manual: you add each item, its label, an optional icon or image, and a link yourself. 65 71 66 = Is this only for Table of Contents? =72 = Is this only for a Table of Contents? = 67 73 68 No. It's a general -purpose horizontal section navigation. Table of Contents is one common use, but it works just as well for landing page section jumps, documentation, service/product page navigation, and long single-page sites.74 No. It's a general horizontal navigation bar. A Table of Contents is one common use, but it also works well for landing page navigation, documentation, product pages, and one-page sites. 69 75 70 76 = Is this plugin free? = 71 77 72 Yes, completely. Every feature described above is included, no premium version, no upsells, no ads. 73 74 = Does this require Elementor Pro? = 75 76 No. Only the free Elementor plugin is required. 78 Yes, completely free. No premium version, no upsells, no ads. 77 79 78 80 = Why don't I see any items? = 79 81 80 For Navrail TOC: the widget builds its navigation from headings already on the page. Make sure the selected Heading Levels match the headings actually used in your content. For Navrail Navigation: make sure the repeater has items added under Content → Navigation Items.82 For Navrail TOC: it builds its list from headings already on the page, so check that the Heading Levels you selected match the headings you actually used. For Navrail Navigation: make sure you've added items (the repeater panel in Elementor, or "Navigation Item" child blocks in the block editor). 81 83 82 This plugin is an independent, third-party extension for Elementor and is not affiliated with or endorsed by Elementor.com.84 This plugin is an independent, third-party extension and is not affiliated with or endorsed by Elementor.com or the WordPress Gutenberg project. 83 85 84 86 == Screenshots == … … 87 89 2. Frontend display of the horizontal TOC navigation bar 88 90 3. Frontend display of a manually-configured Navrail Navigation bar 91 4. Style settings panel in the block editor (Gutenberg) 89 92 90 93 == Changelog == 94 95 = 3.0.0 = 96 * Added: full support for the WordPress block editor (Gutenberg) — "Navrail TOC" and "Navrail Navigation" blocks, with the same features as the Elementor widgets: numbering, separators, a leading label, title shortening, sticky positioning, and separate Normal/Hover/Active styling. The block editor's leading label is plain text; Elementor's version also supports Dynamic Tags. 97 * Changed: renamed the plugin from "Navrail TOC for Elementor" to "Navrail – Horizontal Navigation & Table of Contents", since it no longer needs Elementor. The plugin's link on WordPress.org stays the same. 98 * Changed: Elementor is now optional. The plugin and its blocks work with no other plugin installed; the Elementor widgets simply stay inactive (with a friendly notice) until Elementor is installed. 91 99 92 100 = 2.0.0 = -
navrail-toc-for-elementor/trunk/assets/css/navrail-nav.css
r3671253 r3679318 21 21 --navrail-nav-media-direction: row; 22 22 --navrail-nav-item-height: 0px; 23 --navrail-nav-items-gap: 24px; 24 --navrail-nav-item-padding-block: 8px; 25 --navrail-nav-item-padding-inline: 16px; 23 26 24 27 display: flex; … … 50 53 flex-wrap: nowrap; 51 54 align-items: center; 55 gap: var(--navrail-nav-items-gap); 52 56 list-style: none; 53 57 margin: 0; … … 80 84 gap: var(--navrail-nav-title-spacing); 81 85 min-height: var(--navrail-nav-item-height); 86 padding-block: var(--navrail-nav-item-padding-block); 87 padding-inline: var(--navrail-nav-item-padding-inline); 82 88 max-width: 100%; 83 89 white-space: nowrap; … … 128 134 the link's accessible name instead of being removed from the DOM. */ 129 135 .navrail-nav__title--visually-hidden { 136 position: absolute; 137 width: 1px; 138 height: 1px; 139 padding: 0; 140 margin: -1px; 141 overflow: hidden; 142 clip: rect(0, 0, 0, 0); 143 white-space: nowrap; 144 border: 0; 145 } 146 147 /* Used by the Gutenberg "Navrail Navigation" block only: since its Items 148 are static, individually-saved child blocks that can't know the parent's 149 "Show Titles" setting at save time (block context isn't available inside 150 a static block's save()), the parent's own wrapper carries this class 151 instead and hides every descendant title with one rule — functionally 152 identical to the per-item class above that Elementor's PHP applies. */ 153 .navrail-nav--hide-titles .navrail-nav__title { 130 154 position: absolute; 131 155 width: 1px; -
navrail-toc-for-elementor/trunk/assets/css/navrail-toc.css
r3661787 r3679318 21 21 --htoc-separator-width: 1px; 22 22 --htoc-separator-height: 16px; 23 --htoc-items-gap: 24px; 24 --htoc-item-padding-block: 8px; 25 --htoc-item-padding-inline: 16px; 23 26 24 27 display: flex; … … 67 70 flex-wrap: nowrap; 68 71 align-items: center; 72 gap: var(--htoc-items-gap); 69 73 list-style: none; 70 74 margin: 0; … … 95 99 display: inline-flex; 96 100 align-items: center; 101 padding-block: var(--htoc-item-padding-block); 102 padding-inline: var(--htoc-item-padding-inline); 97 103 max-width: 100%; 98 104 white-space: nowrap; -
navrail-toc-for-elementor/trunk/assets/js/navrail-core.js
r3671253 r3679318 21 21 var NavrailCore = {}; 22 22 23 var _rtlUsesNegativeScrollLeft = null; 24 25 /** 26 * One-time, cached, purely behavioral detection of which `scrollLeft` 27 * convention this browser engine uses for RTL scroll containers. 28 * Every evergreen engine today follows the CSSOM View spec (0 at 29 * reading-start, running down to `-max` at reading-end), but two 30 * older conventions existed historically: a positive-only scheme 31 * (`max` at reading-start, down to `0` at reading-end) and a scheme 32 * that left `scrollLeft` completely unadjusted for direction 33 * (numerically identical to LTR — `0` to `+max`). Both older schemes 34 * share the trait this test relies on: neither accepts a negative 35 * `scrollLeft` value, so assigning one gets silently clamped back to 36 * `0`. Only the modern convention actually allows it to go negative. 37 * A single assignment-and-read-back on a disposable probe element 38 * distinguishes the two cases without depending on any element's 39 * live scroll position — unlike inferring the convention from the 40 * sign of a real track's current `scrollLeft`, which is ambiguous 41 * exactly at the boundary where that value is `0`. 42 * 43 * @return {boolean} True if this engine uses the modern (negative) 44 * convention. 45 */ 46 function rtlUsesNegativeScrollLeft() { 47 if ( null !== _rtlUsesNegativeScrollLeft ) { 48 return _rtlUsesNegativeScrollLeft; 49 } 50 51 if ( 'undefined' === typeof document || ! document.body ) { 52 // No DOM available to probe. Assume the modern spec — true 53 // for every current evergreen engine. 54 return true; 55 } 56 57 var probe = document.createElement( 'div' ); 58 probe.setAttribute( 'dir', 'rtl' ); 59 probe.style.cssText = 'position:absolute;visibility:hidden;top:-9999px;left:-9999px;width:2px;height:2px;overflow:scroll;'; 60 61 var inner = document.createElement( 'div' ); 62 inner.style.cssText = 'width:6px;height:1px;'; 63 probe.appendChild( inner ); 64 65 document.body.appendChild( probe ); 66 probe.scrollLeft = -1; 67 _rtlUsesNegativeScrollLeft = ( probe.scrollLeft < 0 ); 68 document.body.removeChild( probe ); 69 70 return _rtlUsesNegativeScrollLeft; 71 } 72 23 73 /** 24 74 * @param {Function} fn … … 64 114 this.resizeObserver = null; 65 115 this._fadeRaf = null; 116 this._isProgrammaticScroll = false; 66 117 67 118 this.onClick = this.onClick.bind( this ); … … 228 279 229 280 var activeItem = this.getItemById( id ); 230 if ( activeItem ) {281 if ( activeItem && ! this._isProgrammaticScroll ) { 231 282 this.scrollItemIntoView( activeItem.linkEl ); 232 283 } … … 234 285 235 286 /** 236 * Brings the active item into view inside the horizontally scrollable 237 * track using the native `scrollIntoView` `inline` alignment option. 238 * This is deliberately used instead of manually computing `scrollLeft`, 239 * because browsers have historically disagreed on how `scrollLeft` 240 * behaves in RTL contexts; `scrollIntoView`'s `inline` option is 241 * handled internally by the browser per the current CSSOM View spec 242 * and is consistent across engines regardless of that discrepancy. 243 * `block: 'nearest'` prevents any accidental vertical page scroll. 287 * Brings the active item into view by setting `this.track`'s own 288 * `scrollLeft` directly — never `Element.scrollIntoView()`. 289 * 290 * `scrollIntoView()` was used originally because manually computing 291 * `scrollLeft` in RTL is awkward (see `getNormalizedScrollLeft`). 292 * But `scrollIntoView()` also walks up and scrolls every *other* 293 * ancestor scroll container needed to bring the element into view — 294 * including the page itself — via its `block` option. `block: 295 * 'nearest'` only minimizes that vertical scroll, it does not 296 * eliminate it: once this widget's own bar has scrolled out of the 297 * vertical viewport (routine during organic scrolling past earlier 298 * headings, since the bar itself lives at a fixed point on the page), 299 * "nearest" for the *link* is no longer "no scroll" — the browser 300 * still has to move the page to make an off-screen link visible at 301 * all, which fights whatever scroll the reader is already doing. 302 * 303 * Setting `scrollLeft` on `this.track` touches only that element. 304 * There is no `block` option and no ancestor involved, so this is 305 * structurally incapable of moving the page vertically — regardless 306 * of whether the track happens to be on-screen at the moment. 244 307 * 245 308 * @param {HTMLElement} linkEl 246 309 */ 247 310 Base.prototype.scrollItemIntoView = function ( linkEl ) { 311 if ( ! this.track ) { 312 return; 313 } 314 315 var max = this.track.scrollWidth - this.track.clientWidth; 316 317 if ( max <= 0 ) { 318 return; 319 } 320 248 321 var align = this.getResponsiveValue( this.config.align ) || 'nearest'; 322 var viewportWidth = this.track.clientWidth; 323 var edges = this.getContentEdges( linkEl, max ); 324 var current = this.getNormalizedScrollLeft( this.track, max ); 325 var target; 326 327 if ( 'start' === align ) { 328 target = edges.start; 329 } else if ( 'end' === align ) { 330 target = edges.end - viewportWidth; 331 } else if ( 'center' === align ) { 332 target = ( edges.start + edges.end ) / 2 - ( viewportWidth / 2 ); 333 } else if ( edges.start < current ) { 334 // 'nearest': item's start edge has scrolled past the 335 // viewport's start edge — pull it back into view there. 336 target = edges.start; 337 } else if ( edges.end > current + viewportWidth ) { 338 // Item's end edge is past the viewport's end edge. 339 target = edges.end - viewportWidth; 340 } else { 341 // Already fully visible; 'nearest' means "do nothing". 342 return; 343 } 344 345 target = Math.max( 0, Math.min( max, target ) ); 346 249 347 var reducedMotion = !! ( window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches ); 250 251 try { 252 linkEl.scrollIntoView( { 253 behavior: reducedMotion ? 'auto' : 'smooth', 254 block: 'nearest', 255 inline: align 256 } ); 257 } catch ( e ) { 258 linkEl.scrollIntoView(); 348 var track = this.track; 349 var previousBehavior = track.style.scrollBehavior; 350 351 // The stylesheet sets `scroll-behavior: auto` on the track so 352 // drag/wheel scrolling stays instant; switching to `smooth` only 353 // for this JS-driven move, then reverting once it's had time to 354 // finish, keeps that native drag/wheel feel intact. 355 track.style.scrollBehavior = reducedMotion ? 'auto' : 'smooth'; 356 this.setNormalizedScrollLeft( track, max, target ); 357 358 if ( ! reducedMotion ) { 359 setTimeout( function () { 360 track.style.scrollBehavior = previousBehavior; 361 }, 500 ); 362 } else { 363 track.style.scrollBehavior = previousBehavior; 259 364 } 260 365 }; … … 429 534 } 430 535 536 // While a programmatic scroll to a target is in flight, the 537 // scroll-spy observer below still fires as the viewport passes 538 // other sections along the way — that's fine for updating which 539 // link shows as active, but setActive() must not also snap the 540 // horizontal track to whichever link that is. This widget's own 541 // bar commonly scrolls out of the vertical viewport during a 542 // scroll to a distant target, and scrollIntoView({block:'nearest'}) 543 // on a now off-screen link pulls the page back to reveal the bar, 544 // directly fighting the scroll this function just started — which 545 // is exactly why clicking any link could end up settling back near 546 // wherever the bar itself lives instead of the intended target. 547 this._isProgrammaticScroll = true; 548 431 549 var lastTarget = computeTarget(); 432 550 … … 437 555 438 556 if ( reducedMotion ) { 557 setTimeout( function () { 558 self._isProgrammaticScroll = false; 559 }, 100 ); 439 560 return; 440 561 } … … 455 576 if ( attempts < maxAttempts ) { 456 577 setTimeout( correct, 120 ); 578 } else { 579 // A little extra margin past the last correction for the 580 // browser's own smooth-scroll animation to fully settle. 581 setTimeout( function () { 582 self._isProgrammaticScroll = false; 583 }, 200 ); 457 584 } 458 585 }() ); … … 477 604 /** 478 605 * Normalizes `scrollLeft` in RTL to a value that always runs from 0 479 * (reading-start) to `max` (reading-end), regardless of which of the 480 * three historical browser conventions for RTL `scrollLeft` is in 481 * effect (0 -> -max, 0 -> +max, or +max -> 0). The convention is 482 * detected at read-time from the sign of the live value rather than by 483 * sniffing the browser, which keeps this correct across engines and 484 * future browser versions. 606 * (reading-start) to `max` (reading-end), regardless of which RTL 607 * `scrollLeft` convention this engine uses (see 608 * `rtlUsesNegativeScrollLeft()`). 485 609 * 486 610 * @param {HTMLElement} el … … 489 613 */ 490 614 Base.prototype.getNormalizedScrollLeft = function ( el, max ) { 491 var scrollLeft = el.scrollLeft;492 493 615 if ( ! this.isRTL ) { 494 return scrollLeft; 495 } 496 497 return scrollLeft > 0 ? ( max - scrollLeft ) : ( -scrollLeft ); 616 return el.scrollLeft; 617 } 618 619 return rtlUsesNegativeScrollLeft() ? ( -el.scrollLeft ) : ( max - el.scrollLeft ); 620 }; 621 622 /** 623 * The write-side inverse of `getNormalizedScrollLeft()`: takes a 624 * normalized value (0 = reading-start, `max` = reading-end) and 625 * assigns the correctly-converted raw `scrollLeft` for whichever RTL 626 * convention this engine uses. 627 * 628 * @param {HTMLElement} el 629 * @param {number} max 630 * @param {number} value 631 */ 632 Base.prototype.setNormalizedScrollLeft = function ( el, max, value ) { 633 if ( ! this.isRTL ) { 634 el.scrollLeft = value; 635 return; 636 } 637 638 el.scrollLeft = rtlUsesNegativeScrollLeft() ? ( -value ) : ( max - value ); 639 }; 640 641 /** 642 * Computes `linkEl`'s horizontal position as two scroll-invariant 643 * offsets — `start` and `end` — expressed in the same 0-to-`max` 644 * normalized coordinate space as `getNormalizedScrollLeft()` 645 * (distance along reading order from the track's reading-start edge). 646 * Being scroll-invariant is what makes these directly comparable 647 * against a current or target normalized `scrollLeft`: the values 648 * don't change no matter what the track's scroll position was at the 649 * moment this is called. 650 * 651 * Built from `getBoundingClientRect()` (physical viewport pixels, 652 * always left-to-right regardless of `dir`) plus the track's current 653 * normalized scroll offset, rather than `offsetLeft`, so this doesn't 654 * depend on assumptions about which ancestor happens to be the 655 * element's `offsetParent`. 656 * 657 * @param {HTMLElement} linkEl 658 * @param {number} max 659 * @return {{start: number, end: number}} 660 */ 661 Base.prototype.getContentEdges = function ( linkEl, max ) { 662 var trackRect = this.track.getBoundingClientRect(); 663 var linkRect = linkEl.getBoundingClientRect(); 664 var current = this.getNormalizedScrollLeft( this.track, max ); 665 var physLeft = linkRect.left - trackRect.left; 666 var physRight = linkRect.right - trackRect.left; 667 668 if ( ! this.isRTL ) { 669 return { start: physLeft + current, end: physRight + current }; 670 } 671 672 var viewportWidth = this.track.clientWidth; 673 674 // Reading order runs right-to-left, so the link's reading-order 675 // "start" edge is its physical *right* edge, and its "end" edge 676 // is its physical *left* edge. 677 return { 678 start: viewportWidth + current - physRight, 679 end: viewportWidth + current - physLeft 680 }; 498 681 }; 499 682 … … 641 824 642 825 /** 643 * Elementor's own frontend script can, depending on the site's 644 * asset loading configuration (e.g. deferred/module script 645 * loading), finish initializing either before or after this file 646 * executes — the order scripts appear in the HTML does not 647 * guarantee execution order once defer/module scripts are 648 * involved. Guard both directions: register immediately if 649 * `elementorFrontend` is already ready, otherwise wait for the 650 * `elementor/frontend/init` event that Elementor fires exactly 651 * once, globally, the moment it finishes initializing — this is 652 * the documented, timing-safe hook point for third-party scripts. 826 * Catch-all for every matching element Elementor's own lifecycle will 827 * never reach — chiefly this widget's markup rendered by a Gutenberg 828 * block, but really anything not inside Elementor-managed content. 829 * This runs unconditionally, even on a site where Elementor happens 830 * to be active for OTHER content on the same page (its frontend 831 * script commonly loads site-wide), since that alone doesn't mean 832 * *this* element is one of its widgets. 833 * 834 * Elementor-rendered instances are deliberately skipped here (even 835 * if Elementor hasn't reached them yet) so each is only ever 836 * initialized once, via the frontend/element_ready hook above — 837 * which, unlike this one-time scan, also fires on every live-preview 838 * re-render inside the Elementor editor. 653 839 */ 840 function scanForUninitialized() { 841 var nodes = document.querySelectorAll( rootSelector ); 842 Array.prototype.forEach.call( nodes, function ( el ) { 843 if ( el[ instanceProp ] || el.closest( '.elementor' ) ) { 844 return; 845 } 846 initWidget( el ); 847 } ); 848 } 849 654 850 if ( window.elementorFrontend && window.elementorFrontend.hooks ) { 655 851 registerElementReadyHook(); 656 852 } else if ( window.jQuery ) { 657 853 window.jQuery( window ).on( 'elementor/frontend/init', registerElementReadyHook ); 854 } 855 856 if ( 'loading' === document.readyState ) { 857 document.addEventListener( 'DOMContentLoaded', scanForUninitialized ); 658 858 } else { 659 document.addEventListener( 'DOMContentLoaded', function () { 660 if ( window.elementorFrontend && window.elementorFrontend.hooks ) { 661 registerElementReadyHook(); 662 } 663 } ); 859 scanForUninitialized(); 664 860 } 665 861 }; -
navrail-toc-for-elementor/trunk/navrail-toc-for-elementor.php
r3671253 r3679318 2 2 /** 3 3 * Plugin Name: Navrail – Horizontal Navigation & Table of Contents 4 * Description: Horizontal, auto-scrolling navigation bars — built automatically from headings (Navrail TOC) or manually, item by item (Navrail Navigation). Currently available as an Elementor widget.5 * Version: 2.0.04 * Description: Horizontal, auto-scrolling navigation bars — built automatically from headings (Navrail TOC) or manually, item by item (Navrail Navigation). Available as Elementor widgets and Gutenberg blocks. 5 * Version: 3.0.0 6 6 * Requires at least: 6.3 7 7 * Requires PHP: 7.4 … … 20 20 } 21 21 22 define( 'NAVRAIL_VERSION', ' 2.0.0' );22 define( 'NAVRAIL_VERSION', '3.0.0' ); 23 23 define( 'NAVRAIL_PLUGIN_FILE', __FILE__ ); 24 24 define( 'NAVRAIL_PATH', plugin_dir_path( __FILE__ ) ); … … 28 28 * Main plugin bootstrap class. 29 29 * 30 * Kept as a single, small bootstrap class on purpose: this plugin registers 31 * exactly one widget, so a larger loader/service-container architecture would 32 * add indirection without adding value. 30 * Registers, depending on what's active: Gutenberg blocks (always, since 31 * the block editor is WordPress core) and Elementor widgets (only when 32 * Elementor is installed and active). Both integrations share the same 33 * underlying JS/CSS — see register_scripts()/register_styles(). 33 34 */ 34 35 final class NAVRAIL_Plugin { … … 73 74 } 74 75 76 // Scripts, styles, and Gutenberg blocks are registered regardless of 77 // Elementor: the block editor is a WordPress core feature, and the 78 // same shared assets now serve both integrations (see 79 // register_scripts() for how the two are told apart at runtime). 80 add_action( 'init', array( $this, 'register_scripts' ) ); 81 add_action( 'init', array( $this, 'register_styles' ) ); 82 add_action( 'init', array( $this, 'register_blocks' ) ); 83 75 84 if ( ! did_action( 'elementor/loaded' ) ) { 76 85 add_action( 'admin_notices', array( $this, 'admin_notice_missing_elementor' ) ); … … 85 94 add_action( 'elementor/widgets/register', array( $this, 'register_widgets' ) ); 86 95 add_action( 'elementor/elements/categories_registered', array( $this, 'register_category' ) ); 87 add_action( 'elementor/frontend/before_register_scripts', array( $this, 'register_scripts' ) ); 88 add_action( 'elementor/frontend/before_register_styles', array( $this, 'register_styles' ) ); 89 } 90 91 /** 92 * Registers script/style handles with Elementor's asset manager without 93 * enqueueing them. Elementor only prints/enqueues these when a widget 94 * whose get_script_depends()/get_style_depends() references them is 95 * actually present on the rendered page (frontend or editor preview). 96 } 97 98 /** 99 * Per-file cache-busting version: the file's own `filemtime()` when 100 * it can be read, falling back to `NAVRAIL_VERSION` (e.g. the file 101 * is missing, or the filesystem call is unavailable/restricted). 102 * Using the real modification time — rather than one shared plugin 103 * version for every asset — means a browser is guaranteed to fetch 104 * the new copy of exactly the file that changed the moment it 105 * changes, with no reliance on remembering to bump a version string. 106 * 107 * @param string $relative_path Path relative to the plugin root, e.g. 'assets/js/navrail-core.js'. 108 * @return string 109 */ 110 private function get_asset_version( $relative_path ) { 111 $absolute_path = NAVRAIL_PATH . $relative_path; 112 $mtime = @filemtime( $absolute_path ); 113 114 return $mtime ? (string) $mtime : NAVRAIL_VERSION; 115 } 116 117 /** 118 * Registers script handles on `init` — early enough for the Gutenberg 119 * blocks' block.json `viewScript`/`style` references (bare handle 120 * names, not `file:` paths) to resolve correctly, and equally usable by 121 * Elementor's widgets via get_script_depends(), which doesn't care when 122 * or how a handle was registered, only that it exists by print time. 96 123 */ 97 124 public function register_scripts() { 125 $elementor_active = (bool) did_action( 'elementor/loaded' ); 126 127 $core_deps = array( 'jquery' ); 128 if ( $elementor_active ) { 129 $core_deps[] = 'elementor-frontend'; 130 } 131 98 132 wp_register_script( 99 133 'navrail-core', 100 134 NAVRAIL_URL . 'assets/js/navrail-core.js', 101 array( 'jquery', 'elementor-frontend' ),102 NAVRAIL_VERSION,135 $core_deps, 136 $this->get_asset_version( 'assets/js/navrail-core.js' ), 103 137 true 104 138 ); … … 108 142 NAVRAIL_URL . 'assets/js/navrail-toc.js', 109 143 array( 'navrail-core' ), 110 NAVRAIL_VERSION,144 $this->get_asset_version( 'assets/js/navrail-toc.js' ), 111 145 true 112 146 ); … … 116 150 NAVRAIL_URL . 'assets/js/navrail-nav.js', 117 151 array( 'navrail-core' ), 118 NAVRAIL_VERSION,152 $this->get_asset_version( 'assets/js/navrail-nav.js' ), 119 153 true 120 154 ); … … 134 168 NAVRAIL_URL . 'assets/css/navrail-toc.css', 135 169 array(), 136 NAVRAIL_VERSION170 $this->get_asset_version( 'assets/css/navrail-toc.css' ) 137 171 ); 138 172 … … 141 175 NAVRAIL_URL . 'assets/css/navrail-nav.css', 142 176 array(), 143 NAVRAIL_VERSION 144 ); 177 $this->get_asset_version( 'assets/css/navrail-nav.css' ) 178 ); 179 } 180 181 /** 182 * Registers the Gutenberg blocks from their compiled build/ directories. 183 * Each block.json's `viewScript`/`style` fields reference the same 184 * handles registered in register_scripts()/register_styles() above by 185 * name, so the exact same frontend JS/CSS shipped for the Elementor 186 * widgets also runs these blocks — nothing is duplicated. 187 */ 188 public function register_blocks() { 189 foreach ( array( 'toc', 'navigation', 'navigation-item' ) as $block ) { 190 $path = NAVRAIL_PATH . 'build/' . $block; 191 192 if ( file_exists( $path . '/block.json' ) ) { 193 register_block_type( $path ); 194 } 195 } 145 196 } 146 197 -
navrail-toc-for-elementor/trunk/readme.txt
r3671253 r3679318 1 1 === Navrail – Horizontal Navigation & Table of Contents === 2 2 Contributors: hadiomid 3 Tags: elementor, table of contents, toc, navigation3 Tags: elementor, gutenberg, table of contents, toc, navigation 4 4 Requires at least: 6.3 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 2.0.07 Stable tag: 3.0.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 Create smooth horizontal navigation from your headings, or build custom navigation with icons and images. Made for Elementor.11 Turn your headings into a smooth horizontal navigation bar automatically, or build one by hand. Works with Elementor or Gutenberg. 12 12 13 13 == Description == 14 14 15 Navrail adds two widgets for horizontal, auto-scrolling navigation bars, the same pattern used by modern landing pages, documentation sites, and long-form pages to help visitors jump straight to the section they care about. Currently available as Elementor widgets.15 Navrail adds a horizontal, auto-scrolling navigation bar to your page. This is the same style of navigation used on many landing pages, documentation sites, and long articles, so visitors can jump straight to the part they want. It works with Elementor and with the WordPress block editor (Gutenberg). Elementor is optional. 16 16 17 **Navrail TOC** builds the navigation for you. Pick which heading levels to track, and it detects your sections automatically, smooth-scrolls to them on click, highlights the active one as visitors scroll, and keeps it in view. No manual list-building.17 **Navrail TOC** builds the navigation for you. Choose which heading levels to track (H2, H3, and so on), and it finds your sections automatically. It smooth-scrolls to a section when clicked, and highlights the current section as visitors scroll. No manual list-building needed. 18 18 19 **Navrail Navigation** builds the navigation from a list you configure by hand: one item at a time, each with its own title, optional icon or image, and a link, whether to a section on the page (with the same smooth scroll and active-item tracking as Navrail TOC) or to any normal URL, `tel:`, or `mailto:` link.19 **Navrail Navigation** lets you build the navigation by hand. Add items one by one, each with a title, an optional icon or image, and a link. A link can point to a section on the same page (with the same smooth scroll and highlighting as Navrail TOC), or to any normal URL, phone number, or email address. 20 20 21 Either widget works wellfor:21 Good for: 22 22 23 * Landing page sectionnavigation24 * Long -formsales pages23 * Landing page navigation 24 * Long sales pages 25 25 * Documentation 26 26 * Service and product pages 27 27 * One-page websites 28 * Long articles built with Elementor28 * Long articles 29 29 30 **Navrail TOC **30 **Navrail TOC features** 31 31 32 * Works with any Elementor page structure: headings, sections, anything with a heading tag 33 * Include/Exclude selectors for precise control over which headings are picked up 34 * Optional item separators and hierarchical numbering (1, 1.1, 1.2, 2, ...) 35 * Optional leading text with Dynamic Tags support 36 * Optional title truncation, by width, character count, or word count 32 * Works with any page: headings, sections, anything with a heading tag 33 * Choose which parts of the page to include or skip 34 * Optional numbers before each item (1, 1.1, 1.2, 2, ...) 35 * Optional line separators between items 36 * Optional fixed label before the list 37 * Shortens long titles automatically (by width, character count, or word count) 38 * Optional "stick to top/bottom" behavior 37 39 38 **Navrail Navigation **40 **Navrail Navigation features** 39 41 40 * Manually-configured items via a repeater: title, optional icon or image,link41 * Icon/image position (before, after, above, or below the title), set independently per breakpoint42 * Min Height control to keep items a consistent heightwhen some have an icon/image and others don't43 * Independent Normal/Hover/Active styling for both the item and its title42 * Add items by hand: title, optional icon or image, and a link 43 * Place the icon/image before, after, above, or below the title 44 * Keep all items the same height, even when some have an icon/image and others don't 45 * Separate colors and styles for Normal, Hover, and Active states 44 46 45 ** Shared by both widgets**47 **Works the same in Elementor and the block editor** 46 48 47 * Smooth scroll with active-item highlighting 48 * Auto-scrolling, swipeable navigation bar with edge fade 49 * Adjustable width, alignment, and full style control: colors, borders, spacing, independent Normal/Hover/Active states 50 * Sticky positioning support 51 * RTL-ready 52 * Lightweight, no external libraries, loads only where the widget is used 49 * Smooth scroll with highlighting of the current item 50 * Auto-scrolling, swipeable bar with a soft fade at each edge 51 * Right-to-left (RTL) language support 52 * Lightweight: no external libraries, and only loads on pages where it's used 53 * Elementor also lets you set separate values for Desktop, Tablet, and Mobile; the block editor uses one value for all screen sizes 53 54 54 55 == Installation == … … 56 57 1. Upload the plugin ZIP via Plugins → Add New → Upload Plugin, or extract it into `wp-content/plugins/`. 57 58 2. Activate "Navrail". 58 3. Edit any page with Elementor and drag either the "Navrail TOC" widget (auto-detects headings) or the "Navrail Navigation" widget (manually-configured items) — both are in the "Navrail" category — onto the page. 59 3. **Elementor**: edit a page with Elementor and drag in the "Navrail TOC" widget (finds headings automatically) or the "Navrail Navigation" widget (build the list yourself). Both are in the "Navrail" category. 60 **Block editor**: open the block inserter and search "Navrail". Add the "Navrail TOC" block, or the "Navrail Navigation" block (add "Navigation Item" blocks inside it for each link). 59 61 60 62 == Frequently Asked Questions == 61 63 64 = Is Elementor required? = 65 66 No. The blocks work in the standard WordPress block editor with no other plugin installed. Elementor is only needed if you want to use the Elementor widgets instead. 67 62 68 = What's the difference between Navrail TOC and Navrail Navigation? = 63 69 64 Navrail TOC builds its list automatically from headings already on the page. Navrail Navigation is fully manual: you add each item, its label, an optional icon or image, and a link yourself, whether or not it points at this page.70 Navrail TOC builds its list automatically from the headings already on your page. Navrail Navigation is fully manual: you add each item, its label, an optional icon or image, and a link yourself. 65 71 66 = Is this only for Table of Contents? =72 = Is this only for a Table of Contents? = 67 73 68 No. It's a general -purpose horizontal section navigation. Table of Contents is one common use, but it works just as well for landing page section jumps, documentation, service/product page navigation, and long single-page sites.74 No. It's a general horizontal navigation bar. A Table of Contents is one common use, but it also works well for landing page navigation, documentation, product pages, and one-page sites. 69 75 70 76 = Is this plugin free? = 71 77 72 Yes, completely. Every feature described above is included, no premium version, no upsells, no ads. 73 74 = Does this require Elementor Pro? = 75 76 No. Only the free Elementor plugin is required. 78 Yes, completely free. No premium version, no upsells, no ads. 77 79 78 80 = Why don't I see any items? = 79 81 80 For Navrail TOC: the widget builds its navigation from headings already on the page. Make sure the selected Heading Levels match the headings actually used in your content. For Navrail Navigation: make sure the repeater has items added under Content → Navigation Items.82 For Navrail TOC: it builds its list from headings already on the page, so check that the Heading Levels you selected match the headings you actually used. For Navrail Navigation: make sure you've added items (the repeater panel in Elementor, or "Navigation Item" child blocks in the block editor). 81 83 82 This plugin is an independent, third-party extension for Elementor and is not affiliated with or endorsed by Elementor.com.84 This plugin is an independent, third-party extension and is not affiliated with or endorsed by Elementor.com or the WordPress Gutenberg project. 83 85 84 86 == Screenshots == … … 87 89 2. Frontend display of the horizontal TOC navigation bar 88 90 3. Frontend display of a manually-configured Navrail Navigation bar 91 4. Style settings panel in the block editor (Gutenberg) 89 92 90 93 == Changelog == 94 95 = 3.0.0 = 96 * Added: full support for the WordPress block editor (Gutenberg) — "Navrail TOC" and "Navrail Navigation" blocks, with the same features as the Elementor widgets: numbering, separators, a leading label, title shortening, sticky positioning, and separate Normal/Hover/Active styling. The block editor's leading label is plain text; Elementor's version also supports Dynamic Tags. 97 * Changed: renamed the plugin from "Navrail TOC for Elementor" to "Navrail – Horizontal Navigation & Table of Contents", since it no longer needs Elementor. The plugin's link on WordPress.org stays the same. 98 * Changed: Elementor is now optional. The plugin and its blocks work with no other plugin installed; the Elementor widgets simply stay inactive (with a friendly notice) until Elementor is installed. 91 99 92 100 = 2.0.0 =
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)