Skip to content

Commit

Permalink
feat(manipulation): Add wrapInner
Browse files Browse the repository at this point in the history
Port of @tomjw64 & @warrengm's great work to the current codebase

Fixes #1335
Fixes #717
Fixes #572

Co-Authored-By: Thomas Williamson <1285308+tomjw64@users.noreply.github.com>
Co-Authored-By: Warren Maresca <5326842+warrengm@users.noreply.github.com>
  • Loading branch information
3 people committed Sep 10, 2020
1 parent 32c5466 commit 9ffc557
Show file tree
Hide file tree
Showing 2 changed files with 238 additions and 0 deletions.
86 changes: 86 additions & 0 deletions lib/api/manipulation.js
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,92 @@ exports.wrap = function (wrapper) {
return this;
};

/**
* The .wrapInner() function can take any string or object that could be passed to
* the $() factory function to specify a DOM structure. This structure may be
* nested several levels deep, but should contain only one inmost element. The
* structure will be wrapped around the content of each of the elements in the set
* of matched elements.
*
* @param {cheerio} wrapper - The DOM structure to wrap around the content of each element in the selection.
*
* @example
*
* const redFruit = $('<div class="red-fruit"></div>')
* $('.apple').wrapInner(redFruit)
*
* //=> <ul id="fruits">
* // <li class="apple">
* // <div class="red-fruit">Apple</div>
* // </li>
* // <li class="orange">Orange</li>
* // <li class="pear">Pear</li>
* // </ul>
*
* const healthy = $('<div class="healthy"></div>')
* $('li').wrapInner(healthy)
*
* //=> <ul id="fruits">
* // <li class="apple">
* // <div class="healthy">Apple</div>
* // </li>
* // <li class="orange">
* // <div class="healthy">Orange</div>
* // </li>
* // <li class="pear">
* // <div class="healthy">Pear</div>
* // </li>
* // </ul>
*
* @see {@link http://api.jquery.com/wrapInner/}
*/
exports.wrapInner = function (wrapper) {
var wrapperFn = typeof wrapper === 'function' && wrapper,
lastIdx = this.length - 1;

_.forEach(
this,
_.bind(function (el, i) {
var children = el.children,
wrapperDom,
elInsertLocation,
j;

if (wrapperFn) {
wrapper = wrapperFn.call(el, i);
}

if (typeof wrapper === 'string' && !isHtml(wrapper)) {
wrapper = this.parents().last().find(wrapper).clone();
}

wrapperDom = this._makeDomArray(wrapper, i < lastIdx).slice(0, 1);
elInsertLocation = wrapperDom[0];
// Find the deepest child. Only consider the first tag child of each node
// (ignore text); stop if no children are found.
j = 0;

while (elInsertLocation && elInsertLocation.children) {
if (j >= elInsertLocation.children.length) {
break;
}

if (elInsertLocation.children[j].type === 'tag') {
elInsertLocation = elInsertLocation.children[j];
j = 0;
} else {
j++;
}
}

updateDOM(children, elInsertLocation);
updateDOM(wrapperDom, el);
}, this)
);

return this;
};

/**
* Insert content next to each element in the set of matched elements.
*
Expand Down
152 changes: 152 additions & 0 deletions test/api/manipulation.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,158 @@ describe('$(...)', function () {
});
});

describe('.wrapInner', function () {
it('(Cheerio object) : should insert the element and add selected element(s) as its parent', function () {
var $container = $('<div class="container"></div>');
$fruits.wrapInner($container);

expect($fruits.children()[0]).to.be($container[0]);
expect($container[0].parent).to.be($fruits[0]);
expect($container[0].children[0]).to.be($('.apple')[0]);
expect($container[0].children[1]).to.be($('.orange')[0]);
expect($('.apple')[0].parent).to.be($container[0]);
expect($fruits.children()).to.have.length(1);
expect($container.children()).to.have.length(3);
});

it('(element) : should insert the element and add selected element(s) as its parent', function () {
var $container = $('<div class="container"></div>');
$fruits.wrapInner($container[0]);

expect($fruits.children()[0]).to.be($container[0]);
expect($container[0].parent).to.be($fruits[0]);
expect($container[0].children[0]).to.be($('.apple')[0]);
expect($container[0].children[1]).to.be($('.orange')[0]);
expect($('.apple')[0].parent).to.be($container[0]);
expect($fruits.children()).to.have.length(1);
expect($container.children()).to.have.length(3);
});

it('(html) : should insert the element and add selected element(s) as its parent', function () {
$fruits.wrapInner('<div class="container"></div>');

expect($fruits.children()[0]).to.be($('.container')[0]);
expect($('.container')[0].parent).to.be($fruits[0]);
expect($('.container')[0].children[0]).to.be($('.apple')[0]);
expect($('.container')[0].children[1]).to.be($('.orange')[0]);
expect($('.apple')[0].parent).to.be($('.container')[0]);
expect($fruits.children()).to.have.length(1);
expect($('.container').children()).to.have.length(3);
});

it("(selector) : should wrap the html of the element with the selector's first match", function () {
var $oranges;
$('.apple').wrapInner('.orange, .pear');
$oranges = $('.orange');
expect($('.pear')).to.have.length(1);
expect($oranges).to.have.length(2);
expect($oranges.eq(0).parent()[0]).to.be($('.apple')[0]);
expect($oranges.eq(0).text()).to.be('Apple');
expect($('.apple').eq(0).children()[0]).to.be($oranges[0]);
expect($oranges.eq(1).parent()[0]).to.be($fruits[0]);
expect($oranges.eq(1).text()).to.be('Orange');
});

it('(fn) : should invoke the provided function with the correct arguments and context', function () {
var $children = $fruits.children();
var args = [];
var thisValues = [];

$children.wrapInner(function () {
args.push(toArray(arguments));
thisValues.push(this);
});

expect(args).to.eql([[0], [1], [2]]);
expect(thisValues).to.eql([$children[0], $children[1], $children[2]]);
});

it("(fn) : should use the returned HTML to wrap each element's contents", function () {
var $children = $fruits.children();
var tagNames = ['div', 'span', 'p'];

$children.wrapInner(function () {
return '<' + tagNames.shift() + '>';
});

expect($fruits.find('div')).to.have.length(1);
expect($fruits.find('div')[0]).to.be($('.apple').children()[0]);
expect($fruits.find('.apple')).to.have.length(1);

expect($fruits.find('span')).to.have.length(1);
expect($fruits.find('span')[0]).to.be($('.orange').children()[0]);
expect($fruits.find('.orange')).to.have.length(1);

expect($fruits.find('p')).to.have.length(1);
expect($fruits.find('p')[0]).to.be($('.pear').children()[0]);
expect($fruits.find('.pear')).to.have.length(1);
});

it("(fn) : should use the returned Cheerio object to wrap each element's contents", function () {
var $children = $fruits.children();
var tags = [$('<div></div>'), $('<span></span>'), $('<p></p>')];

$children.wrapInner(function () {
return tags.shift();
});

expect($fruits.find('div')).to.have.length(1);
expect($fruits.find('div')[0]).to.be($('.apple').children()[0]);
expect($fruits.find('.apple')).to.have.length(1);

expect($fruits.find('span')).to.have.length(1);
expect($fruits.find('span')[0]).to.be($('.orange').children()[0]);
expect($fruits.find('.orange')).to.have.length(1);

expect($fruits.find('p')).to.have.length(1);
expect($fruits.find('p')[0]).to.be($('.pear').children()[0]);
expect($fruits.find('.pear')).to.have.length(1);
});

it('($(...)) : for each element it should add a wrapper elment and add the selected element as its child', function () {
var $fruitDecorator = $('<div class="fruit-decorator"></div>');
var $children = $fruits.children();
$('li').wrapInner($fruitDecorator);

expect($('.fruit-decorator')).to.have.length(3);
expect(
$children.eq(0).children().eq(0).hasClass('fruit-decorator')
).to.be.ok();
expect($children.eq(0).hasClass('apple')).to.be.ok();
expect(
$children.eq(1).children().eq(0).hasClass('fruit-decorator')
).to.be.ok();
expect($children.eq(1).hasClass('orange')).to.be.ok();
expect(
$children.eq(2).children().eq(0).hasClass('fruit-decorator')
).to.be.ok();
expect($children.eq(2).hasClass('pear')).to.be.ok();
});

it('(html) : wraps with nested elements', function () {
var $badOrangeJoke = $(
'<div class="orange-you-glad"><div class="i-didnt-say-apple"></div></div>'
);
$('.orange').wrapInner($badOrangeJoke);

expect(
$('.orange').children().eq(0).hasClass('orange-you-glad')
).to.be.ok();
expect(
$('.orange-you-glad').children().eq(0).hasClass('i-didnt-say-apple')
).to.be.ok();
expect($fruits.children().eq(2).hasClass('pear')).to.be.ok();
expect($('.orange-you-glad').children()).to.have.length(1);
});

it('(html) : should only worry about the first tag children', function () {
var delicious = '<span> This guy is delicious: <b></b></span>';
$('.apple').wrapInner(delicious);
expect($('.apple>span>b')).to.have.length(1);
expect($('.apple>span>b').text()).to.equal('Apple');
});
});

describe('.append', function () {
it('() : should do nothing', function () {
expect($('#fruits').append()[0].tagName).to.equal('ul');
Expand Down

0 comments on commit 9ffc557

Please sign in to comment.