executablebooks / sphinx-panels

A sphinx extension for creating panels in a grid layout
https://sphinx-panels.readthedocs.io
MIT License
83 stars 20 forks source link

Allow synchronized tabs (based on title) #40

Closed pradyunsg closed 3 years ago

pradyunsg commented 3 years ago

Is your feature request related to a problem? Please describe.

It'd be super cool to be able to use sphinx-panel's tab implementation to have synchronized tabs on a single page.

Describe the solution you'd like

This should be possible by having input elements on a per-title on a per-page basis. This would introduce some complexity (only add the inputs on the first tab with it, on a per-page basis) but it would allow us to basically deprecate the sphinx-tabs extension since we'll become feature-equivalent to it.

Describe alternatives you've considered

Status quo, of recommending sphinx-tabs for synchronized tabs.

Additional context

I spent a lot of time thinking, and couldn't figure out a good reason, that someone might want tabs with the same title for a tab across multiple "tab groups", on the same page and not want them to be synchronized. :)

chrisjsewell commented 3 years ago

Yeh it would be great if we could get this to work. Note, I had a very brief play around with it, but didn't find an immediate solution.

pradyunsg commented 3 years ago

Coolieo. I'll give this a shot!

chrisjsewell commented 3 years ago

BTW, still waiting for your reply on https://github.com/executablebooks/meta/issues/137 😉. It would be great to get that worked out before doing any further work here and sphinx-book-theme etc.

pradyunsg commented 3 years ago

Wait, what?

pradyunsg commented 3 years ago

Oh, I thought I wrote a big comment there alreadY!

pradyunsg commented 3 years ago

Question: would it be OK to inject a script tag in the document for this? The following gets 80% there, but it'd need us to inject a style tag with #<id>:checked + * label[for=<id>] + .tabbed-content {display: block} for each of the IDs on the page.

diff --git a/sphinx_panels/scss/panels/_tabs.scss b/sphinx_panels/scss/panels/_tabs.scss
index e5156de..7f02029 100644
--- a/sphinx_panels/scss/panels/_tabs.scss
+++ b/sphinx_panels/scss/panels/_tabs.scss
@@ -55,6 +55,34 @@
   }
 }

+// Hide radio buttons
+.tabbed-input--hidden {
+  opacity: 0;
+  position: absolute;
+
+  // Active tab label
+  &:checked + label {
+    border-color: var(--tabs-color-label-active);
+    color: var(--tabs-color-label-active);
+
+    // Show tabbed block content
+    +.tabbed-content {
+      display: block;
+    }
+  }
+
+  // Focused tab label
+  &:focus + label {
+    outline-style: auto;
+  }
+
+  // Disable focus indicator for pointer devices
+  &:not(.focus-visible) + label {
+    outline: none;
+    -webkit-tap-highlight-color: transparent;
+  }
+}
+
 // Tabbed block container
 .tabbed-set {
   border-radius: px2rem(2px);
@@ -63,40 +91,12 @@
   margin: 1em 0;
   position: relative;

-  // Hide radio buttons
-  >input {
-    opacity: 0;
-    position: absolute;
-
-    // Active tab label
-    &:checked+label {
-      border-color: var(--tabs-color-label-active);
-      color: var(--tabs-color-label-active);
-
-      // Show tabbed block content
-      +.tabbed-content {
-        display: block;
-      }
-    }
-
-    // Focused tab label
-    &:focus+label {
-      outline-style: auto;
-    }
-
-    // Disable focus indicator for pointer devices
-    &:not(.focus-visible)+label {
-      outline: none;
-      -webkit-tap-highlight-color: transparent;
-    }
-  }
-
   // Tab label
   >label {
     border-bottom: px2rem(2px) solid transparent;
     color: var(--tabs-color-label-inactive);
     cursor: pointer;
-    font-size: px2rem(20px);
+    font-size: px2rem(14px);
     font-weight: 700;
     padding: px2em(20px, 20px) 1.25em px2em(10px, 20px);
     transition: color 250ms;
diff --git a/sphinx_panels/tabs.py b/sphinx_panels/tabs.py
index ae274a1..405beaa 100644
--- a/sphinx_panels/tabs.py
+++ b/sphinx_panels/tabs.py
@@ -8,6 +8,8 @@ from sphinx.util.docutils import SphinxDirective
 from sphinx.util.logging import getLogger
 from sphinx.util.nodes import NodeMatcher

+from .utils import slugify
+
 LOGGER = getLogger(__name__)

@@ -121,9 +123,29 @@ class TabbedHtmlTransform(SphinxPostTransform):
     default_priority = 200
     builders = ("html", "dirhtml", "singlehtml", "readthedocs")

+    def __init__(self, document, startnode=None):
+        super().__init__(document, startnode)
+
+        # This is used to ensure we put one <input> element for each title.
+        self._seen_inputs_map = {}
+
+    def add_input_unless_repeated_in_document(self, input_node):
+        node_key = input_node["id"]
+
+        if node_key in self._seen_inputs_map:
+            return True
+
+        # Was not already on input, we'll add it now.
+        self._seen_inputs_map[node_key] = input_node
+        return False
+
     def get_unique_key(self):
         return str(uuid4())

+    def get_key_for(self, title_tag):
+        title = title_tag.children[0]
+        return "tabbed--extension--" + slugify(title)
+
     def run(self):
         matcher = NodeMatcher(nodes.container, type="tabbed")
         tab_set = None
@@ -140,6 +162,14 @@ class TabbedHtmlTransform(SphinxPostTransform):
                 tab_set = TabSet(node)
         self.render_tab_set(tab_set)

+        # Add inputs that we need to have in the document
+        for input_tag in self._seen_inputs_map.values():
+            self.document.insert(0, input_tag)
+
+    # def add_input_unless_repeated(self, input_node):
+    #     if self.add_input_unless_repeated_in_document(input_node):
+    #         return
+
     def render_tab_set(self, tab_set: Optional[TabSet]):

         if tab_set is None:
@@ -163,16 +193,17 @@ class TabbedHtmlTransform(SphinxPostTransform):
             # TODO warn and continue if incorrect children
             title, content = tab.children
             # input <input checked="checked" id="id" type="radio">
-            identity = self.get_unique_key()
+            identity = self.get_key_for(title)
             input_node = tabbed_input(
                 "",
                 id=identity,
                 set_id=set_identity,
                 type="radio",
                 checked=(idx == selected_idx),
+                classes=["tabbed-input--hidden"],
             )
             input_node.source, input_node.line = tab.source, tab.line
-            container += input_node
+            self.add_input_unless_repeated_in_document(input_node)
             # label <label for="id">Title</label>
             # TODO this actually has to be text only
             label = tabbed_label("", *title.children, input_id=identity)
diff --git a/sphinx_panels/utils.py b/sphinx_panels/utils.py
index 19ebd7e..6bc9e3d 100644
--- a/sphinx_panels/utils.py
+++ b/sphinx_panels/utils.py
@@ -1,9 +1,10 @@
 from ast import literal_eval
 import re

-REGEX = re.compile(
+KEY_VALUE_REGEX = re.compile(
     r'\s*(?:(?P<key>[a-zA-Z0-9_]+)\s*\=)?\s*(?P<value>".*"|[^,]+)\s*(?:,|$)'
 )
+SLUGIFY_REGEX = re.compile(r"\W")

 def eval_literal(string):
@@ -17,9 +18,14 @@ def eval_literal(string):
 def string_to_func_inputs(text):
     args = []
     kwargs = {}
-    for key, value in REGEX.findall(text):
+    for key, value in KEY_VALUE_REGEX.findall(text):
         if key:
             kwargs[key.strip()] = eval_literal(value.strip())
         else:
             args.append(eval_literal(value.strip()))
     return args, kwargs
+
+
+def slugify(text):
+    print(text)
+    return SLUGIFY_REGEX.sub("-", text)
pradyunsg commented 3 years ago
#tabbed--extension--reStructuredText:checked + * + * .tabbed-label[for="tabbed--extension--reStructuredText"] + .tabbed-content {
    display: block;
}
#tabbed--extension--markdown:checked + * .tabbed-label[for="tabbed--extension--markdown"] + .tabbed-content {
    display: block;
}

Works on the page I'm working with. It's ugly, but it works. :)

pradyunsg commented 3 years ago

Bah, I give up. I'd much rather have 3 lines of JS than these 50 lines of Python-generating-CSS. :)