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
4 Answers
Reset to default 12The 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