2

I am using Animate Plus for animating accordion. I have multiple definition lists (dl) and I would like to target them all, not just the first one.

Here is the relevant piece of code:

const accordion = {
  element: document.querySelector("dl"),
  translate: 0
}

const buttons = Array.from(
  accordion.element.getElementsByTagName("button"),
  element => ({
    element,
    translate: 0
  })
)

I was trying to convert to an array and use the spread operator […document.querySelectorAll("dl")] but without success.

How can I use querySelectorAll in this specific context to target all dl tags?

Example of my accordion can be found here: https://codepen.io/anon/pen/QYwQqV

Tzar
  • 5,132
  • 4
  • 23
  • 57
  • 2
    Just like you did with `.getElementsByTagName()` and `Array.from()` – Andreas Jan 23 '19 at 09:44
  • @Andreas This the error I got when I tried it: `undefined is not an object (evaluating 'accordion.element.getElementsByTagName')` under `const button`. – Tzar Jan 23 '19 at 10:04
  • This should be part of the question and not only a comment... – Andreas Jan 23 '19 at 10:05
  • 1
    @Andreas I also have tried using your comment as a guide to fix it but wasn't able to, do you think you can modify the codepen to make it work? You'd get my upvote :-) – Alexandre Elshobokshy Jan 23 '19 at 10:06
  • @Andreas What Islam wrote – could you post a working solution as an answer, so I can accept it and upvote it. – Tzar Jan 23 '19 at 10:16
  • the DOM api doesn't have implicit iteration. If you're gonna make accordion.element not an element, you're gonna have to treat it as if it isn't an element (because it isn't one) – Kevin B Jan 23 '19 at 20:45
  • @Tzar, I edited my previous answer to join a working CodePen for all accordions. – jo_va Jan 23 '19 at 21:24

2 Answers2

0

Here is an updated CodePen using Array.from(document.querySelectorAll('dl')) to target every dl in the document:

https://codepen.io/jo_va/pen/OdVpbJ

Since there are now multiple accordions, I first build an array of accordions and I modified all almost functions to take an accordion and a list of buttons as first parameters.

At the end, you just have to iterate over the accordions and execute the same logic you had, but parameterized for the current accordion and its buttons.

const accordions = Array.from(document.querySelectorAll("dl")).map(dl => ({
  element: dl,
  translate: 0
}))

const getButtons = accordion => Array.from(
    accordion.element.getElementsByTagName("button"),
    element => ({
        element,
        translate: 0
    })
)

const timing = {
    easing: "out-quartic",
    duration: 400
}

const clear = element =>
    Object.values(element.attributes).forEach(({ name }) =>
        element.removeAttribute(name)
    )

const hide = async (accordion, buttons, collapsing) => {
    const objects = buttons.filter(({ translate }) => translate)
    const direction = "reverse"
    rotate(collapsing.previousElementSibling.lastElementChild, direction)
    slide(accordion, objects)
    await fold(collapsing, direction)
    clear(collapsing)
}

const show = (accordion, buttons, expanding) => {
    const button = expanding.previousElementSibling.lastElementChild
    const index = buttons.findIndex(({ element }) => element == button)
    const objects = buttons.slice(index + 1)
    const { height } = expanding.getBoundingClientRect()
    expanding.className = "open"
    rotate(button)
    slide(accordion, objects, height)
    fold(expanding)
}

const slide = (accordion, array, to = 0) => {
    center(accordion, to)
    animate({
        ...timing,
        elements: array.map(({ element }) => element.parentElement),
        transform(index) {
            const object = array[index]
            const from = object.translate
            object.translate = to
            return [`translateY(${from}px)`, to]
        }
    })
}

const center = (accordion, height) => {
    const from = accordion.translate
    const to = Math.round(-height / 2)
    accordion.translate = to
    animate({
        ...timing,
        elements: accordion.element,
        transform: [`translateY(${from}px)`, to]
    })
}

const fold = async (content, direction = "normal") =>
    await animate({
        ...timing,
        direction,
        elements: content,
        opacity: [0, 1],
        transform: ["scaleY(0)", 1]
    })

const rotate = ({ lastElementChild: elements }, direction = "normal") =>
    animate({
        elements,
        direction,
        easing: "out-cubic",
        duration: 600,
        transform: ["rotate(0turn)", 0.5]
    })

const toggle = (accordion, buttons) => async ({ target }) => {
    const collapsing = accordion.element.querySelector(".open")
    const expanding = target.parentElement.nextElementSibling
    if (collapsing) await hide(accordion, buttons, collapsing)
    if (collapsing != expanding) show(accordion, buttons, expanding)
}

accordions.forEach(accordion => {
  const buttons = getButtons(accordion)
  buttons.forEach(
    ({ element }) => element.addEventListener("click", toggle(accordion, buttons))
  )
})

import animate from "https://cdn.jsdelivr.net/npm/animateplus@2/animateplus.js"

I hope that I correctly understood your problem and that helps you.

Cheers.

jo_va
  • 13,504
  • 3
  • 23
  • 47
  • I tried it, and for some reason it's not working for me. This is the error I get: `accordion.element.getElementsByTagName is not a function. (In 'accordion.element.getElementsByTagName("button")', 'accordion.element.getElementsByTagName' is undefined)` Could you expand your answer a bit? Thanks. – Tzar Jan 23 '19 at 09:54
  • 1
    @Tzar, it is probably failing because `querySelectorAll` has yielded a collection of list elements, making `accordion.element` an array which doesn't have a `getElementsByTagName` method. Please see my solution, dealing with collecting `button` elements out of an array of `dl`s. – Val Geyvandov Jan 23 '19 at 20:51
  • 1
    @Tzar, I have updated the answer and included a CodePen of the solution, it is working for both accordions. – jo_va Jan 23 '19 at 21:19
  • Hey @jo_va, I found a problem. Transforms don’t get applied to `dl`s now. If you check my CodePen, you’ll see that when you click on a button the whole `dl` moves upwards by the height of `dd`. In your solution it stays fixed. – Tzar Jan 24 '19 at 09:32
  • 1
    Hi @Tzar, I modified the CodePen, it's now working, I will edit the answer. – jo_va Jan 24 '19 at 10:40
  • @Tzar, I will have a look, since you accepted the answer, could you upvote it? Cheers – jo_va Jan 24 '19 at 13:34
  • @jo_va Of course I upvoted it. Somebody downvoted it, and that’s why it’s at 0. – Tzar Jan 24 '19 at 13:36
0

How about this:

// collect all dl elements
const accordion = {
  definitionLists: Array.from(document.querySelectorAll("dl")),
  translate: 0
}

// get all button elements inside of each dl in a single array
const buttons = accordion.definitionLists.reduce((acc, dl) => {
  let currentDlButtons = Array.from(dl.getElementsByTagName("button"), 
   element => ({
     element,
     translate: 0
    }));
  return acc.concat(currentDlButtons);
}, []);
Val Geyvandov
  • 95
  • 1
  • 4