Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swap two <li> elements

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() );
    }
}
like image 347
Mish.k.a Avatar asked Sep 08 '15 09:09

Mish.k.a


People also ask

How do you swap elements in two lists?

def swap(A,B,i,j): TEMP_B = B[j] B[j] = A[i] A[i] = TEMP_B return A,B X = np. array([[1.25,3],[1.5,2],[2,2.75],[2.25,2],[2,0.5],[3.25,0.75], [3.5,2.25],[4.25,0.75]]) Y = np. array([[2.75,3.5],[3.25,3],[4.5,2.75],[3.5,4.75]]) X,Y = swap(X,Y,1,1) OUTPUT::: Temp = [ 3.25 3. ]

How do you switch elements in a list Python?

To swap two list elements x and y by value, get the index of their first occurrences using the list. index(x) and list. index(y) methods and assign the result to variables i and j , respectively. Then apply the multiple assignment expression lst[i], lst[j] = lst[j], lst[i] to swap the elements.


2 Answers

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>
like image 158
Yoshi Avatar answered Oct 22 '22 14:10

Yoshi


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

like image 20
nicgordon Avatar answered Oct 22 '22 12:10

nicgordon