|
414 | 414 | */ |
415 | 415 | $menu.toggleClass("c-menu-top", tocHidden === 1); |
416 | 416 |
|
417 | | - // Prefer data attributes (if you added them), fallback to legacy classes. |
| 417 | + // Prefer data attributes, fallback to legacy classes. |
418 | 418 | var $circle = $menu.find("[data-lp-menu='circle']").first(); |
419 | 419 | if (!$circle.length) { |
420 | 420 | $circle = $menu.find(".circle").first(); |
|
529 | 529 | } |
530 | 530 | } |
531 | 531 |
|
| 532 | + /* ----------------------------- |
| 533 | + * Iframe helpers |
| 534 | + * ----------------------------- */ |
| 535 | + function getLpIframe() { |
| 536 | + var $i = $("#content_id"); |
| 537 | + if (!$i.length) { |
| 538 | + $i = $("#content_id_blank"); |
| 539 | + } |
| 540 | + return $i; |
| 541 | + } |
| 542 | +
|
| 543 | + function syncCurrentItemFromIframe() { |
| 544 | + try { |
| 545 | + var $i = getLpIframe(); |
| 546 | + if (!$i.length) { |
| 547 | + return; |
| 548 | + } |
| 549 | + var src = $i.attr("src") || ""; |
| 550 | + var m = src.match(/[?&]item_id=(\d+)/); |
| 551 | + if (m && m[1]) { |
| 552 | + window.lpCurrentItemId = parseInt(m[1], 10) || window.lpCurrentItemId; |
| 553 | + } |
| 554 | + } catch (e) {} |
| 555 | + } |
| 556 | +
|
532 | 557 | /* ----------------------------------------------------------- |
533 | | - * When TOC is hidden, navigation must NOT rely on |
534 | | - * TOC DOM ordering. Use backend/runtime directions ('next'/'previous') |
535 | | - * to guarantee correct sequencing. |
| 558 | + * TOC-based navigation helpers |
536 | 559 | * ----------------------------------------------------------- */ |
| 560 | + function collectTocItemIds() { |
| 561 | + var ids = []; |
| 562 | + var seen = {}; |
| 563 | +
|
| 564 | + $("#toc_id [id^='toc_']").each(function () { |
| 565 | + var raw = (this.id || "").replace("toc_", ""); |
| 566 | + var n = parseInt(raw, 10); |
| 567 | + if (!Number.isFinite(n) || n <= 0) return; |
| 568 | + if (seen[n]) return; |
| 569 | + seen[n] = true; |
| 570 | + ids.push(n); |
| 571 | + }); |
| 572 | +
|
| 573 | + if (!ids.length) { |
| 574 | + $("#toc_id a[href*='item_id=']").each(function () { |
| 575 | + try { |
| 576 | + var href = $(this).attr("href") || ""; |
| 577 | + var m = href.match(/[?&]item_id=(\d+)/); |
| 578 | + if (!m) return; |
| 579 | + var n = parseInt(m[1], 10); |
| 580 | + if (!Number.isFinite(n) || n <= 0) return; |
| 581 | + if (seen[n]) return; |
| 582 | + seen[n] = true; |
| 583 | + ids.push(n); |
| 584 | + } catch (e) {} |
| 585 | + }); |
| 586 | + } |
| 587 | +
|
| 588 | + return ids; |
| 589 | + } |
| 590 | +
|
537 | 591 | function getCurrentItemIdSafe() { |
538 | | - // Prefer runtime value (always the most reliable after switches) |
| 592 | + // Prefer runtime value (most reliable when available) |
539 | 593 | try { |
540 | 594 | if (window.olms && typeof olms.lms_item_id !== "undefined") { |
541 | 595 | var n = parseInt(olms.lms_item_id, 10); |
|
550 | 604 | return Number.isFinite(fromUrl) ? fromUrl : 0; |
551 | 605 | } |
552 | 606 |
|
| 607 | + function resolveDirectionToId(direction, currentId) { |
| 608 | + var ids = collectTocItemIds(); |
| 609 | + if (!ids.length) { |
| 610 | + return 0; |
| 611 | + } |
| 612 | +
|
| 613 | + var idx = ids.indexOf(currentId); |
| 614 | + if (idx === -1) { |
| 615 | + return direction === "next" ? (ids[0] || 0) : 0; |
| 616 | + } |
| 617 | +
|
| 618 | + if (direction === "next") { |
| 619 | + return ids[idx + 1] || 0; |
| 620 | + } |
| 621 | + if (direction === "previous") { |
| 622 | + return ids[idx - 1] || 0; |
| 623 | + } |
| 624 | + return 0; |
| 625 | + } |
| 626 | +
|
553 | 627 | function buildContentUrlForItem(itemId) { |
554 | 628 | var cid = getSearchParam("cid") || ""; |
555 | 629 | var sid = getSearchParam("sid") || "0"; |
|
565 | 639 |
|
566 | 640 | // Iframe loading overlay (safe: does not cancel navigation) |
567 | 641 | var $loader = $("#lp-iframe-loader"); |
568 | | - var $iframe = $("#content_id"); |
569 | | - if (!$iframe.length) { |
570 | | - $iframe = $("#content_id_blank"); |
571 | | - } |
| 642 | + var $iframe = getLpIframe(); |
572 | 643 |
|
573 | 644 | var loaderTimer = null; |
574 | 645 | var maxHideTimer = null; |
|
595 | 666 | if ($iframe.length) { |
596 | 667 | showLoader("initial"); |
597 | 668 | $iframe.off("load.lpLoader").on("load.lpLoader", function () { |
| 669 | + syncCurrentItemFromIframe(); |
598 | 670 | hideLoader("iframe load"); |
599 | 671 | }); |
600 | 672 | } else { |
|
643 | 715 | var isDirection = (target === "next" || target === "previous" || target === "first" || target === "last"); |
644 | 716 | var targetItemId = 0; |
645 | 717 |
|
| 718 | + showLoader("navigate " + label); |
| 719 | + var currentItemId = getCurrentItemIdSafe(); |
| 720 | +
|
| 721 | + // If target is numeric, validate it. |
646 | 722 | if (!isDirection) { |
647 | 723 | targetItemId = parseInt(target, 10); |
648 | 724 | if (!Number.isFinite(targetItemId) || targetItemId <= 0) { |
649 | 725 | console.warn("LP NAV: invalid target item id for " + label + ":", target); |
| 726 | + hideLoader("nav failed"); |
650 | 727 | return false; |
651 | 728 | } |
652 | 729 | } |
653 | 730 |
|
654 | | - showLoader("navigate " + label); |
655 | | -
|
656 | | - var currentItemId = getCurrentItemIdSafe(); |
| 731 | + // If direction, first resolve it from TOC order (best for Chamilo LP). |
| 732 | + if (isDirection && (target === "next" || target === "previous")) { |
| 733 | + var tocResolved = resolveDirectionToId(target, currentItemId); |
| 734 | + if (tocResolved > 0 && tocResolved !== currentItemId) { |
| 735 | + targetItemId = tocResolved; |
| 736 | + } |
| 737 | + } |
657 | 738 |
|
658 | | - // Use switch_item if the runtime is ready (preferred, preserves SCORM state) |
| 739 | + // Prefer switch_item if runtime is ready (keeps SCORM/LP state). |
659 | 740 | if (window.olms && typeof switch_item === "function" && Number(olms.lms_initialized) === 1) { |
660 | 741 | console.info("LP NAV: switching item via runtime", { |
661 | 742 | currentItemId: currentItemId, |
662 | | - target: isDirection ? target : targetItemId |
| 743 | + target: isDirection ? (targetItemId > 0 ? targetItemId : target) : targetItemId |
663 | 744 | }); |
664 | 745 |
|
665 | 746 | try { |
666 | | - // NOTE: when target is 'next'/'previous', the backend computes the real next/prev. |
667 | | - switch_item(currentItemId || 0, isDirection ? target : targetItemId); |
668 | | -
|
669 | | - // Keep window var in sync only when we know the numeric target. |
670 | | - if (!isDirection) { |
| 747 | + // If we resolved a numeric target, pass numeric. Otherwise pass direction. |
| 748 | + switch_item(currentItemId || 0, isDirection ? (targetItemId > 0 ? targetItemId : target) : targetItemId); |
| 749 | + if (!isDirection || targetItemId > 0) { |
671 | 750 | window.lpCurrentItemId = targetItemId; |
672 | 751 | } |
673 | 752 | return true; |
674 | 753 | } catch (e) { |
675 | | - console.warn("LP NAV: switch_item() failed, fallback to iframe src update.", e); |
| 754 | + console.warn("LP NAV: switch_item() failed, falling back to iframe src update.", e); |
676 | 755 | } |
677 | 756 | } |
678 | 757 |
|
679 | | - // update iframe src (works, but may not update full LMS runtime state) |
680 | | - if ($iframe && $iframe.length) { |
681 | | - // If direction was requested, try to map it to a numeric id from runtime |
682 | | - if (isDirection && window.olms) { |
683 | | - var fallbackId = 0; |
684 | | - try { |
685 | | - if (target === "next") fallbackId = parseInt(olms.lms_next_item, 10); |
686 | | - if (target === "previous") fallbackId = parseInt(olms.lms_previous_item, 10); |
687 | | - } catch (e) {} |
688 | | -
|
689 | | - if (Number.isFinite(fallbackId) && fallbackId > 0 && fallbackId !== currentItemId) { |
690 | | - targetItemId = fallbackId; |
691 | | - } else { |
692 | | - console.warn("LP NAV: cannot resolve numeric item for direction fallback:", target, fallbackId); |
693 | | - hideLoader("nav failed"); |
694 | | - return false; |
695 | | - } |
696 | | - } |
| 758 | + // Fallback: resolve direction from runtime vars only if TOC resolution failed. |
| 759 | + if (isDirection && targetItemId <= 0 && window.olms) { |
| 760 | + try { |
| 761 | + if (target === "next") targetItemId = parseInt(olms.lms_next_item, 10) || 0; |
| 762 | + if (target === "previous") targetItemId = parseInt(olms.lms_previous_item, 10) || 0; |
| 763 | + } catch (e) {} |
| 764 | + } |
697 | 765 |
|
698 | | - if (targetItemId > 0) { |
| 766 | + // Final fallback: update iframe src directly. |
| 767 | + if (targetItemId > 0) { |
| 768 | + var $i = getLpIframe(); |
| 769 | + if ($i.length) { |
699 | 770 | var src = buildContentUrlForItem(targetItemId); |
700 | 771 | console.info("LP NAV: updating iframe src (fallback).", src); |
701 | | - $iframe.attr("src", src); |
| 772 | + $i.attr("src", src); |
702 | 773 | window.lpCurrentItemId = targetItemId; |
703 | 774 | return true; |
704 | 775 | } |
|
708 | 779 | return false; |
709 | 780 | } |
710 | 781 |
|
| 782 | + /* ----------------------------------------------------------- |
| 783 | + * Deterministic bindings for Next/Previous (Chamilo LP safe) |
| 784 | + * ----------------------------------------------------------- */ |
| 785 | + function bindPrimaryNavButtons() { |
| 786 | + var $prev = $("#scorm-previous"); |
| 787 | + var $next = $("#scorm-next"); |
| 788 | +
|
| 789 | + function bind($el, direction, label) { |
| 790 | + if (!$el || !$el.length) return; |
| 791 | +
|
| 792 | + // Tag for easier debugging |
| 793 | + $el.attr("data-lp-nav", direction); |
| 794 | +
|
| 795 | + // Remove legacy inline handlers to prevent NaN or dead navigation. |
| 796 | + // We still keep behavior via lpNavigateToItem(). |
| 797 | + $el.removeAttr("onclick").removeAttr("target"); |
| 798 | +
|
| 799 | + if ($el.is("a")) { |
| 800 | + $el.attr("href", "#"); |
| 801 | + } |
| 802 | +
|
| 803 | + $el.off("click.lpNavPrimary").on("click.lpNavPrimary", function (e) { |
| 804 | + e.preventDefault(); |
| 805 | + e.stopPropagation(); |
| 806 | + console.info("LP NAV: " + label + " clicked -> navigating", direction); |
| 807 | + lpNavigateToItem(direction, label); |
| 808 | + return false; |
| 809 | + }); |
| 810 | + } |
| 811 | +
|
| 812 | + if (!$prev.length || !$next.length) { |
| 813 | + console.warn("LP NAV: primary #scorm-previous/#scorm-next elements not found. Will rely on heuristic patching."); |
| 814 | + } |
| 815 | +
|
| 816 | + bind($prev, "previous", "previous"); |
| 817 | + bind($next, "next", "next"); |
| 818 | + } |
| 819 | +
|
711 | 820 | function looksLikePrev(el) { |
712 | 821 | var $el = $(el); |
713 | 822 | var t = ($el.attr("title") || "").toLowerCase(); |
|
742 | 851 | function patchNavArrowsWhenTocHidden() { |
743 | 852 | if (tocHidden !== 1) return; |
744 | 853 |
|
745 | | - // Prefer known IDs if present (most reliable on top menu) |
| 854 | + // Prefer known IDs if present (most reliable) |
746 | 855 | var $prevEl = $("#scorm-previous"); |
747 | 856 | var $nextEl = $("#scorm-next"); |
748 | 857 |
|
|
769 | 878 | return; |
770 | 879 | } |
771 | 880 |
|
772 | | - // Remove legacy inline handlers and targets |
773 | 881 | $el.removeAttr("onclick").removeAttr("target"); |
774 | | -
|
775 | 882 | if ($el.is("a")) { |
776 | 883 | $el.attr("href", "#"); |
777 | 884 | } |
|
847 | 954 | } |
848 | 955 | } |
849 | 956 |
|
| 957 | + // Bind primary next/prev always (safe for Chamilo LP + SCORM) |
| 958 | + bindPrimaryNavButtons(); |
| 959 | + setTimeout(bindPrimaryNavButtons, 200); |
| 960 | + setTimeout(bindPrimaryNavButtons, 600); |
850 | 961 | if (tocHidden === 1) { |
851 | 962 | // Run immediately + retries (menu markup sometimes arrives late) |
852 | 963 | patchNavArrowsWhenTocHiddenSafe(); |
|
0 commit comments