diff --git a/src/pat/autotoc/autotoc.js b/src/pat/autotoc/autotoc.js
index 968ae2773..30f06c957 100644
--- a/src/pat/autotoc/autotoc.js
+++ b/src/pat/autotoc/autotoc.js
@@ -1,6 +1,7 @@
import $ from "jquery";
import Base from "@patternslib/patternslib/src/core/base";
import utils from "@patternslib/patternslib/src/core/utils";
+import Tab from "bootstrap/js/dist/tab";
export default Base.extend({
name: "autotoc",
@@ -10,8 +11,9 @@ export default Base.extend({
section: "section",
levels: "h1,h2,h3",
IDPrefix: "autotoc-item-",
- classTOCName: "autotoc-nav",
- classSectionName: "autotoc-section",
+ classTOCName: "autotoc-nav nav",
+ classContentAreaName: "autotoc-content tab-content",
+ classSectionName: "autotoc-section tab-pane fade",
classLevelPrefixName: "autotoc-level-",
classActiveName: "active",
scrollDuration: "slow",
@@ -26,67 +28,108 @@ export default Base.extend({
var self = this;
- self.$toc = $("").addClass(self.options.classTOCName);
+ const $nav = $("")
+ .attr("aria-label", "Tab Navigation");
+
+ const $toc = $("
")
+ .addClass(self.options.classTOCName)
+ .appendTo($nav);
if (self.options.prependTo) {
- self.$toc.prependTo(self.options.prependTo);
+ $nav.prependTo(self.options.prependTo);
} else if (self.options.appendTo) {
- self.$toc.appendTo(self.options.appendTo);
+ $nav.appendTo(self.options.appendTo);
} else {
- self.$toc.prependTo(self.$el);
+ $nav.prependTo(self.$el);
}
if (self.options.className) {
self.$el.addClass(self.options.className);
}
- $(self.options.section, self.$el).addClass(self.options.classSectionName);
+ $(self.options.section, self.$el)
+ .addClass(self.options.classSectionName)
+ .attr("role", "tabpanel")
+ .attr("tabindex", "0");
var asTabs = self.$el.hasClass("autotabs");
+ if (asTabs) {
+ $toc.addClass("nav-tabs");
+ self.$contentArea = $("").addClass(self.options.classContentAreaName);
+ self.$contentArea.insertAfter($nav);
+ $(self.options.section, self.$el).appendTo(self.$contentArea);
+ $(self.options.section, self.$el).find("legend").hide();
+ } else {
+ $toc.addClass([
+ "flex-column",
+ "float-end",
+ "border",
+ "mt-0",
+ "me-0",
+ "mb-3",
+ "ms-3",
+ "py-2",
+ "px-0"
+ ]);
+ }
+
var activeId = null;
$(self.options.levels, self.$el).each(function (i) {
- const section = this.closest(self.options.section);
const $level = $(this);
- let id = $level.prop("id") ? $level.prop("id") : $(section).prop("id");
-
- if (!id || $("#" + id).length > 0) {
- id = self.options.IDPrefix + self.name + "-" + i;
- }
- if (window.location.hash === "#" + id) {
- activeId = id;
+ const $section = $level.closest(self.options.section);
+ let sectionId = $section.prop("id");
+ let sectionHash = `#${sectionId}`;
+ const tabId = `${self.options.IDPrefix}${self.name}-${i}`;
+ const tabHash = `#${tabId}`;
+ const levelId = `${tabId}-pane`;
+ const levelHash = `#${levelId}`;
+
+ if (!asTabs) {
+ $level.attr("id", levelId).attr("aria-labelledby", tabId);
+ } else {
+ $section.attr("aria-labelledby", tabId);
+ if (!sectionId) {
+ // sections without ID get auto generated "-pane"
+ sectionId = levelId;
+ sectionHash = `#${levelId}`;
+ $section.attr("id", levelId);
+ }
}
- if (activeId === null && $level.hasClass(self.options.classActiveName)) {
- activeId = id;
+
+ // NOTE: if you have nested autotocs then you have to add the
+ // parent autotoc tabId to `options.IDPrefix` of the sub autotoc
+ // in order to mark parent and sub tab as active.
+ if (activeId === null && (window.location.hash.indexOf(tabHash) == 0 || $level.hasClass(self.options.classActiveName))) {
+ activeId = tabId;
}
- $level.data("navref", id);
+
+ const $navItem = $("");
+ $navItem
+ .addClass("nav-item")
+ .attr("role", "presentation")
+ .appendTo($toc);
+
const $nav = $("");
- $nav.appendTo(self.$toc)
- .text($level.text())
- .attr("id", id)
- .attr("href", "#" + id)
- .addClass(self.options.classLevelPrefixName + self.getLevel($level))
+ $nav.appendTo($navItem)
+ .html($level.html())
+ .attr("id", tabId)
+ .attr("href", asTabs ? sectionHash : levelHash)
+ .attr("aria-controls", asTabs ? sectionId : levelId)
+ .attr("data-bs-toggle", "tab")
+ .attr("data-bs-target", asTabs ? sectionHash : levelHash)
+ .addClass([
+ "nav-link",
+ self.options.classLevelPrefixName + self.getLevel($level),
+ ])
.on("click", function (e, options) {
- e.stopPropagation();
- e.preventDefault();
if (!options) {
options = {
doScroll: true,
skipHash: false,
};
}
- var $el = $(this);
- self.$toc
- .children("." + self.options.classActiveName)
- .removeClass(self.options.classActiveName);
- self.$el
- .children("." + self.options.classActiveName)
- .removeClass(self.options.classActiveName);
- $(e.target).addClass(self.options.classActiveName);
- $level
- .parents(self.options.section)
- .addClass(self.options.classActiveName);
if (
options.doScroll !== false &&
self.options.scrollDuration &&
@@ -107,37 +150,36 @@ export default Base.extend({
$(this).trigger("clicked");
if (!options.skipHash) {
if (window.history && window.history.pushState) {
- window.history.pushState({}, "", "#" + $el.attr("id"));
+ window.history.pushState({}, "", tabHash);
}
}
});
- $level.data("autotoc-trigger-id", id);
+
+ if (!asTabs) {
+ $nav.addClass([
+ "text-decoration-underline",
+ "p-0",
+ "mx-3",
+ ]);
+ }
self.tabs.push({
- section: section,
- id: id,
+ section: $section[0],
+ id: levelId,
nav: $nav[0],
});
});
- if (activeId) {
- $("a#" + activeId).trigger("click", {
- doScroll: true,
- skipHash: true,
- });
- } else {
- self.$toc.find("a").first().trigger("click", {
- doScroll: false,
- skipHash: true,
- });
- }
+ const activeTabButton = activeId ? $toc.find("a#" + activeId)[0] : $toc.find("a").first()[0];
+ const tab = Tab.getOrCreateInstance(activeTabButton);
+ tab.show();
// After DOM tree is built, initialize eventual validation
- this.initialize_validation(self.$el);
+ this.initialize_validation();
},
- initialize_validation: function ($el) {
- const el = $el[0];
+ initialize_validation: function () {
+ const el = this.el
// Initialize only on forms
const form = el.closest("form");
diff --git a/src/pat/autotoc/autotoc.scss b/src/pat/autotoc/autotoc.scss
index 6129ed26c..4ff4cbc29 100644
--- a/src/pat/autotoc/autotoc.scss
+++ b/src/pat/autotoc/autotoc.scss
@@ -1,69 +1,10 @@
.pat-autotoc {
.autotoc-nav {
- float: right;
- border: 1px solid #ddd;
- padding: 0.5em 0;
- margin: 0 0 1em 1em;
- a {
- display: block;
- }
- a:focus {
- outline-style: none;
- }
- .autotoc-level-1 {
- margin: 0 1em 0 1em;
- }
.autotoc-level-2 {
- margin: 0 1em 0 2em;
+ margin-left: 2em;
}
.autotoc-level-3 {
- margin: 0 1em 0 3em;
- }
- }
- &.autotabs {
- .autotoc-nav {
- float: none;
- padding: 0;
- margin: 0 0 0.3em 0;
- border: 0;
- border-bottom: 1px solid #ddd;
- &:after {
- content: "";
- display: table;
- line-height: 0;
- }
- a {
- display: inline-block;
- margin: 0 0.5em -1px 0.5em;
- line-height: 1.5em;
- padding: 0.4em 0.8em;
- text-decoration: none;
- border-radius: 4px 4px 0 0;
- &.active {
- border: 1px solid #ddd;
- border-bottom-color: var(--bs-body-bg);
- color: var(--bs-secondary-text);
- cursor: default;
- }
- &.active:hover {
- background-color: transparent;
- }
- &:hover {
- // background-color: #eee;
- background-color: var(--bs-secondary-bg);
- border-color: #eee;
- border-bottom-color: var(--bs-body-bg);
- }
- }
- }
- .autotoc-section {
- display: none;
- &.active {
- display: block;
- legend {
- display: none;
- }
- }
+ margin-left: 3em;
}
}
}
diff --git a/src/pat/autotoc/autotoc.test.js b/src/pat/autotoc/autotoc.test.js
index fbfdb29bc..6397f8552 100644
--- a/src/pat/autotoc/autotoc.test.js
+++ b/src/pat/autotoc/autotoc.test.js
@@ -33,38 +33,47 @@ describe("1 - AutoTOC", function () {
document.body.innerHTML = "";
});
it("by default creates TOC from h1/h2/h3", async function () {
- expect(document.querySelectorAll("nav").length).toEqual(0);
+ expect(document.querySelectorAll(".nav").length).toEqual(0);
registry.scan(document.body);
await utils.timeout(1);
- expect(document.querySelectorAll("nav").length).toEqual(1);
- expect(document.querySelectorAll("nav > a").length).toEqual(9);
- expect(document.querySelectorAll("nav > a.autotoc-level-1").length).toEqual(4);
- expect(document.querySelectorAll("nav > a.autotoc-level-2").length).toEqual(4);
- expect(document.querySelectorAll("nav > a.autotoc-level-3").length).toEqual(1);
- expect(document.querySelectorAll("nav > a.autotoc-level-4").length).toEqual(0);
+ expect(document.querySelectorAll(".nav").length).toEqual(1);
+ expect(document.querySelectorAll(".nav > li > a").length).toEqual(9);
+ expect(
+ document.querySelectorAll(".nav > li > a.autotoc-level-1").length
+ ).toEqual(4);
+ expect(
+ document.querySelectorAll(".nav > li > a.autotoc-level-2").length
+ ).toEqual(4);
+ expect(
+ document.querySelectorAll(".nav > li > a.autotoc-level-3").length
+ ).toEqual(1);
+ expect(
+ document.querySelectorAll(".nav > li > a.autotoc-level-4").length
+ ).toEqual(0);
});
it("sets href and id", async () => {
registry.scan(document.body);
await utils.timeout(1);
- expect(document.querySelectorAll("nav > a")[0].getAttribute("id")).toEqual(
+ const firstTab = document.querySelectorAll(".nav > li > a")[0]
+ expect(firstTab.getAttribute("id")).toEqual(
"autotoc-item-autotoc-0"
);
- expect(document.querySelectorAll("nav > a")[0].getAttribute("href")).toEqual(
- "#autotoc-item-autotoc-0"
- );
+ expect(
+ firstTab.getAttribute("data-bs-target")
+ ).toEqual("#autotoc-item-autotoc-0-pane");
});
it("can have custom levels", async function () {
this.$el.attr("data-pat-autotoc", "levels: h1");
- expect($("> nav", this.$el).length).toEqual(0);
+ expect($("> .nav", this.$el).length).toEqual(0);
registry.scan(this.$el);
await utils.timeout(1);
expect($("> nav", this.$el).length).toEqual(1);
- expect($("> nav > a.autotoc-level-1", this.$el).length).toEqual(4);
- expect($("> nav > a.autotoc-level-2", this.$el).length).toEqual(0);
+ expect($("> nav > ul > li > a.autotoc-level-1", this.$el).length).toEqual(4);
+ expect($("> nav > ul > li > a.autotoc-level-2", this.$el).length).toEqual(0);
});
it("can be appended anywhere", async function () {
this.$el.attr("data-pat-autotoc", "levels: h1;appendTo:.placeholder");
@@ -78,21 +87,22 @@ describe("1 - AutoTOC", function () {
expect($("div.placeholder", this.$el).children().eq(0).attr("id")).toEqual(
"first-elem"
);
- expect($("div.placeholder", this.$el).children().eq(1).attr("class")).toEqual(
- "autotoc-nav"
- );
+ expect(
+ $("div.placeholder", this.$el).children().eq(1).find("ul").attr("class")
+ ).toContain("autotoc-nav nav flex-column");
+
});
it("can be prepended anywhere", async function () {
this.$el.attr("data-pat-autotoc", "levels: h1;prependTo:.placeholder");
- expect($("> nav", this.$el).length).toEqual(0);
- expect($("div.placeholder > nav", this.$el).length).toEqual(0);
+ expect($("> .nav", this.$el).length).toEqual(0);
+ expect($("div.placeholder > .nav", this.$el).length).toEqual(0);
registry.scan(this.$el);
await utils.timeout(1);
- expect($("> nav", this.$el).length).toEqual(0);
+ expect($("> .nav", this.$el).length).toEqual(0);
expect($("div.placeholder > nav", this.$el).length).toEqual(1);
- expect($("div.placeholder", this.$el).children().eq(0).attr("class")).toEqual(
- "autotoc-nav"
+ expect($("div.placeholder", this.$el).children().eq(0).find("ul").attr("class")).toContain(
+ "autotoc-nav nav flex-column"
);
expect($("div.placeholder", this.$el).children().eq(1).attr("id")).toEqual(
"first-elem"
@@ -102,7 +112,7 @@ describe("1 - AutoTOC", function () {
registry.scan(this.$el);
await utils.timeout(1);
- expect($("> nav > a.active", this.$el).text()).toEqual("Title 1");
+ expect($("> nav > ul > li > a.active", this.$el).text()).toEqual("Title 1");
});
it("the first element with `classActiveName` will be the active", async function () {
$("h1:eq(1)", this.$el).addClass("active");
@@ -111,7 +121,7 @@ describe("1 - AutoTOC", function () {
registry.scan(this.$el);
await utils.timeout(1);
- expect($("> nav > a.active", this.$el).text()).toEqual("Title 2");
+ expect($("> nav > ul > li > a.active", this.$el).text()).toEqual("Title 2");
});
it("custom className", async function () {
this.$el.attr("data-pat-autotoc", "className:SOMETHING");
diff --git a/src/pat/controlpanels/schemaeditor--implementation.js b/src/pat/controlpanels/schemaeditor--implementation.js
index 6de6eefdb..0857d5029 100644
--- a/src/pat/controlpanels/schemaeditor--implementation.js
+++ b/src/pat/controlpanels/schemaeditor--implementation.js
@@ -8,7 +8,7 @@ export default class SchemaEditor {
init() {
if ($("#form fieldset", this.el).length >= 2) {
// If multiple fieldsets, release after autotoc has been initialized
- if ($("#form .autotoc-nav > a", this.el).length) {
+ if ($("#form .autotoc-nav a", this.el).length) {
// pat-autotoc already initialized, script probably run after
// mockup initialization.
this.init_schemaeditor();
@@ -107,7 +107,7 @@ export default class SchemaEditor {
var self = this;
// ///////////////
// ADD FIELD INIT
- $("#form .autotoc-nav > a", this.el).each(function () {
+ $("#form .autotoc-nav a", this.el).each(function () {
$(this).on("click", function (e) {
e.preventDefault();
var fieldset_id = $(this).attr("data-fieldset_drag_id");
@@ -115,7 +115,7 @@ export default class SchemaEditor {
});
});
- var fieldset_id = $("#form .autotoc-nav > a.active", this.el).attr(
+ var fieldset_id = $("#form .autotoc-nav a.active", this.el).attr(
"data-fieldset_drag_id"
);
this.init_add_field(fieldset_id);
@@ -247,8 +247,7 @@ $.fn.plone_schemaeditor_html5_sortable = function (
}
$(this).attr("data-fieldset_drag_id", i);
});
- $("#form .autotoc-nav > a").each(function (i) {
- // Plone 5 / mockup
+ $("#form .autotoc-nav a").each(function (i) {
var $fieldset = $("#form fieldset#fieldset-" + i, self.el);
if ($fieldset.data("can-add-fields") === true) {
$(this).attr("droppable", "true");