Whilst HTML 5 did give us the <details> and <summary> tags for collapsible sections, it is often just the wrong code from a semantic point of view. For example if you only want to show part of a list until the user clicks on a section, you would have to split that list into two separate ones, which is grammatical/structural gibberish.
尽管HTML 5确实为我们提供了可折叠部分的<details>和<summary>标签,但是从语义的角度来看,它通常只是错误的代码。 例如,如果您只想显示列表的一部分,直到用户单击某个部分,则必须将该列表分成两个单独的列表,这是语法/结构上的胡言乱语。
When making these types of code sections the norm is for people to rely on JavaScript for it. Most of this scripting relies on bloated libraries, and is hardcoded to specific elements. In his good article on collapsing sections — the inspiration for this article — Alex Zito-Wolf falls into many of the pitfalls and traps of the traditional approach.
在制作这些类型的代码段时,人们通常会依靠JavaScript。 此脚本大部分依赖肿的库,并且被硬编码为特定元素。 亚历克斯·齐托-沃尔夫( Alex Zito-Wolf)在其关于折叠部分的出色文章(本文的灵感)中发现了传统方法的许多陷阱和陷阱。
为此使用JavaScript有什么问题? (What’s Wrong With Using JavaScript For This?)
The first issue is one of graceful degradation. To meet the best of the best of the best — with honors — of accessibility norms, you should plan for what happens when JavaScript just isn’t there. The issues in his original includes:
第一个问题是正常降级之一。 为了达到最好的可访问性准则中最好的最好的准则,您应该计划当JavaScript不存在时会发生什么。 他原著中的问题包括:
It doesn’t work scripting disabled/blocked. That’s bad for accessibility and usability. Worse, the elements are hidden and CANNOT be opened when scripting is AWOL.
禁用/阻止脚本无效。 这对可访问性和可用性不利。 更糟的是,元素是隐藏的,当脚本是擅离职守无法打开。
- Static links in the markup that do nothing when scripting is off / blocked / disabled. Their text content would be very confusing since for non-visual users. 禁用/阻止/禁用脚本时,标记中的静态链接不起作用。 因为对于非视觉用户,他们的文本内容将非常混乱。
The use of
display:none
in the default state means that search engines won’t see the hidden content. EVER! This could also effect some non-visual UA’s and is done because a little over a decade ago we had this sleazy trick called “Content cloaking” that black hat SEO dirtbags used to use in abusing the system. Prior to that point in history, search didn’t even bother looking at your style. And apart from looking for abuse and mobile compatibility, really still doesn’t care!在默认状态下使用
display:none
意味着搜索引擎将看不到隐藏的内容。 永远! 这也可能会影响一些非可视化的UA,并且这样做是因为十多年前,我们曾有过这种卑鄙的技巧,称为“内容掩盖”,黑帽SEO污物袋过去曾用它来滥用系统。 在此之前,搜索甚至不必理会您的样式。 除了寻找滥用和移动兼容性之外,真的还是不在乎!- It’s hard coded to only work on lists. 硬编码仅适用于列表。
- You also have to hard code the call to his functionality in the script. It doesn’t simply “auto-hook” things for you. 您还必须在脚本中硬编码对其功能的调用。 它并不仅仅是为您“自动挂机”。
I don’t put that on him, he’s just doing what most people do and what most tutorials teach. That’s how people typically write their JavaScript because they never learned about accessibility, progressive enhancement, graceful degradation, or even the most basic concepts of what HTML actually is; much less what it’s actually for.
我没有把这个放在他身上,他只是在做大多数人所做的事情以及大多数教程所教的内容。 人们之所以这样来编写JavaScript,是因为他们从未了解过可访问性,渐进增强,优雅降级甚至是HTML实际的最基本概念。 更不用说了。
This is doubly true if you’ve been suckered by various garbage frameworks like bootcrap, tailwind, jquery, react, angular, vue, etc, etc, etc. Said train wreck disasters of how NOT to use HTML, CSS, or JavaScript being riddled with bad practices that tell users with accesibility needs to go plow themselves.
如果您被各种垃圾框架(如bootcrap,顺风,jquery,react,angular,vue等)所困扰,那么这是双倍的事实。有关如何不使用HTML,CSS或JavaScript的火车毁灭性灾难不胜枚举会告诉具有可访问性的用户的不良做法需要自己耕种。
Trust me, my day job is freelancing as an accessibility and efficiency consultant. I see said “frameworks” and broken methogologies lead developers down the road to failure on nearly every contract now.
相信我,我的日常工作是作为可访问性和效率顾问的自由职业。 我看到所说的“框架”和破碎的方法使开发人员如今几乎每笔合同都失败了。
But the thing is, JavaScript in and of itself isn’t the problem here. In fact done in a better fashion we can eliminate almost all of those problems!
但问题是,JavaScript本身并不是问题所在。 实际上,以更好的方式完成我们几乎可以消除所有这些问题!
那么我们如何做得更好? (So How Do We Do Better?)
IMHO there are two good ways of handling this, each has advantages and disadvantages:
恕我直言,有两种好的处理方法,每种都有优点和缺点:
- Use JavaScript but implements a graceful degradation plan. Our end goal being that our semantic markup behaves normally when scripting is disabled. 使用JavaScript,但可以实施适当的降级计划。 我们的最终目标是当禁用脚本时,我们的语义标记会正常运行。
“Abuse” an
<input type=”checkbox”>
to act as a toggle for CSS via the :checked attribute. This way requires NO JavaScript to function.通过:checked属性“滥用”
<input type=”checkbox”>
作为CSS的切换。 这种方式不需要JavaScript即可运行。
Both techniques can then leverage the “any sibling combinator” to show/hide elements after that one. Likewise they’ll both target a parent element so the adjacent sibling combinator can be used to change the text in the open/close button after.
然后,两种技术都可以利用“任何同级组合器”来显示/隐藏该元素之后的元素。 同样,它们都将目标对象设为父元素,因此可以使用相邻的同级组合器更改之后的“打开/关闭”按钮中的文本。
脚本化方式 (The Scripted Way)
When using JavaScript one of the best ways to do this is to use a class to say what element you want to be the last one shown by default, with everything after it hidden. Using just one class we can “walk the DOM” to get its parent.
使用JavaScript时,执行此操作的最佳方法之一是使用一个类来说明要默认显示的最后一个元素是什么,其中的所有元素都隐藏起来。 仅使用一个类,我们就可以“遍历DOM”以获取其父级。
标记 (Markup)
<h1>Collapsible Section Demo</h1><h2>Fruits</h2>
<ul>
<li>Tomato 🍅</li>
<li class="collapseAfter">Grapes 🍇</li>
<li>Watermelon 🍉</li>
<li>Mango 🥭</li>
<li>Peach 🍑</li>
</ul><h2>Vegetables</h2>
<ul>
<li>Broccoli 🥦</li>
<li class="collapseAfter">Corn 🌽</li>
<li>Carrot 🥕</li>
<li>Eggplant 🍆</li>
</ul><h2>Test Section</h2>
<div>
<p class="collapseAfter">
The first paragraph will show all the time, however later paragraphs will not. This method works on ANY markup so long as there's a common parent wrapper, such as a <code><div></code>
</p><p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vel nibh et dolor dignissim tempor vel vel ante. Fusce vehicula vitae elit eu commodo. Vivamus consectetur diam vitae ligula gravida, sit amet dignissim libero ultricies. Sed at efficitur dolor, ut eleifend arcu. Ut porta volutpat lobortis. Nullam sit amet est ac mauris sollicitudin ultricies. Donec maximus, velit vel lacinia faucibus, odio urna suscipit sem, at tincidunt tortor leo id orci. Nam iaculis tristique ligula. Vivamus urna ligula, iaculis a tellus sed, feugiat venenatis tellus. Maecenas ullamcorper neque at vestibulum cursus. Ut urna sapien, tempus ut ornare eget, ultrices vitae nisl.
<p><p>
Interdum et malesuada fames ac ante ipsum primis in faucibus. Duis commodo dui lectus, id elementum lectus condimentum a. Maecenas feugiat ornare lorem, ac tincidunt purus ultricies eget. Ut fringilla pharetra mi sed sodales. Cras accumsan eleifend massa, et ullamcorper ante tempus ut. Nam dolor diam, finibus a felis et, lacinia viverra dui. Donec magna leo, vestibulum id urna sit amet, dapibus pharetra orci. Sed sodales et erat sed egestas. Phasellus condimentum tempus urna, id facilisis ligula maximus vel.
</p>
</div>
As you can see, the only thing “out of the ordinary” there is the extra <div>
parent around the paragraphs, and the additional class. The way we’re doing this you need all “siblings” in a show hide section to wrapped in a container. With UL/LI that’s easy, as it would be with TBODY/TR or any other organized semantic structure. With paragraphs or other tags though, we need to add something.
如您所见,唯一“与众不同”的地方是在段落周围有额外的<div>
父级以及额外的类。 我们执行此操作的方式需要将show hide部分中的所有“兄弟姐妹”包装在容器中。 有了UL / LI,这很容易,就像使用TBODY / TR或任何其他有组织的语义结构一样。 但是,对于段落或其他标签,我们需要添加一些内容。
JavaScript (JavaScript)
(function() { var collapseHooks = document.getElementsByClassName('collapseAfter'); for (var hook of collapseHooks) {
var
wrap = hook.parentNode,
a = wrap.parentNode.insertBefore(
document.createElement('a'),
wrap.nextSibling
);
a.addEventListener('click', toggleCollapse, false);
a.className = 'collapseAnchor';
a.href="#"; /* without this it's not keyboard focusable */
wrap.classList.add('closed', 'collapseWrap');
} // for hook function toggleCollapse(e) {
e.preventDefault();
e.currentTarget.previousElementSibling.classList.toggle('closed');
} // toggleCollapse})();
I put it into a IIFE so that the scope is isolated. We grab all the elements with .collapseAfter on them and loop through each of those hooks in turn. For each we grab the parentNode wrapper, create an anchor after that wrapper, assign all the properties we need that anchor to have, and then add classes to the parent to say that our scripting is working — .collapseWrap
— and that it’s .closed
. Our CSS will only apply if .collapseWrap
is present, so scripting off it behaves as if the open/close doesn’t exist.
我将其放入IIFE,以便隔离范围。 我们使用.collapseAfter捕获所有元素,然后依次遍历每个这些钩子。 对于每一个,我们抓住parentNode包装器,在该包装器之后创建一个锚,分配我们需要该锚具有的所有属性,然后向父级添加类以表示我们的脚本正在工作— .collapseWrap
—并且它是.closed
。 我们CSS仅在存在.collapseWrap
情况下适用,因此对其进行脚本编写就像打开/关闭不存在一样。
Note that if we don’t put a href on the anchor it won’t click in most browsers and keyboard navigation will skip right past it. Likewise we do NOT put any content into the anchor, as we can let CSS use generated content to do the heavy lifting of controlling/changing the text inside it.
请注意,如果我们在锚点上未添加href,则在大多数浏览器中都不会单击href,并且键盘导航将跳过该点。 同样,我们不会在锚中放置任何内容,因为我们可以让CSS使用生成的内容来繁重地控制/更改其中的文本。
The toggleCollapse
function, called when our anchor is clicked, simply prevents the event, then toggles the .closed
class on the previousElementSibling
of the anchor that launched the event, which we know is our wrapper around the area that can collapse.
当单击锚点时调用toggleCollapse
函数,它仅阻止事件发生,然后在.closed
事件的锚点的previousElementSibling
上切换.closed
类,我们知道这是围绕可折叠区域的包装。
As such our event handler is tiny and fast, and why I really do advocate “walking the DOM” over trying to get/query for elements every blasted time. If your HTML structure is consistent, you can simply sibling/parent/child your way to what you want in the event handler off of event.currentTarget
.
因此,我们的事件处理程序非常小巧,快速,并且为什么我真正提倡“遍历DOM”而不是每次尝试获取/查询元素。 如果您HTML结构是一致的,则可以简单地使事件处理程序中的event.currentTarget
成为同级/父级/子级。
CSS —魔术发生的地方 (CSS — Where the Magic Happens)
.collapseAnchor {
text-decoration:none;
color:blue;
}.collapseAnchor:before {
content:"See Less";
}.collapseWrap.closed + .collapseAnchor:before {
content:"See More";
}.collapseAnchor:after {
content:"\25B2";
display:inline-block;
padding:0.4em 0 0 0.3em;
vertical-align:top;
font-size:0.625em;
}.collapseWrap.closed + .collapseAnchor:after {
content:"\25BC";
}.collapseWrap.closed > .collapseAfter ~ * {
position:absolute;
top:-999em;
left:-999em;
}
Text in the anchor is generated first as the open state, then as the closed state using the adjacent sibling combinator: “+”. This easily lets us set the text for each state.
首先使用相邻的同级组合器“ +”将锚中的文本生成为打开状态,然后生成为关闭状态。 这很容易让我们为每个状态设置文本。
That little UTF-8 triangle is often hard to align properly cross browser, but by setting it to inline-block and adding some padding, we can eyeball where it should be and push it around with margin. When the previous element is closed change it to a down arrow.
那个小的UTF-8三角形通常很难在浏览器中正确对齐,但是通过将其设置为内联块并添加一些填充,我们可以将其视线放在适当的位置并用边距推动它。 关闭上一个元素后,将其更改为向下箭头。
The real trick though is that last part. When the wrapper has the .closed
class we find the .collapseAfter
that’s a child of it, and hide any tag that’s a sibling after it. Some people will say “don’t ever use the * selector” because of overhead issues, but in this case that’s 100% bullcookies. The scope by which it is applied is restricted by the combinator before it, reducing the application/render overhead to nil.
真正的把戏不过是最后一部分。 当包装器具有.closed
类时,我们发现.collapseAfter
是它的子级,并隐藏其后的所有同级标签。 由于开销问题,有些人会说“永远不要使用*选择器”,但是在这种情况下,这是100%的Bullcookie。 组合器在其之前限制了其应用范围,从而将应用程序/渲染开销减少为零。
Hiding is handled via absolute positioning
隐藏通过绝对定位处理
现场演示(Live Demo)
Here’s a pen for y’all
这是给你们的笔
脚本方法的优点和缺点(Pro’s and Cons of the Scripted Approach)
As always let’s start with the problems.
与往常一样,让我们从问题开始。
Disadvantages
缺点
- Relies on JavaScript依靠JavaScript
- Manipulates DOM in way that could confuse some less skilled developers 操作DOM可能会使一些技术欠佳的开发人员感到困惑
- Said DOM manipulation could break if the markup doesn’t remain consistent when other people edit it later.如果在以后其他人对其进行标记时标记保持不一致,则表示DOM操作可能会中断。
Advantages
好处
- Non-Scripted or for non-visual UA’s, the content is simply displayed or accessed normally. This is good for both accessibility and search. 非脚本或非可视UA的内容可以正常显示或访问。 这对可访问性和搜索都有利。
- We only need a single class hook to add the functionality to any wrapped block of sibling elements. 我们只需要一个类钩子即可将功能添加到任何包装的兄弟元素块中。
- Keyboard navigable anchor 键盘导航锚
- The anchor and its content are not static in the markup. Doing so often makes non-visual navigation harder. 锚及其内容在标记中不是静态的。 这样做通常会使非视觉导航更加困难。
But can we do it even better?
但是我们可以做得更好吗?
没有JavaScript的方式 (The No JavaScript Way)
To do this without JavaScript we will need to add hidden INPUT and LABEL to our markup as the hooks for our toggles. Remember that clicking on a LABEL tag is the same as clicking on any input it wraps, or that it is FOR. The latter is what we need here.
要在没有JavaScript的情况下执行此操作,我们需要在标记中添加隐藏的INPUT和LABEL作为切换的钩子。 请记住,单击LABEL标记与单击它包装的任何输入相同,或者为FOR。 后者是我们在这里需要的。
Markup
标记
<h2>Fruits</h2>
<input
type="checkbox"
id="toggle_fruits"
class="toggle_collapse"
hidden
aria-hidden="true"
>
<ul>
<li>Tomato 🍅</li>
<li class="collapseAfter">Grapes 🍇</li>
<li>Watermelon 🍉</li>
<li>Mango 🥭</li>
<li>Peach 🍑</li>
</ul>
<label for="toggle_fruits" hidden aria-hidden="true"></label><h2>Vegetables</h2>
<input
type="checkbox"
id="toggle_vegetables"
class="toggle_collapse"
hidden
aria-hidden="true"
>
<ul>
<li>Broccoli 🥦</li>
<li class="collapseAfter">Corn 🌽</li>
<li>Carrot 🥕</li>
<li>Eggplant 🍆</li>
</ul>
<label for="toggle_vegetables" hidden aria-hidden="true"></label><h2>Test Section</h2>
<input
type="checkbox"
id="toggle_testSection"
class="toggle_collapse"
hidden
aria-hidden="true"
>
<div>
<p class="collapseAfter">
The first paragraph will show all the time, however later paragraphs will not. This method works on ANY markup so long as there's a common parent wrapper, such as a <code><div></code>
</p><p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vel nibh et dolor dignissim tempor vel vel ante. Fusce vehicula vitae elit eu commodo. Vivamus consectetur diam vitae ligula gravida, sit amet dignissim libero ultricies. Sed at efficitur dolor, ut eleifend arcu. Ut porta volutpat lobortis. Nullam sit amet est ac mauris sollicitudin ultricies. Donec maximus, velit vel lacinia faucibus, odio urna suscipit sem, at tincidunt tortor leo id orci. Nam iaculis tristique ligula. Vivamus urna ligula, iaculis a tellus sed, feugiat venenatis tellus. Maecenas ullamcorper neque at vestibulum cursus. Ut urna sapien, tempus ut ornare eget, ultrices vitae nisl.
<p><p>
Interdum et malesuada fames ac ante ipsum primis in faucibus. Duis commodo dui lectus, id elementum lectus condimentum a. Maecenas feugiat ornare lorem, ac tincidunt purus ultricies eget. Ut fringilla pharetra mi sed sodales. Cras accumsan eleifend massa, et ullamcorper ante tempus ut. Nam dolor diam, finibus a felis et, lacinia viverra dui. Donec magna leo, vestibulum id urna sit amet, dapibus pharetra orci. Sed sodales et erat sed egestas. Phasellus condimentum tempus urna, id facilisis ligula maximus vel.
</p>
</div>
<label for="toggle_testSection" hidden aria-hidden="true"></label>
The use of the hidden
and aria-hidden=”true”
attributes tells all user agents to ignore those elements. To make them not be ignored across all screen media UA’s we will need to set them to show via a display state in the CSS.
使用hidden
和aria-hidden=”true”
属性告诉所有用户代理忽略这些元素。 为了使它们不会在所有屏幕媒体UA上被忽略,我们需要将它们设置为通过CSS中的显示状态显示。
Remember that in production all your CSS should be included via <link> with a media=””
appropriate to the device you’re targeting. In our case “screen”. If you see a stylesheet <link>
or <style>
that lacks media=”something” or is set to media=”all” you’re looking at those pesky 3i of web development: ignorance, incompetence, and ineptitude. Yes bootcrap, tailwind, and the rest, I’m talking to YOU!
请记住,在生产中,应通过<link>使用适合于您要定位的设备的media=””
来包含所有CSS。 在我们的例子中是“屏幕”。 如果看到样式表<link>
或<style>
缺少media =“ something ”或设置为media =“ all”,则说明Web开发的那些烦人的3i:无知,无能和无能。 是的,废话,顺风,其余的,我在跟你说话!
CSS —更神奇 (CSS — Even More Magical)
.toggle_collapse {
/* make it show to work across all ua's */
display:block;
}.toggle_collapse, /* hide off screen */
.toggle_collapse + * > .collapseAfter ~ * {
position:absolute;
top:-999em;
left:-999em;
}.toggle_collapse:checked + * .collapseAfter ~ * {
position:static;
}.toggle_collapse + * + label {
display:inline-block; /* make visible for screen media */
color:blue;
}.toggle_collapse + * + label:before {
content:"See More";
}.toggle_collapse:checked + * + label:before {
content:"See Less";
}.toggle_collapse + * + label:after {
content:"\25BC";
display:inline-block;
padding:0.4em 0 0 0.3em;
vertical-align:top;
font-size:0.625em;
}.toggle_collapse:checked + * + label:after {
content:"\25B2";
}
Shockingly similar to our scripted version. First we have to make our “hidden” elements show so that they’re properly keyboard navigable on screen media, but we still want them hidden so we move them off screen. The code to hide them off screen is shared with our hidden state since both need to do the same thing.
令人震惊地类似于我们的脚本版本。 首先,我们必须显示“隐藏”元素,以便它们可以在屏幕媒体上正确地在键盘上导航,但是我们仍然希望它们隐藏起来,以便将它们移出屏幕。 将它们隐藏在屏幕外的代码与我们的隐藏状态共享,因为两者都需要做相同的事情。
The selector for the hidden state is way more complex:
隐藏状态的选择器更加复杂:
.toggle_collapse + * > .collapseAfter ~ * {
Basically after whatever tag comes immediately after our INPUT.toggleCollapse
we look for a collapseAfter child, and hide all the sibling tags after it. Again, some people will get their panties in a twist over the use of the any element selector, and in doing so are showing little more than ignorance.
基本上,在INPUT.toggleCollapse
之后紧随其后的任何标签之后,我们INPUT.toggleCollapse
寻找一个INPUT.toggleCollapse
子项,并将所有同级标签隐藏在它之后。 同样,有些人会因为使用any元素选择器而感到内裤扭曲,这样做的确显示了无知。
As the checked state will be our closed state, we end up having to swap the closed/open text and arrows… but for the most part the CSS works in a nearly identical fashion to the scripted version.
由于检查状态将是我们的关闭状态,因此最终不得不交换关闭/打开的文本和箭头……但是在大多数情况下,CSS的工作方式几乎与脚本版本相同。
Live Demo
现场演示
非脚本版本的优缺点(Pros and Cons of the Non-Scripted Version)
You have to weigh the various issues with this way of doing it as well.
您还必须通过这种方式权衡各种问题。
Disadvantages
缺点
- Way more markup方式更多标记
- Confusing markup for those unfamiliar with abusing hidden input this way对于不熟悉这种隐藏输入滥用方式的人来说,它们的标记令人困惑
- Older UA’s that don’t know “hidden” or “aria-hidden” will get hung up or confuse the user with that INPUT tag when CSS is not applied.当不应用CSS时,不知道“隐藏”或“隐藏”的较旧的UA将被挂断或使用户混淆该INPUT标签。
Advantages
好处
- No JavaScript, and even works for screen media users who visit with scripting blocked/disabled/unavailable!没有JavaScript,甚至适用于使用脚本阻止/禁用/不可用访问的屏幕媒体用户!
- In modern UA’s the offending extra elements don’t exist for screen readers, braille readers, search engines, or other such non-visual UA’s 在现代UA中,对于屏幕阅读器,盲文阅读器,搜索引擎或其他此类非可视UA而言,不存在令人讨厌的额外元素
keyboard navigable to the input (though some extra CSS should likely be added to make that apparent)
键盘可导航至输入(尽管应该添加一些额外CSS以使其清晰可见)
结论 (Conclusion)
Both of these approaches have merits. The scripted version is really nice because all you have to do is make sure there’s a common wrapper and apply the class where you want the break/collapse to occur. The non-scripted one works so long as you’re on a screen media device. Both gracefully degrade to non-visual and non-scripted usability and accessibility, as every effort was put towards dotting the t’s and crossing the i’s… or… uhm… something.
这两种方法都有优点。 脚本版本非常好,因为您要做的就是确保有一个通用包装器,并在您希望中断/折叠发生的位置应用该类。 只要您使用的是屏幕媒体设备,无脚本脚本就可以正常工作。 两者都优雅地降级为非可视和非脚本的可用性和可访问性,因为所有努力都致力于加点t并越过i或…等等。
It’s truly amazing what CSS lets us do nowadays, but there are still good reasons to use JavaScript too, so long as you “keep it in your pants” and let CSS do most of the “real work”. Of the two methods I’d actually suggest the CSS approach for people who know what they’re doing, and the JavaScript one for beginners or those who just like the convenience of how it only takes that one class to apply it. Honestly the decision to which I’d use would likely be made on a case by case basis.
如今,CSS让我们做的事真是令人惊讶,但只要您“将其保留在裤子里”并让CSS来完成大部分“实际工作”,仍然有充分的理由使用JavaScript。 在这两种方法中,我实际上会向那些知道自己在做什么的人建议使用CSS方法,而JavaScript是针对初学者或那些只喜欢使用一个类来应用它的便利性的人CSS方法。 老实说,我会根据具体情况做出决定。
翻译自: https://medium.com/@deathshadow/collapsible-sections-with-or-without-javascript-3fd871955a9d