Closed yacir closed 6 years ago
Hey @yacir, that sounds awesome. It's a feature that was requested quite often and I like the general way how you implemented it. I would be happy to integrate it into Material if we can make it work, however I would favor a way that degrades gracefully in older browsers or if JavaScript is not available because Material is built with progressive enhancement in mind which is why it is so successful.
My general feeling is that I want to use radio buttons to allow interaction without JavaScript, see for example this link - I know it's ugly but it shows the basic idea. What you would need to provide is source that can be styled by Material. It must be dependency free (no Bootstrap) and work with HTMl and CSS only. Of course I can assist you on the way. Also I think that our users would favor minimal configuration, so the defaults should work with Material if possible.
A very good example of such a symbiosis is facelessuser/pymdown-extensions which is tightly integrated with Material. @facelessuser and me did a lot of iteration on extensions like Task List and Details for example.
Does this work for you?
Hey @squidfunk thank you for your quick feedback, i'm really happy with it.
I'm agree with you. The idea is that the developer who wants to use a Bootstrap based template, he can choose between the bootstrap3
or bootstrap4
and he will not have to add any extra css ou js. But for the one who wana use it with a dependency free template like material he will use the default
one.
I made the default-template.html
following the the W3C Exemple but i can definitely change it for something JS free like the link you gived me 😊
I ll made the needed changes and let you know.
Is it good for you?
Yes, of course. Just try to implement it without JavaScript and come back to me when you got a proposal we can discuss.
What do you think about this solution:
{% extends "base-template.html" %}
{% macro code_tabs(headers, contents, active_class, group_id) -%}
{% for header in headers %}
<input name="{{ group_id }}"
type="radio"
id="{{ header.id }}"
{{ 'checked="checked"' if loop.first else '' }}
class="code-tab"
data-lang="{{ header.lang }}"
aria-controls="{{ contents[loop.index0].id }}-panel"
role="tab"/>
<label for="{{ header.id }}"
class="code-tab-label"
data-lang="{{ header.lang }}"
id="{{ header.id }}-label">{{ header.title|title }}</label>
<div class="code-tabpanel"
role="tabpanel"
data-lang="{{ header.lang }}"
id="{{ contents[loop.index0].id }}-panel"
aria-labelledby="{{ header.id }}-label">{{ contents[loop.index0].code }}
</div>
{% endfor %}
{%- endmacro %}
The generated code will be:
<div class="md-fenced-code-tabs" id="tab-group-0">
<input name="tab-group-0" type="radio" id="tab-group-0-0_swift" checked="checked" class="code-tab" data-lang="swift" aria-controls="tab-group-0-0_swift-panel"
role="tab">
<label for="tab-group-0-0_swift" class="code-tab-label" data-lang="swift" id="tab-group-0-0_swift-label">Swift 2</label>
<div class="code-tabpanel" role="tabpanel" data-lang="swift" id="tab-group-0-0_swift-panel" aria-labelledby="tab-group-0-0_swift-label">
code...
</div>
<input name="tab-group-0" type="radio" id="tab-group-0-1_swift" class="code-tab" data-lang="swift" aria-controls="tab-group-0-1_swift-panel"
role="tab">
<label for="tab-group-0-1_swift" class="code-tab-label" data-lang="swift" id="tab-group-0-1_swift-label">Swift 3</label>
<div class="code-tabpanel" role="tabpanel" data-lang="swift" id="tab-group-0-1_swift-panel" aria-labelledby="tab-group-0-1_swift-label">
code...
</div>
</div>
A basic CSS to manage the interaction could be like
.md-fenced-code-tabs { display: flex; flex-wrap: wrap;}
.code-tab { position: absolute; opacity: 0; }
.code-tabpanel { display: none; order: 99; }
.code-tab:checked + .code-tab-label + .code-tabpanel { display: block; }
Thanks! Do you have some site where I can see this? It's easier to check out and evaluate then.
@yacir I'd also make a note that this tab implementation will not allow nesting of tabbed fenced code. To use this extension, a user will have to sacrifice nested fenced code which pymdown-extensions provides via SuperFences. Both extensions will not work together. While there is nothing wrong with this if the user expects it, it should be at least noted so that this is pointed out in Material docs if it is decided that it will support it.
Hmm, I was hoping that it works somehow. Is there no way to support this in SuperFences?
They both use the same syntax, but with extra options in the header. So when using these tabbed fences, SuperFences would most likely ignore tabbed fenced if the fences included the fct_label
in the header.
```lang fct_label="whatever"
Since the tab extension auto-generates a label if `fct_label` is not included, SuperFences would need to be run *before* this tab extension is run. Then hopefully SuperFences would capture what it needed, and then this extension would capture whatever is left.
So in this case, fences without labels would work nested, but the tabbed ones would not be nested. SuperFences would need to add this tabbed functionality for it to work with both, or be forked and altered and used instead of SuperFences. We had talked before about tabbed fenced blocks, and I was never against it, but what I generally don't like is the approach of assuming consecutive code blocks are meant to be tabbed. I much prefer an explicit way of saying this "this code block is meant to be tabbed". That way just posting two fences right after one another won't auto-magically become tabs. Most likely this could be done with a slight tweak to the fence syntax to signify a tabbed fence.
We never really settled on a standardized HTML output, so I never investigated this further.
In short, if we have a reasonable HTML output, we could deal with how to augment the Markdown syntax and add tabbed support.
@squidfunk here is a sample file md-code-tabs.html.zip.
@facelessuser Thanks for the feedback! Agree with you and with you approach:
what I generally don't like is the approach of assuming consecutive code blocks are meant to be tabbed.
I think there's a way to do it right, adding an option in code first line to explicitly tel us that this following fences should be rendered as a tabs. The extension will only render those fences and ignore the others. I'm thinking about something like that:
```lang as_tab=true tab_label="whatever"
```
What do you think about it?
```lang as_tab=true tab_label="whatever"
@yacir I think something like this is reasonable.
I think at this point, I'm most interested in seeing @squidfunk's evaluation of the HTML output. I'm generally not interested in multiple tab output modes. I would generally prefer a generalized, singular output. Maybe just support default-template. I'm not sure I want to support specific things like bootstrap iterations etc.
Thanks for your input. I fiddled a little and this is my proposal (see CodePen for demo). Narrow your browser window to < 900px to see the mobile.
My proposal is based on the following requirements:
Here is how I addressed the requirements
As discussed, the solution is based on radio buttons which target input
fields that make the respective code section appear or disappear. For this to work, the input
field must be located in front of the code section and on the same level.
In order to make the label
styleable, the corresponding input
must also be located in front of the label. This leads to the only possible design where the order is input
, label
, code section.
Because we want to collapse all non-active languages on mobile behind a selection, we need a state of the radio button that hides all code sections and displays all labels. This input field must precede all other labels. I thought of this design because in my opinion it's the only possible option. In theory, we could put languages and code sections in separate containers so we could make the languages container horizontally scrollable but this would violate 2., because the labels would not be located to the input
elements which is not fixable due to requirement 1.
As we must place everything like discussed, the code sections and input
and label
tags are interleaved, so we need to use display: flex
and wrap the code section around with maximum width. We apply order: 99
to the active code section so that it comes after all labels and hide all other code sections. For this to work, all labels must be on the same level, which is a further requirement and satisfies 1., 2. and 3.
On mobile, we show a Change language label, which when active displays all other labels and hides all other code sections. The display is no perfect in the CodePen demo I made, there's still some room for experimentation, but the basic idea is clear.
When we have JavaScript available, we could synchronize all code tabs, so if the language in one code section is changed, it changes all other code sections. For this to be reasonably easy, the best thing would be to add a data-language="${language}
to each label, so we can better register event listeners with JavaScript.
I'm also happy with the proposed syntax by @yacir, maybe renaming the flags a little:
``` lang as_tab=true tab_label="Custom label"
I know that the HTML I'm proposing may seem a little weird, but all of it is necessary for this solution to work. The most headache will probably be the Change language label/code, because without styling there's no real application for it. Maybe we could do it like we did it with Task List - optional CSS classes which are mandatory for Material to be set but leave a little flexibility for other implementations.
Questions I have:
as_tab
flag?Disclaimer: I just hacked this together, so the CSS might change, but the HTML is final
<div class="superfences-tabs">
<input name="tabs_1" type="radio" id="tab_1_0" />
<label for="tab_1_0">Change language</label>
<input name="tabs_1" type="radio" id="tab_1_1" checked="checked" />
<label for="tab_1_1">Bash</label>
<div class="codehilite">
<pre>
#!/bin/bash
STR="Hello World!"
echo $STR
</pre>
</div>
<input name="tabs_1" type="radio" id="tab_1_2" />
<label for="tab_1_2">C</label>
<div class="codehilite">
<pre>
#include <stdio.h>
int main(void) {
printf("hello, world\n");
}
</pre>
</div>
<input name="tabs_1" type="radio" id="tab_1_3" />
<label for="tab_1_3">C++</label>
<div class="codehilite">
<pre>
#include <iostream>
int main() {
std::cout << "Hello, world!\n";
return 0;
}
</pre>
</div>
<input name="tabs_1" type="radio" id="tab_1_4" />
<label for="tab_1_4">C#</label>
<div class="codehilite">
<pre>
using System;
class Program {
static void Main(string[] args) {
Console.WriteLine("Hello, world!");
}
}
</pre>
</div>
</div>
* {
box-sizing: border-box;
}
body {
padding: 10px;
background: #f2f2f2;
font-family: Helvetica;
}
.superfences-tabs {
display: flex;
position: relative;
flex-wrap: wrap;
}
.superfences-tabs input {
position: absolute;
opacity: 0;
}
.superfences-tabs label {
width: 100%;
padding: 4px;
margin: 0 8px;
cursor: pointer;
}
.superfences-tabs input:nth-child(n+1):checked + label {
color: #333;
}
@media (min-width: 900px) {
.superfences-tabs label {
width: auto;
}
}
.superfences-tabs .codehilite {
display: none;
width: 100%;
background: #ddd;
margin-top: 10px;
}
.superfences-tabs .codehilite pre {
font-family: Monaco;
font-size: 13px;
padding: 10px 12px;
margin: 0;
}
@media (min-width: 900px) {
.superfences-tabs .codehilite {
order: 99;
}
.superfences-tabs input:nth-child(n+1):checked + label {
color: #ff5252;
}
}
.superfences-tabs input:nth-child(n+1):checked + label + .codehilite {
display: block;
}
.superfences-tabs input:first-child + label {
display: none;
}
@media (max-width: 899px) {
.superfences-tabs input:first-child + label {
display: initial !important;
position: absolute;
width: auto;
display: initial;
right: 0;
color: #ff5252;
}
.superfences-tabs input:first-child ~ label {
display: none;
}
.superfences-tabs input:first-child:checked ~ label {
display: initial;
}
.superfences-tabs input:checked + label {
display: initial !important;
}
}
The next requirement that is very likely to come is tabbed content. I'd bet on it. All in all I now think we have a great direction for implementing this feature which was, as I said, demanded quite often.
For explanation: my last post was mainly targetted at @facelessuser regarding the integration of this feature into SuperFences. This would make the proposed markdown-fenced-code-tabs extension obsolete, but as PymdownX is tightly integrated with Material and offers some nice features like SuperFences, it's the best possible way. @yacir: I hope you don't take this the wrong way, but I would rather favor integration into SuperFences than rolling something own. Interferences with SuperFences is also a real killer for integrating this feature.
@squidfunk, thanks for the detailed post. I will review it and let you know what I think when I get around to implemented this. I have a couple of things that are currently eating up my time, but when I have a chance, I'll come back to this and collaborate further to see what we can do.
The demo looked cool, and I definitely like the idea of this. I will create an issue over on pymdown-extensions and link it back here.
I can answer at least a couple of questions:
What if two code sections with the same language are specified?
I don't think this is a problem. We'll keep it dumb. If the user puts two blocks with the same language, it is on them. I'm fine with managing things to prevent duplicate id's etc., but I'm not big on protecting the user from themselves. The example currently is that people would use this for only different languages, but I think people could use this for lots of different purposes, I'm not going to police or babysit how people use it 😄 .
What if in three consecutive code sections, the middle section doesn't define the as_tab flag?
That's on them. First will be a tab set with one entry. Second would be a normal code block, third will be a tab set with one entry. It doesn't make sense to have a single tab, but I won't prevent people from doing such. See answer to first question.
Is this integrateable into SuperFences, so that we could even render indented tabbed code sections (e.g. in lists)?
Yes, this should be doable in SuperFences. The idea is to add the option into SuperFences. Exactly how that will work and look yet, I'm not sure, but user side, I imagine it will be similar to what has been suggested. Exact implementation is subject to change, but I'll keep you posted if I plan to deviate at all from the initial proposal. I haven't looked it over it great detail yet, but I'll keep things open for input as well.
Thanks @facelessuser, glad you also think this is a good direction to progress. Regarding the Change language label - I don't know if it makes sense that you generate this in SuperFences because we need to localize (i18n) it properly, so it should be generated by Material. I thought a little about it and have the following idea:
We generate the bare-minimum HTML (so for for each code block a label + input + code section) and on mobile, if no JS is available, fall back to an accordion-based approach (see this link which I took for inspiration). If we have JavaScript available, which should be the default case, we generate the additional input/label and collapse all languages behind that selection as proposed. So Superfences would need to generate this:
<div class="superfences-tabs">
<input name="tabs_${index}" type="radio" id="tab_${index}_${tab_index}" />
<label for="tab_${index}_${tab_index}">Bash</label>
<div class="codehilite"> <!-- or <pre>, should depend on environment -->
...
</div>
...
</div>
Feel free to change the class names, but it would be nice if they would be in the style of the Task List extension, for consistency. I thought about giving the label also a class name, but it's theoretically targetable like this, so, I'm undecided.
The first item should be marked with checked="checked"
. This should also work if Codehililite is not enabled (thus rendering pre
and code
tags instead of the div
, as it works with Material now, so people can use other JS-based syntax highlighters if they want to.
Oh regarding your ideas concerning my questions - I totally agree. I tend to overthink possible pitfalls when users try to use that extension and minimize them, but that's probably not necessary for developers. Let's keep it as simple as possible.
I don't know if it makes sense that you generate this in SuperFences because we need to localize (i18n) it properly
Ahh, sorry, I actually misinterpreted your question. I see now that there is a "Change Language". You are correct, I would absolutely omit that as it should be a generalized output.
We generate the bare-minimum HTML (so for for each code block a label + input + code section) and on mobile, if no JS is available, fall back to an accordion-based approach (see this link which I took for inspiration).
This is what I originally thought you'd do, so I think we are on the same page.
Oh regarding your ideas concerning my questions - I totally agree. I tend to overthink possible pitfalls when users try to use that extension and minimize them, but that's probably not necessary for developers. Let's keep it as simple as possible.
Yeah, it all depends on who your customer is. If I am dealing with a user interface, I put a lot of checks in as the user needs protection from themselves. If I'm writing for a developer, I avoid catastrophic stuff, but I'd rather give them flexibility and let them do the work of "user proofing" their code.
Okay, great! This means we should have a pretty solid concept now and I will wait for your implementation. When you have a working prototype ready, I will integrate it into Material, add some documentation and push out a new release. Feel free to come back if you have any questions.
Hey guys, will this implementation allow to eliminate current problems with using markdown_fenced_code_tabs
with superfences
in the same mkdocs project? The problem we're facing is the handling of the ordered and non-ordered lists is broken when both superfences and fenced code tabs extensions are added. Consider the following example (directly from our docs):
java.util.Map
. For example, the following code saves an object with properties "name"
and "age"
in the "Person"
table:
HashMap person = new HashMap();
person.put( "name", "Joe" );
person.put( "age", 25 );
Backendless.Data.of( "Person" ).save( person );
This approach is referenced as "Java Map" further in the documentation. or
When both markdown_fenced_code_tabs
and superfences
are enabled, it is rendered like this (which is great, but code tabs in other parts of the doc do not work): http://take.ms/cHD4C
However, without superfences, it looks broken: http://take.ms/DtDWJ
The original fenced_code extension that is shipped with Python Markdown doesn't handle nesting (nested under things like lists, dictionary, admonitions, etc.). markdown_fenced_code_tabs
likely builds off the the original, so it shares the same issues. This is due to a weakness in Python Markdown. I won't go into details here.
With SuperFences, I went through great pains to make it work in most situations, working around Python Markdown's weaknesses. That is why SuperFences is required for proper nested code. With that said, by adding tab support into the SuperFences syntax, you will automatically get tabs with the benefit of nesting support.
I just need to carve out the time to make it happen.
@facelessuser thank you for the response. I would love to see these changes, I am sure many other users would too.
@squidfunk thank for the help.
@markainick because this feature is still unavailable with superfences
i've pushed un new version of the markdown_fenced_code_tabs
extension you can take a look to the documentation.
Hope it helps.
@yacir is this update supposed to work when the pymdownx.superfences
extension is enabled? I updated to the latest version of the markdown_fenced_code_tabs
extension and when the superfences are turned on, the code tabs are not rendered at all.
@markpiller unfortunately it still don't work with superfences
.
(WIP) Superfences + tabs 😮
```Bash tab=""
#!/bin/bash
STR="Hello World!"
echo $STR
#include
int main(void) {
printf("hello, world\n");
}
#include <iostream>
int main() {
std::cout << "Hello, world!\n";
return 0;
}
using System;
class Program {
static void Main(string[] args) {
Console.WriteLine("Hello, world!");
}
}
Lists
#!/bin/bash
STR="Hello World!"
echo $STR
#include
int main(void) {
printf("hello, world\n");
}
#include <iostream>
int main() {
std::cout << "Hello, world!\n";
return 0;
}
using System;
class Program {
static void Main(string[] args) {
Console.WriteLine("Hello, world!");
}
}
#!/bin/bash
STR="Hello World!"
echo $STR
Break
#include
int main(void) {
printf("hello, world\n");
}
Break
#include <iostream>
int main() {
std::cout << "Hello, world!\n";
return 0;
}
Break
using System;
class Program {
static void Main(string[] args) {
Console.WriteLine("Hello, world!");
}
}
Looks great, really works for me! The syntax is also nice - only one option for tab rendering and title.
Thanks! Yeah, multiple options in the header kind of rubbed me wrong. I couldn't find a reason to justify requiring two. I may in the end allow tab=
if you don't want to define a title. There are still some little details I need to think about, and some more tests that need to be done, but overall, I'm pretty pleased with the results. It turned out much easier to implement than I thought.
That sounds great :-) Happy to integrate it into Material finally solving this problem.
Will Change language
label on mobile be editable? I'm asking because sometimes you want to use tabs for different things than programming languages.
My personal opinion is it should be something more general like "change tab", or just an icon that gets the idea across.
Or maybe something like https://codepen.io/lukejacksonn/pen/PwmwWV Bad: requires JS for hiding elements.
Edit: only CSS version - https://codepen.io/chriscoyier/pen/GJRXYE
Will Change language label on mobile be editable? I'm asking because sometimes you want to use tabs for different things than programming languages.
It will definitely be translatable via i18n and localized. However, as I understand tabs are only supported for code blocks, not regular content so there's always code inside those tabs, or am I wrong?
Edit: only CSS version - https://codepen.io/chriscoyier/pen/GJRXYE
I very much like this one, may take something out of it for the implementation.
Well..."code blocks" yes, but "code blocks" can contain anything, not just code. It's more preformatted text, commonly used for code. In addition, we could render things like flow charts in it...which technically isn't even in code blocks. It may make sense to wrap content of a tab in another div with a class and target that instead of the code blocks directly...
That is some of the things I am still figuring out. But code blocks, they look pretty awesome so far 🙂 .
Okay, so SuperFences allows users to create special, custom fences which is used to create flow charts and/or sequence diagrams. In order to get these to work, I had to add a div around the tab content so I could craft a generalized approach. Instead of targeting the code block and hiding that, you'd just target the content div with the class .superfences-content
; that way it works with code blocks or whatever has been customized to be rendered in the fence's place.
@squidfunk, I think I'm done with support for now: https://github.com/facelessuser/pymdown-extensions/pull/302. You are free to start playing with it and give feedback if it turns out something needs to change. As noted, I did add an extra div to the output that wraps the tab content. It makes sense that the div should be targeted for hiding and showing instead of the content inside (code blocks etc.).
Sounds great. I could try and integrate it – how do it do that? I guess I clone the repository, uninstall the version I installed with pip and install it from source? Not very experienced with Python ;-)
You can actually upgrade your local version based on the tag with pip. Cloning it yourself is not required. When I'm by my computer again, I'll illustrate how this is done.
@squidfunk, you can install with the following. It just performs an install upgrade, and you specify that you are using git + the git URL @ the branch.
python -m pip install -U git+https://github.com/facelessuser/pymdown-extensions.git@fenced-tabs
@facelessuser, I am having a difficulty getting it working. I have installed per the instruction you provided above, here's the output of the installation command:
Collecting git+https://github.com/facelessuser/pymdown-extensions.git@fenced-tabs
Cloning https://github.com/facelessuser/pymdown-extensions.git (to revision fenced-tabs) to /private/var/folders/4k/pxnpxlp963n_l2zh87rdc_v00000gn/T/pip-req-build-E1lCmk
Requirement not upgraded as not directly required: Markdown>=2.6.10 in /usr/local/lib/python2.7/site-packages (from pymdown-extensions==4.10.2) (2.6.11)
Building wheels for collected packages: pymdown-extensions
Running setup.py bdist_wheel for pymdown-extensions ... done
Stored in directory: /private/var/folders/4k/pxnpxlp963n_l2zh87rdc_v00000gn/T/pip-ephem-wheel-cache-6d9QhQ/wheels/de/f0/9f/718b0ab1c3218cbd3f7d5fd56c80d973fc2d85fcd8f1924153
Successfully built pymdown-extensions
Installing collected packages: pymdown-extensions
Found existing installation: pymdown-extensions 4.10.2
Uninstalling pymdown-extensions-4.10.2:
Successfully uninstalled pymdown-extensions-4.10.2
Successfully installed pymdown-extensions-4.10.2
My mkdocs extensions are:
markdown_extensions:
- admonition
- codehilite:
linenums: False
guess_lang: False
- toc:
permalink: True
- pymdownx.details
- pymdownx.superfences
The markdown content is written as follows:
#### Blocking API ```` ```java tab="Using Java Map" Long result = Backendless.Persistence.of( "TABLE-NAME" ).remove( Map entity ) ``` ```java tab="Custom Class" Long result = Backendless.Persistence.of( E ).remove( E entity ); ``` ```` #### Non-Blocking API ```` ```java tab="Using Java Map" public void Backendless.Persistence.of( "TABLE-NAME" ).remove( Map entity, AsyncCallbackresponder ) ``` ```java tab="Custom Class" Backendless.Persistence.of( E ).remove( E entity, AsyncCallback responder ); ``` ````
And with that the content is rendered like this: http://take.ms/cMmGZ
Could you please let me know if I am doing something wrong here?
Why do you have two tab fences wrapped in another fence? Basically you have one non tabbed fence with the text showing tab fence syntax. Also, you'll have to add sufficient CSS.
I was following the example from here: https://github.com/facelessuser/pymdown-extensions/blob/fenced-tabs/docs/src/markdown/extensions/superfences.md#tabbed-fences
Specifically, this: http://take.ms/jjaO1
I added the CSS, the problem is with the HTML that's generated.
@markpiller, thinking about this, most likely you probably got your example from the GitHub rendering of the docs? It isn't always easy looking at that as it isn't rendered properly with the non-standard features that my docs use.
Here is a good example of syntax that should work: test file. It's just various test cases. As you can see, this test file renders as this: html. This is the current CSS used to render the test: css.
@markpiller, Just for reference, this is how the document you referenced looks when rendered for publishing:
@facelessuser my bad, it works great for all our use-cases. Thank you very much!
@markpiller, no problem, glad it's working and you find it useful. Keep in mind that this is currently in beta. Some things are subject to change between now and release.
After evaluating this feature, I think I'm going to lock in the current format and cut a release over the weekend. I'm doubtful there will be any real changes to the structure at this point.
Implemented in #793, it now looks like this:
I made it very simple and tried to add as few noise as possible by leaving the bottom borders out. Also I didn't realize the mobile version I discussed before because I don't really think it's necessary and most of the time the proposed solution should work. It could only be a problem when people have too many tabs.
Very cool. I should be releasing latest pymdown-extensions later today.
Hello @squidfunk, first off, thanks for your great job with material theme! 😊
I'm working on a new version of the markdown-fenced-code-tabs extension and i will be happy if it could be supported by the mkdocs-material.
The current version generates tabs exclusively as Bootstrap3 HTML template 😞.
But the new version i'm working on and which is already ready offerts the option to choose the rendering template. You can choose
bootstrap3
,bootstrap4
ordefault
.There is any chance that you support the
default-template
? 😊Here is the branch link for the new version.
Let me know if you need more details or if you have any suggestions.