最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Swap two <li> elements - Stack Overflow

programmeradmin1浏览0评论

I have a page with an unordered list in two columns. After I click on one, it enlarges so there's only one column for it and there is a space on a row where it was so I wanted to move the next li before it, so there wouldn't be that space.

The picture shows divs before click (they are not empty, I just erased the content for this purpose), how it changes after click on the div with li index 1 and how I would like to swap the li with indexes 1 and 2.

I found some solutions but nothing has worked for me. I ended up with:

function swap(n) {
    var l = n.ancestor("li.msg-box-wrapper");
    var m = n.ancestor("#doubleColumnList").all("li.msg-box-wrapper");        
    var k = m.indexOf(n.ancestor("li.msg-box-wrapper")); 
    if ((k%2 != 0)) {
        $(l).before( $(l).next() );
    }
}

I have a page with an unordered list in two columns. After I click on one, it enlarges so there's only one column for it and there is a space on a row where it was so I wanted to move the next li before it, so there wouldn't be that space.

The picture shows divs before click (they are not empty, I just erased the content for this purpose), how it changes after click on the div with li index 1 and how I would like to swap the li with indexes 1 and 2.

I found some solutions but nothing has worked for me. I ended up with:

function swap(n) {
    var l = n.ancestor("li.msg-box-wrapper");
    var m = n.ancestor("#doubleColumnList").all("li.msg-box-wrapper");        
    var k = m.indexOf(n.ancestor("li.msg-box-wrapper")); 
    if ((k%2 != 0)) {
        $(l).before( $(l).next() );
    }
}
Share Improve this question edited Sep 9, 2015 at 8:02 Boann 50k16 gold badges124 silver badges152 bronze badges asked Sep 8, 2015 at 9:05 Mish.k.aMish.k.a 2994 silver badges20 bronze badges 3
  • I have tried a solution. Hope it will solve your problem. check jsfiddle – Abhisek Malakar Commented Sep 8, 2015 at 9:54
  • I can't help thinking that this is just a bad idea, and is going to end up confusing your users. I wouldn't expect the page to be drastically re-ordered just because an element increased in size. – Anthony Grist Commented Sep 8, 2015 at 10:13
  • There are supposed to be two columns and when there's half of the other one empty it just lookes weird. – Mish.k.a Commented Sep 8, 2015 at 11:27
Add a comment  | 

4 Answers 4

Reset to default 12

The trick to get this to work, is realising that the new position of the active element should be the "first-in-line" from it's current position.

To find an element that is first-in-line simply look for an element that is either:

  • the first child, or
  • has an left-offset smaller than it's previous sibling (in a ltr-context at least). (candidate.offset().left < candidate.prev().offset().left)

So the following will work:

  • on activation (click) note the current position, and

    • find the next element that is first-in-line (including the clicked element).
    • swap these two elements
  • on de-activation simply move every active element back to it's original position.


For ease-of-use I've rewritten my original answer as a jquery plugin. As I couldn't find a good name, it's currently called foobar.

usage:

// '.wrapper' is the element containing the *toggle-able* elements.
$('.wrapper').foobar({
  // the element-selector
  elements: 'li',

  // the toggle-selector (if a *deeper* element should be used to toggle state)
  triggerOn: '.toggle',

  // indicates an active element
  activeClass: 'active',

  // get's called on activation [optional]
  onActivate: function ($el) {
    console.log('activating', $el);
  },

  // get's called on de-activation [optional]
  onDeactivate: function ($el) {
    console.log('de-activating', $el);
  }
});

the plugin:

(function ($, pluginName) {
  'use strict';

  /**
   * Plugin behavior
   */
  $.fn[pluginName] = function (options) {
    var settings = $.extend(true, {}, $.fn[pluginName].defaults, options);

    // triggerOn-selector is required
    if (null === settings.triggerOn) {
      throw 'the `triggerOn` must be set.';
    }

    // without an element-selector
    if (null === settings.elements) {
      // use triggerOn-selector as default
      settings.elements = settings.triggerOn;
    }

    // apply behavior to each element in the selection
    return this.each(function() {
        var
          $wrapper = $(this),
          $elements = $wrapper.find(settings.elements)
        ;

        $wrapper.on(settings.event, settings.triggerOn, function () {
          var
            $el = $(this).closest(options.elements),
            isActive = $el.hasClass(settings.activeClass)
          ;

          reset($elements, settings.activeClass, settings.onDeactivate);

          if (!isActive) {
            activate($el, $elements, settings.activeClass, settings.onActivate);
          }
        });
    });
  };

  /**
   * Plugin defaults
   */
  $.fn[pluginName].defaults = {
    // required
    triggerOn: null,

    // defaults
    elements: null,
    event: 'click',
    activeClass: 'active',
    onActivate: function () {},
    onDeactivate: function () {}
  };

  /**
   * Reset all currently active elements
   *
   * @param {jQuery}   $elements
   * @param {String}   activeIndicator
   * @param {Function} onDeactivate
   */
  function reset($elements, activeIndicator, onDeactivate)
  {
    $elements
      .filter(function () {
        return $(this).hasClass(activeIndicator);
      })
      .each(function () {
        deactivate($(this), $elements, activeIndicator, onDeactivate);
      })
    ;
  }

  /**
   * Deactivate the given element by moving it back to it's original position and removing the active-indicator.
   *
   * @param {jQuery}   $el
   * @param {jQuery}   $elements
   * @param {String}   activeIndicator
   * @param {Function} onDeactivate
   */
  function deactivate($el, $elements, activeIndicator, onDeactivate)
  {
    var originalIndex = $el.index();

    $el.removeClass(activeIndicator).insertBefore(
      $elements.eq(originalIndex)
    );

    onDeactivate($el);
  }

  /**
   * Activate the given element by moving it to a suitable position while applying the required indicator.
   *
   * @param {jQuery}   $el
   * @param {jQuery}   $elements
   * @param {String}   activeIndicator
   * @param {Function} onActivate
   */
  function activate($el, $elements, activeIndicator, onActivate)
  {
    $el
      .insertAfter(
        $elements.eq(findSuitablePosition($elements, $el.index()))
      )
      .addClass(activeIndicator)
    ;

    onActivate($el);
  }

  /**
   * @param {jQuery} $elements
   * @param {Number} originalIndex
   */
  function findSuitablePosition($elements, originalIndex)
  {
    // short-circuit simple case
    if (0 === originalIndex) {
      return originalIndex;
    }

    var
      candidateIndex = originalIndex,
      lim = $elements.length,
      $candidate
    ;

    for (; candidateIndex < lim; candidateIndex += 1) {
      $candidate = $elements.eq(candidateIndex);

      if ($candidate.offset().left < $candidate.prev().offset().left) {
        return candidateIndex;
      }
    }

    throw 'could not find a suitable position.';
  }
})(jQuery, 'foobar');

Demo: http://plnkr.co/edit/8ARXgq2pLSzm9aqHI8HL?p=preview


original answer:

The following will work, if you're willing to use jQuery.

It's a bit more complicated than need be, but this way it works for more than two columns as well. Note the code style is so that it can be easily followed.

$('.wrapper').each(function () {
  var $wrapper = $(this);

  $wrapper.on('click', 'li', function () {
    var
      $el = $(this),
      isOpen = $el.is('.open')
    ;

    reset();

    if (!isOpen) {
      open($el);
    }
  });

  function open($el)
  {
    var originalIndex = $el.index();

    // note index and move to suitable position
    $el
      .data('original-index', originalIndex)
      .insertAfter(
        $wrapper.find('li').eq(findSuitablePosition(originalIndex))
      )
      .addClass('open')
    ;
  }

  function reset()
  {
    $wrapper.find('.open').each(function () {
      var
        $el = $(this),
        originalIndex = $el.data('original-index')
      ;

      $el.removeClass('open').insertBefore(
        $wrapper.find('li').eq(originalIndex)
      );
    });
  }

  function findSuitablePosition(originalIndex)
  {
    // short-circuit simple case
    if (0 === originalIndex) {
      return originalIndex;
    }

    var
      $candidates = $wrapper.find('li'),
      candidateIndex = originalIndex,
      lim = $candidates.length,
      candidate
    ;

    for (; candidateIndex < lim; candidateIndex += 1) {
      candidate = $candidates.eq(candidateIndex);

      if (candidate.offset().left < candidate.prev().offset().left) {
        return candidateIndex;
      }
    }

    throw 'could not find a suitable position.';
  }
});
ul {
  list-style: none;
  margin: 0;
  padding: 5px 10px;
  width: 300px;
  border: 1px solid #ccc;
  overflow: hidden;
  font-family: sans-serif;
  margin-bottom: 5px;
}

li {
  float: left;
  margin: 10px 5px;
  padding: 3px;
  border: 1px solid #ccc;
  box-sizing: border-box;
}

ul li.open {
  width: calc(100% - 10px);
  height: 40px;
  border-color: green;
}

.two li {
  width: calc(50% - 10px);
}

.three li {
  width: calc(33% - 10px);
}

.four li {
  width: calc(25% - 10px);
}
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<ul class="wrapper two">
  <li>0</li>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
  <li>6</li>
</ul>

<ul class="wrapper three">
  <li>0</li>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
  <li>6</li>
  <li>7</li>
  <li>8</li>
  <li>9</li>
</ul>

<ul class="wrapper four">
  <li>0</li>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
  <li>6</li>
  <li>7</li>
  <li>8</li>
  <li>9</li>
  <li>10</li>
  <li>11</li>
  <li>12</li>
</ul>

In case you wanted an answer that doesn't use jQuery here is one such solution:

var list = document.getElementById('list');
var listItems = list.children;

function select(e) {
  // Remove the selected class from the previously selected list item
  var selectedEl = document.querySelector('.selected');
  if (selectedEl) {
    selectedEl.classList.remove('selected');
  }

  // Add the selected class to the current list item
  var targetEl = e.target;
  targetEl.classList.add('selected');

  // Find the current li's position in the node list
  var targetPosition = Array.prototype.indexOf.call(listItems, targetEl);

  // If it is in an odd position, and there is a sibling after it
  // move that sibling before it
  if (targetPosition % 2 > 0 && targetEl.nextElementSibling) {
    list.insertBefore(targetEl.nextElementSibling, targetEl);
  }
}

// Add click listeners
for(var i = 0, len = listItems.length; i < len; i++) {
  listItems[i].addEventListener('click', select);
}
ul {
  position: relative;
  width: 400px;
}

li {
  text-align: center;
  float: left;
  display: block;
  height: 20px;
  width: 190px;
  margin: 5px;
  background: red;
}

.selected {
  width: 390px;
}
<ul id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
</ul>

The crux of the solution is the call to Node.insertBefore using the clicked list item as the reference node.

Codepen version

You can easily do that with jquery.

I made you a JSFiddle here

The idea is to define to classes, one for the small rects and on for the big rect. Than onclick of one of the small, set all to smalls and the clicked on to big. Than if neceserely, reorder the list.

HTML

<ul class="container">
 <li class="small item">1</div>
 <li class="small item">2</div>
 <li class="small item">3</div>
 <li class="small item">4</div>
 <li class="small item">5</div>
 <li class="small item">6</div>
</ul>

CSS

.container{
width: 130px;
list-style-type: none;
}
.small{
margin: 5px;
width: 50px;
height: 20px;
background-color: red;
float: left;
}
.big{
margin: 5px;
width: 110px;
height: 60px;
background-color: red;
float: left;
}

Jquery:

$(function(){

//ON LI CLICKED
$(".small").click(function() {

    //LI REMOVE BIG ONE
    $( ".item" ).removeClass( "big" ).addClass( "small" );

    //ADD BIG CLASS
    $(this).removeClass( "small" ).addClass( "big" );

    //IF NECESSARY THAN REORDER
    if($( "li" ).index( $(this) )%2==1){
        alert("reorder");
        //CHANGE INDEX OF LI
        $(this).siblings().eq(1).after(this);
    }
});

});

you can check whether the clicked element is on odd position and then insert the next element before it. check it -

$('.box').on('click', function() {
    if($(this).index()%2 != 0) {
        $(this).next().insertBefore($(this));
    }
    $('.box').removeClass('fullwidth');
    $(this).addClass('fullwidth');
});

Working Demo

UPDATE: Added a sorter() function to sort all the div in their original places.

$('.box').each(function(i) {
    $(this).attr('i', i);
});
function sorter() {
    for(var x=0; x<$('.box').length; x++) {
        $('.box').each(function() {
            if($(this).attr('i') > $(this).next().attr('i')) {
                $(this).insertAfter($(this).next());
            }
        });
    }
}

call it first whenever .box is clicked.

UPDATED FIDDLE

发布评论

评论列表(0)

  1. 暂无评论