JS to unwrap children - Need to imitate display:contents;


#21

Yes you should be looking in Firefox as I mentioned in bold :slight_smile:


#22

Yup, fair play. I don't have IE11, so can't really test. But It doesn't surprise me that you have a nice solution and then it breaks in IE. If IE support is a requirement, then your approach will probably be be better.

Lol. It's been a long day. I get it now.


#23

Yup, CSS.supports() will be the problem

https://caniuse.com/#feat=css-supports-api


#24

Ok thanks at least we know there is a workaround albeit more convoluted.

I think you've given Ray enough to work with now so perhaps wait to hear from him (he's on a different timezone to us) before we go too mad with it :slight_smile:


#25

I'm here, just getting caught up on the posts


#26

Hi Ray,

This is a revised version for IE11. Just try this one and see if it does what you need.

<!doctype html>
<html>
  <head>
    <meta charset='utf-8'>
    <title>JavaScript unwrap children</title>
    <style>
      .wrap {
        width:90%;
        max-width:1000px;
        margin:auto;
        padding:20px;
        outline:1px solid red;
        box-sizing: border-box;
      }
      .sidebar {
        margin:10px 0;
        padding:40px;
        background:lime;
      }
      .child {
        background:#eee;
        margin:10px 0;
        padding:10px;
      }

      @media all and (max-width: 760px) {
        .sidebar {
          display:contents;
        }
      }

      /* check for display:contents support */
      detect {
        display:none
      }

      @supports ( display: contents ) {
        detect {
          display:block;
        }
      }
    </style>
  </head>

  <body>
    <div class="wrap">
      <h2>JavaScript unwrap children</h2>
      <p>Remove green parent div and leave child divs in page.</p>
      <p>A JavaScript substitute for <code>display:contents;</code> in all browsers.<br>How can this script be run with a media query or based on screen widths?</p>
      <p><b>Reduce Browser Window to kick in media query</b><br> Firefox 56+ will use <code>display:contents;</code>.</p>
      <div class="sidebar">
        <div class="child">child div to remain in page</div>
        <div class="child">child div to remain in page</div>
        <div class="child">child div to remain in page</div>
      </div>
      <p>More content..</p>
    </div>

    <script>
      function unwrap(el){
        var docFrag = document.createDocumentFragment();
        while (el.firstChild) {
          var child = el.removeChild(el.firstChild);
          docFrag.appendChild(child);
        }

        el.parentNode.replaceChild(docFrag, el);
      }

      function handleViewportChange(mql, els) {
        if (mql.matches) {
          console.log('Viewport smaller than 761px');
          sidebarParent.innerHTML = sidebarParentAltered;
        } else {
          console.log('Viewport larger than 761px');
          sidebarParent.innerHTML = sidebarParentNormal;
        }
      }

      function doesntSupportDisplayContents(){
        var newEl = document.createElement('detect');
        document.body.appendChild(newEl);
        return getComputedStyle(newEl).getPropertyValue('display') === 'none';
      }

      if (doesntSupportDisplayContents()){
        var sidebar = document.querySelector('.sidebar');
        var sidebarParent = sidebar.parentNode;

        var sidebarParentNormal = sidebarParent.innerHTML;
        unwrap(sidebar);
        var sidebarParentAltered = sidebarParent.innerHTML;

        var mql = window.matchMedia('screen and (max-width: 760px)');
        mql.addListener(handleViewportChange);
        handleViewportChange(mql);
      }
    </script>
  </body>
</html>

#27

Yes that is working for me in IE11 - windows 7


#28

Lovely :slight_smile:

I'll let you read through the rest of the thread. This is by no means a definitive solution, but let me know if you have any questions.


#29

Just so I understand correctly, is the use of .innerHTML okay in this case.

I thought it was hard on the DOM, of course I may not be understanding what it's doing in this case.

      function handleViewportChange(mql, els) {
        if (mql.matches) {
          console.log('Viewport smaller than 761px');
          sidebarParent.innerHTML = sidebarParentAltered;
        } else {
          console.log('Viewport larger than 761px');
          sidebarParent.innerHTML = sidebarParentNormal;
        }
      }

      if (doesntSupportDisplayContents()){
        var sidebar = document.querySelector('.sidebar');
        var sidebarParent = sidebar.parentNode;

        var sidebarParentNormal = sidebarParent.innerHTML;
        unwrap(sidebar);
        var sidebarParentAltered = sidebarParent.innerHTML;

        var mql = window.matchMedia('screen and (max-width: 760px)');
        mql.addListener(handleViewportChange);
        handleViewportChange(mql);
      }

In Paul's example a cloneNode was created and restated later.

 //first save html so we can re-instate it
var oldHtml = document.querySelector('.sidebar').cloneNode(true);

#30

Yeah, if sidebarParent has thousands of divs, tables, lists, images, etc, then it's probably not the best idea. However, I pretty much suspect this isn't the case. Performance wise, I would say no worries.

However, the second point in the first answer on that SO thread is more valid:

This could also break references to already constructed DOM elements

So if you have other JS stuff going on in sidebarParent (other event handlers etc), then it's better to use Paul's method of cloning and re-inserting elements.


#31

The sidebar will have my nav menu at the top. Below that will be smaller pieces of aside content. Nothing massive.

On desktop the menu will be a vertical menu at the top of the sidebar, on mobiles it will turn into a toggle menu below the header. That was the main reason for needing to break the children out of the sidebar div.

There will be a script to toggle the menu for mobiles.


#32

If the script is attached to sidebarParent, it'll still work fine. Maybe have a go at getting things working to your liking then let us know which method you go with.


#33

Hi Pullo,

I've got things a little further along now and ran into some problems. Hopefully it's just a snag and not a major ordeal.

Here is a link to the page template in progress (test.html).

And a zip file below to pull down all files directly.
js-test.zip (4.6 KB)

I've got the nav menu in place now and media queries are swapping it out from the sidebar for desktops. Then below the header as a toggle menu for mobiles.

The page works fine in Firefox since it is using display:contents instead of the unwrap.js
Firefox's behavior serves as an example of what all browsers need to do.

Browsers that use the script (Chrome and IE) require a page reload after the queries kick in, in order for the menu to toggle. It looks like the page reload is needed to rebuild the DOM.

I think that's the problem. The menu toggle script is tied to the <ul class="menu">

It is nested in <nav class="top">, the first child of <div class="sidebar">

The toggle menu script setting the ul.menu to display:none; is what I think causes sidebarParent to lose track of it's .innerHTML . Hence the need for the page reload to build the DOM again.

I appreciate your help, do you think we can get this working without a page reload?

<div class="sidebar">
   <nav class="top">
      <button class="menuToggle">Menu</button>
      <ul class="menu">
         <li><a href="#">Link 1</a></li>
         <li><a href="#">Link 2</a></li>
         <li><a href="#">Link 3</a></li>
         <li><a href="#">Link 4</a></li>
      </ul>
   </nav>
   <div class="mid">
      <p>Mid div</p>
      <p>Mid div</p>
   </div>
   <div class="bot">
      <p>Bottom div</p>
      <p>Bottom div</p>
   </div>
</div>

Even though the html above is a little further along it still carries this same basic structure we've been dealing with.

<div class="sidebar">
  <div class="child">child div to remain in page</div>
  <div class="child">child div to remain in page</div>
  <div class="child">child div to remain in page</div>
</div>

#34

Here is a screenshot gif of the page in Chrome.
Maybe this will help with everything I tried to explain above.


Help needed understanding statements in function
#35

If I understand, this is something most visitors will not encounter but is something you noticed while testing.

But it sounds like you may want to use a throttled resize event.


#36

I’ll have a look later today but I’m guessing the answer is that you should have cloned the html as in my example which also copies all event handlers as well. Innerhtml does not do this as it just copies the html but not any event handlers applied to the code.

If you want to keep using innerhtml then you will need to bind the event handlers using the match media each time. There was an example of this on the toggle menu codepen I showed in the thread.

I’ll be back at my computer later in the day and will take a look if @pullo doesn’t beat me to it :slight_smile:


#37

Just moving your clickhandler to here makes it all work.

function handleViewportChange(mql, els) {
   if (mql.matches) {
    console.log('Viewport smaller than 651px');
    sidebarParent.innerHTML = sidebarParentAltered;
	function toggleMenuClickHandler(evt) {
      let menu = document.querySelector('.menu ');
      menu.classList.toggle("toggle");
   }

   let menuToggle = document.querySelector('.menuToggle');
   menuToggle.addEventListener("click", toggleMenuClickHandler);
   } else {
    console.log('Viewport larger than 651px');
    sidebarParent.innerHTML = sidebarParentNormal;
   }
}

However wait for @Pullo to confirm as you may need to unbind the handler for the larger screen.


#38

Morning,

That'll be it.

Meh. Not the biggest fan of binding and rebinding stuff.

IMO, there are two solutions here:

  1. Use Paul's original solution (which despite his claims to the contrary is pretty solid :slight_smile: )
  2. Attach the event listener to an element that will always be present in the DOM and delegate.

The second solution would be fairly easy to implement, but if you find yourself doing this more than once or twice, then Paul's solution is better.

Anyway, just swap out this:

function toggleMenuClickHandler(evt) {
  let menu = document.querySelector('.menu ');
  menu.classList.toggle("toggle");
}

let menuToggle = document.querySelector('.menuToggle');
menuToggle.addEventListener("click", toggleMenuClickHandler);

For this:

function toggleMenuClickHandler(evt) {
  var clickedEl = evt.target;
  if(clickedEl.className === 'menuToggle') {
    clickedEl.nextElementSibling.classList.toggle("toggle");
  }
}

var inner = document.querySelector('.inner');
inner.addEventListener("click", toggleMenuClickHandler);

And things will work as expected.

HTH


Help needed understanding statements in function
#39

These two actions will only happen once, and they happen simultaneously...

  1. Remove the sidebar div and retain the children
  2. Menu turns into a toggle menu

Yes that works fine, all good in IE11 too.
Do you think it's fine to stay with this code now.

As I mentioned in my first thread, I'm not well versed with JS. I'm just looking for advice as to which way would be better and or cause the least problems.

It's not that I wanted to keep using innerhtml. I just needed a solid working solution for unwrapping the sidebar. I was depending on the experts to point me in the right direction.

So with what I mentioned above about the two actions only happening once. Will it be safe to stay with the innerhtml?

EDIT:
Test page template has been updated and everything is working as expected.

Thanks for everyone's help ! :slight_smile:


#40

Hey Ray,

Yup, I'd say what you have is fine :slight_smile: