Universal function to create children elements and append to parent nodes - javascript

I want to create and use universal javascript function with which it should be easy to create new children for parent nodes in easy, fast and flexible way.
Look at my code:
<!DOCTYPE html>
<html>
<body>
<style>
div {
border: 2px solid #eeeeee;
background-color: #dff0d8;
}
ol {
background-color: #dff0d8;
}
li {
background-color: #eff0c8;
}
</style>
<script>
function addNewElement(newElementType,parentId) {
var newElement = document.createElement(newElementType);
newElement.innerHTML = 'new element';
parentId.appendChild(newElement);
// actually I want to use just this simple code, what makes this function universal, but it doesn't work..
// while next commented lines work as it should
/**
if (parentId == "someThing"){
someThing.appendChild(newElement);
}
if (parentId == "list"){
list.appendChild(newElement);
}
**/
}
</script>
<p>In next example we can add new child element to this list:</p>
<ol id="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ol>
<button onclick="addNewElement('li','list')">Add new li-element to this list</button>
<p>In next example we can add new child element to this div:</p>
<div id="someThing">Something here</div>
<button onclick="addNewElement('div','someThing')">Add new div-element to this div</button>
</body>
</html>
With parentId.appendChild(newElement) you doesn't get expected result, while it works as it should with specified calls that are shown in /** commented lines **/:
if (parentId == "someThing"){
someThing.appendChild(newElement);
}
if (parentId == "list"){
list.appendChild(newElement);
}
I'm a newbie in JS, so I don't fully understand why I can't use it parentId.appendChild(newElement) to get same results.
I guess it should be simple to make it work even without any jQuery or other libraries.
So I ask you how can I achieve this?

First of all, you shouldn't use the same element ID more than once.
According to W3C:
The id attribute specifies a unique id for an HTML element (the value must be unique within the HTML document).
So I changed your HTML, i.e. removed IDs from buttons and passed required IDs into addNewElement function:
<p>In next example we can add new child element to this list:</p>
<ol id="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ol>
<button onclick="addNewElement('li', 'list')">Add new li-element to this list</button>
<p>In next example we can add new child element to this div:</p>
<div id="someThing">Something here</div>
<button onclick="addNewElement('div', 'someThing')">Add new div-element to this div</button>
Then I updated addNewElement function:
function addNewElement(elementType, parentId) {
let parentElement = document.getElementById(parentId);
let newElement = document.createElement(elementType);
newElement.innerHTML = 'new element';
parentElement.appendChild(newElement);
}
And it works.
Please look at the jsFiddle for more details.

While you've already accepted an answer, I felt that it might be worth offering a more extensible approach, which allows you to use unobtrusive JavaScript (rather than relying upon in-line event-handlers such as onclick) for easier maintenance.
It's also a little more extensible and customisable:
// a simple function to help derive the correct element
// from the supplied argument, 'needle':
function derive(needle) {
// if the needle has a nodeType and if that nodeType is
// exactly equal to 1:
if (needle.nodeType && needle.nodeType === 1) {
// the needle is then an element-node, and here
// we convert that node into an Array of one:
needle = [needle];
// otherwise, if the needle is a string, and
// document.getElementById() finds an element
// with that id:
} else if ('string' === typeof needle && document.getElementById(needle)) {
// we find that element-node again, using the string
// and again convert it to an Array of one:
needle = [document.getElementById(needle)];
// otherwise, if the needle is - again - a string, and
// document.querySelectorAll() can find a collection
// (of one or more) elements matching the selector that
// the needle is implied to be then we retrieve those
// elements and, using Array.from(), we convert the
// collection into an Array:
} else if ('string' === typeof needle && document.querySelectorAll(needle)) {
needle = Array.from(document.querySelectorAll(needle));
}
// here we return the results to the calling context:
return needle;
}
function addNewElement(opts) {
// the default settings for the function:
// append: Boolean, true: the content will be
// inserted after the found sibling-
// node; false: the content will be
// inserted before the found sibling-
// node.
// classes: String, a string of white-space
// separated class-names to add to
// the new contents,
// Array, an array of class-names to
// add to the new contents.
// content: String, a string of HTML you wish
// to appear in the newly-added content.
// count: Number, the number of elements you
// wish to insert at once.
// create: String, the element-type to create
// null, if you want the function to
// 'decide' for itself.
// parent: Node, the element to which you want
// to add new elements,
// String, the id of the element to
// which you want to add new elements,
// or a CSS selector by which you want
// find the element(s) in the document
// to add new elements to.
// sibling: Node, the node beside which the new
// element(s) should be added.
// Null, the function will try to determine
// the desired element beside which the
// content should be added, based on
// the 'append' setting (above).
var settings = {
'append': true,
'classes' : null,
'content': 'Newly-added element.',
'count': 1,
'create': null,
'parent': document.body,
'sibling': null
},
// uninitialised variables for use later, primarily
// to declare/instantiate variables in one place:
parents,
childType,
created,
sibling,
clone,
classes,
count,
// a documentFragment to enable the addition of multiple
// elements at the same time without triggering (quite so)
// many redraws of the document/page:
fragment = document.createDocumentFragment();
// using Object.keys to iterate over the opts Object, if
// one is supplied or an empty object to avoid errors,
// using the Array.prototype.forEach() method:
Object.keys(opts || {}).forEach(function(key) {
// here we update/overwrite the keys of the
// settings object to the values held in those
// properties of the opts Object:
settings[key] = opts[key];
});
// we call the derive function to retrieve an array
// of element(s):
parents = derive(settings.parent);
// checking, and then storing, the value of
// settings.append; it it's equal to true the
// assessment returns true, if it's equal to
// false the assessment returns false (this
// is a naive check, because it requires that
// a Boolean is stored in that property):
appendCheck = settings.append === true;
// ensuring that the settings.count number
// is a number by parsing the potential
// String, other-based number, into base-10:
count = parseInt(settings.count, 10);
// iterating over each of the parents:
parents.forEach(function(pater) {
// 'pater' the first argument is a reference
// to the current array-element of the array
// over which we're iterating.
// retrieving the element-type to be created,
// if a value was supplied in settings.create
// then we use that (we don't check it's a
// valid element, or that it can be validly
// contained in the nominated parent), otherwise
// if the current element node has children
// then we retrieve the localName of its
// lastElementChild, if it has no children
// the ternary returns null and we move to
// the string of 'div':
childType = settings.create || (pater.children.length > 0 ? pater.lastElementChild.localName : null) || 'div';
// here we create the element:
created = document.createElement(childType);
// if the earlier assessment of settings.append
// resulted in true:
if (appendCheck === true) {
// we find the sibling beside which to insert the
// new content; if a node was supplied we use that,
// otherwise we use the lastElementChild or lastChild:
sibling = settings.sibling || pater.lastElementChild || pater.lastChild;
} else if (appendCheck === false) {
// otherwise, we use either the supplied value or
// we use the firstElementChild or firstChild:
sibling = settings.sibling || pater.firstElementChild || pater.firstChild
}
// assign the supplied - or default - content to the
// created element:
created.innerHTML = settings.content;
// if any class-names have been supplied:
if (settings.classes) {
// we first check whether the settings.classes
// variable is an Array (using Array.isArray),
// which returns a Boolean (true or false); if
// it returns true we simply use the Array otherwise
// we assume it's a String and split that String
// on its white-space characters (/\s+/):
classes = Array.isArray(settings.classes) ? settings.classes : settings.classes.split(/\s+/);
// iterating over the array of class-names:
classes.forEach(function(cN) {
// the first argument (cN) is a reference
// to the current array-element of the
// Array over which we're iterating.
// here we use the Element.classList API to
// add each of the class-names:
created.classList.add(cN);
});
}
// a simple for loop to add the desired
// number of new elements (as supplied in
// the settings.count, or opts.count
// setting):
for (var i = 0; i < count; i++) {
// clone the created-element (and its
// child elements):
clone = created.cloneNode(true);
// append the cloned node to the
// documentFragment we created
// earlier:
fragment.appendChild(clone);
}
// here we use parentNode.insertBefore() to insert
// the new contents (held in fragment) either the
// sibling.nextSibling (if appendCheck is true) or
// before the sibling (if appendCheck is false):
pater.insertBefore(fragment, (appendCheck ? sibling.nextSibling : sibling));
});
}
// retrieving the <button> elements on the page, and converting
// to an Array, using Array.from():
var buttons = Array.from(document.querySelectorAll('button'));
// iterating over those <button> elements in the Array:
buttons.forEach(function(button) {
// using the anonymous function of the addEventListener()
// to call the addNewElement function, in which
// we set the opts.parent setting to the
// previousElementSibling of the button
// firing the event:
button.addEventListener('click', function() {
addNewElement({
'parent': button.previousElementSibling
});
});
});
function derive(needle) {
if (needle.nodeType && needle.nodeType === 1) {
needle = [needle];
} else if ('string' === typeof needle && document.getElementById(needle)) {
needle = [document.getElementById(needle)];
} else if ('string' === typeof needle && document.querySelectorAll(needle)) {
needle = Array.from(document.querySelectorAll(needle));
}
return needle;
}
function addNewElement(opts) {
var settings = {
'append': true,
'classes': null,
'create': null,
'content': 'Newly-added element.',
'count': 1,
'parent': document.body,
'sibling': null
},
parents,
childType,
created,
sibling,
clone,
classes,
fragment = document.createDocumentFragment();
Object.keys(opts || {}).forEach(function(key) {
settings[key] = opts[key];
});
parents = derive(settings.parent);
appendCheck = settings.append === true;
parents.forEach(function(pater) {
childType = settings.create || (pater.children.length > 0 ? pater.lastElementChild.localName : null) || 'div';
created = document.createElement(childType);
if (appendCheck === true) {
sibling = settings.sibling || pater.lastElementChild || pater.lastChild;
} else if (appendCheck === false) {
sibling = settings.sibling || pater.firstElementChild || pater.firstChild
}
created.innerHTML = settings.content;
if (settings.classes) {
classes = Array.isArray(settings.classes) ? settings.classes : settings.classes.split(/\s+/);
classes.forEach(function(cN) {
created.classList.add(cN);
});
}
for (var i = 0; i < settings.count; i++) {
clone = created.cloneNode(true);
fragment.appendChild(clone);
}
pater.insertBefore(fragment, (appendCheck ? sibling.nextSibling : sibling));
});
}
var buttons = Array.from(document.querySelectorAll('button'));
buttons.forEach(function(button) {
button.addEventListener('click', function() {
addNewElement({
'parent': button.previousElementSibling
});
});
});
div {
border: 2px solid #eeeeee;
background-color: #dff0d8;
}
ol {
background-color: #dff0d8;
}
li {
background-color: #eff0c8;
}
<p>In next example we can add new child element to this list:</p>
<ol id="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ol>
<button>Add new li-element to this list</button>
<p>In next example we can add new child element to this div:</p>
<div id="someThing">Something here</div>
<button>Add new div-element to this div</button>
JS Fiddle demo.
References:
Array.from().
Array.isArray().
Array.prototype.forEach().
Conditional (ternary) Operator.
document.createDocumentFragment().
document.createElement().
document.getElementById().
document.querySelector().
document.querySelectorAll().
Element.classList API.
Element.innerHTML.
Element.localName.
EventTarget.addEventListener().
JavaScript regular expressions Guide.
Node.appendChild().
Node.firstChild.
Node.insertBefore().
Node.nextSibling.
Node.nodeType.
Node.previousSibling.
NonDocumentTypeChildNode.nextElementSibling.
NonDocumentTypeChildNode.previousElementSibling.
Object.keys().
ParentNode.children.
ParentNode.children.
ParentNode.children.
String.prototype.split().
typeof operator.

Well, I've found simple way how to fix it, but I was looking for something even more basic:
document.getElementById(parentId).appendChild(newElement);
EDIT:
Another way how to do it:
<!DOCTYPE html>
<html>
<body>
<style>
div {
border: 2px solid #eeeeee;
background-color: #dff0d8;
}
ol {
background-color: #dff0d8;
}
li {
background-color: #eff0c8;
}
</style>
<script>
function addNewElement(newElementType,parentId,parentElementType) {
//document.getElementById(clickedId).appendChild(newElement);
var el = parentElementType + "[id=" + parentId + "]";
el = document.querySelector(el);
var newElement = document.createElement(newElementType);
newElement.innerHTML = 'new element';
el.appendChild(newElement);
}
</script>
<p>In next example we can add new child element to this list:</p>
<ol id="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ol>
<button onclick="addNewElement('li','list','ol')">Add new li-element to this list</button>
<p>In next example we can add new child element to this div:</p>
<div id="someThing">Something here</div>
<button onclick="addNewElement('div','someThing','div')">Add new div-element to this div</button>
</body>
</html>
But now we need to pass parent node type in addNewElement function in this new example. Or we can also define classes for ul and div elements and use them instead of ids.
It's more advanced way, but it may be more useful in some cases. Here's the documentary about document.querySelector and document.querySelectorAll.
Also read this querySelector and querySelectorAll vs getElementsByClassName and getElementById in JavaScript, if you want to get some additional info.

I know you already have an answer that works for you, but I just wanted to add one that shows a more flexible way of doing this using a configuration object instead of just passing in a tag name. To make it more flexible you can pass in a reference to a parent object instead of an id. Also, it returns a reference to the newly created element in case you want to do something with it after it is added to the DOM.
'use strict';
var addNewElement = function (configItems, elParent) {
var newElements = [];
if (!Array.isArray(configItems)) {
// if configItems is not an array, and therefore a
// single config object or string, turn it into
// a single element array
configItems = [configItems];
}
// If elParent is a string assume it is
// the id of an element in the page and select it
if (typeof elParent === 'string') {
elParent = document.getElementById(elParent);
}
configItems.forEach(function (config) {
var option,
elChild;
// if a string is passed in, assume it is
// the tagName and create a default config object
if (typeof config === 'string') {
config = {tag: config};
}
elChild = document.createElement(config.tag);
for (option in config) {
if (config.hasOwnProperty(option)) {
switch (option) {
case 'tag':
// do nothing, already used tag to create new element
break;
case 'html':
// just a shortcut so we don't have to use
// innerHTML in our config object
elChild.innerHTML = config.html;
break;
case 'text':
// another shortcut
elChild.textContent = config.text;
break;
case 'class':
// if we are passed an array convert it to a space delimited string
elChild.className = Array.isArray(config.class) ?
config.class.join(' ') : config.class;
break;
default:
// if we haven't already handled it, assume it is
// an attribute to add to the element
elChild.setAttribute(option, config[option]);
}
}
}
// default text if none was specified
if (elChild.innerHTML === '') {
elChild.innerHTML = 'new element';
}
newElements.push(elChild);
elParent.appendChild(elChild);
});
// return a reference to the new element(s)
// in case you want to do something else with it
// after it was inserted into the document
// returns a single item or an array depending on how many
// items you passed it in configItems
return newElements.length === 1 ? newElements[0] : newElements;
};
Usage would look like this:
// just add a new element with the default text by id
addNewElement('li', 'list');
var list = document.getElementById('list');
// a little fancier, this time using an element reference
addNewElement({
tag: 'li',
html: 'Custom List Item!',
class: 'fancy'
}, list);
addNewElement({
tag: 'input',
placeholder: 'Type here',
value: 'Delete me'
}, document.body); // attach to the body
// do something with the element
// after we create it
var houdini = addNewElement({
tag: 'li',
text: 'Now you see me.',
class: ['houdini', 'show'],
}, list);
setTimeout(function () {
houdini.textContent = "Now you don't";
houdini.classList.remove('show');
}, 2000);
var checkElements = addNewElement([
{
tag: 'input',
id: 'check',
type: 'checkbox',
checked: 'checked',
},
{
tag: 'label',
for: 'check',
html: 'Uncheck me!'
}
], document.body);
jsFiddle showing it in action.
Using hasOwnProperty is necessary since we are using for in.
The 'class' case is there because in ES3 you could not use reserved words as property names with dot notation, so when the DOM API was designed they instead used className to represent the class property. Ever since ES5 we can use reserved words as properties without quoting them. This allows us to add a 'class' shortcut property.

Related

JS, How to change only parent element

I have something like that :
<h3 id="plop">I want to change this text <span id="trololo">bla bla bla</span> </h3>
Generaly, I use js to change text on my html elements :
document.getElementById("plop").innerHTML = "I change the text";
But do something like this remove my span element on title.
Then, is it possible to change only the text on h3 ?
I know, i can do something like that:
document.getElementById("plop").innerHTML = "I change the text <span id=\"trololo\">bla bla bla</span>";
But is not my goal.
You can try this:
document.querySelector('#plop').firstChild.nodeValue = "I change the text "
With a RegExp you can preserve the element as it is:
document.getElementById("plop").innerHTML = document.getElementById("plop").innerHTML.replace(/.*(<span.*<\/span>)/,"I change the text $1")
console.log(document.getElementById("plop").innerHTML)
<h3 id="plop">I want to change this text <span id="trololo">bla bla bla</span> </h3>
Note: Don't go much beyond that with regexp and HTML: https://stackoverflow.com/a/1732454/2729605
While you've already accepted an answer to your question, I thought I'd take the time to post an alternative — functional — approach:
// declaring the function changeText() as a constant, using
// Arrow function syntax (since we don't use 'this' within
// the function); this takes two arguments:
// elem: a reference to the node whose text we're changing, and
// ...text: which combines all supplied Strings into an Array,
// using rest parameters:
const changeText = (elem, ...text) => {
// initialises a counter:
let counter = 0,
// in the event that only one String is provided we make the
// assumption that the supplied String should replace all
// text-nodes:
repeat = text.length === 1,
// declared, but uninitialised, variables for later use:
cache,
_temp,
beginsWithSpace,
endsWithSpace;
// retrieves the child-nodes of the given element-node, and we then
// use NodeList.prototype.forEach() to iterate over those nodes:
elem.childNodes.forEach(
// n is a reference to the current Node of the NodeList:
(n) => {
// if the Node has a nodeType, and that nodeType is 3 it is
// a textNode:
if (n.nodeType && n.nodeType === 3) {
// we cache the current nodeValue in the 'cache' variable:
cache = n.nodeValue;
// we use RegExp.prototype.test() which returns a Boolean
// to see if the current nodeValue starts with a space:
beginsWithSpace = (/^\s+/).test(cache);
// ...or ends with a whitespace:
endsWithSpace = (/\s+$/).test(cache);
// here we assign the text - from the supplied argument -
// to the _temp variable; if only one argument exists
// (hence repeat is true) we use the first element of the
// Array, otherwise - if repeat is false - we use the
// String from the text Array at the index of counter, and
// then increment the counter variable:
_temp = repeat ? text[0] : [text[counter++]];
// here we assign a new value to the current Node, using
// a template-literal; if beginsWithSpace evaluates to true
// a single white-space is inserted, otherwise an empty
// String is inserted, followed immediately by the cached
// text to be inserted, followed again by either another
// white-space character or empty string depending on
// the value of endsWithSpace:
n.nodeValue = `${beginsWithSpace ? ' ':''}${ _temp }${endsWithSpace ? ' ' : ''}`;
}
});
}
// calling the function:
changeText(document.getElementById('plop'), "some new text in here");
changeText(document.querySelector('li'), "Node content", "amazing");
changeText(document.querySelector('li:last-child'), "redacted");
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
q::before,
q::after {
content: '"';
color: #777;
}
ul li {
margin: 0.2em 0 0 1.5em;
}
ul li::marker {
content: "\00BB";
color: #f90;
}
<h3 id="plop">I want to change this text <span id="trololo">bla bla bla</span> </h3>
<ul>
<li>This text <em>should be</em> removed</li>
<li>This <em>element</em> has <em>multiple</em> <q>HTML child-nodes</q> which should <em>not</em> be changed<strong>.</strong></li>
</ul>
JS Fiddle demo.
References:
Arrow functions.
document.getElementById().
document.querySelector().
Conditional (ternary) Operator.
node.nodeType.
node.nodeValue.
Regular Expressions.
RegExp.prototype.test().
Rest parameters.
Spread syntax.
String.prototype.replace().
Template literals.

Is there a way to get a list of all existing HTML tags via JavaScript?

There is an exercise in a bootcamp I am attending which tasks one to create a jQuery like selector, here is part of the test:
describe("selectorTypeMatcher", function() {
it("should return the 'id' type for an id selector", function() {
var type = selectorTypeMatcher('#pagetitle');
expect(type).toEqual("id");
});
it("should return the 'class' type for a class selector", function() {
var type = selectorTypeMatcher('.image');
expect(type).toEqual("class");
});
it("should return the 'tag.class' type for a tag.class selector", function() {
var type = selectorTypeMatcher('img.thumbnail');
expect(type).toEqual("tag.class");
});
it("should return the 'tag' type for a tag selector", function() {
var type = selectorTypeMatcher('div');
expect(type).toEqual("tag");
});
});
The following is part of function which I created as described in the test spec.
var selectorTypeMatcher = function(selector) {
if (selector.includes('#')) return 'id';
if (selector.indexOf('.') == 0) return 'class';
if (/<[a-z][\s\S]*>/i.test() && selector.includes('.')) return 'tag.class';
};
I am stuck at the conditional which would check for a tag and class e.g. div.foo
I thought of created an array which would contain all existing tags...
var tags = ["a", "div", "span", "form", "h1", "h2", "h3", "h4"];
And then loop over them and see if that value was followed by an . for a class but that would be a lot of elements...
I thought of leveraging document.querySelectorAll('*') but that just...
Returns a list of the elements within the document (using depth-first
pre-order traversal of the document's nodes) that match the specified
group of selectors. The object returned is a NodeList.
But like it says Returns a list of the elements within the document...
So is there an API that will return all of the existing html elements?
html, head, body, div, p, span etc.
Merci!
You can use HTMLUnknownElement object to check for a valid tag by specification:
if (tagIsValid(selector))
return 'tag';
and tagIsValid definition would be:
function tagIsValid(tag) {
tagChecked = document.createElement(tag).toString();
return tagChecked != "[object HTMLUnknownElement]";
}
if (selector.indexOf('.') > 0) return 'tag.class';
return 'tag';
I think you can end it with that.

Setting a custom filter that will exclude the list item titles from the search criteria

This code (that I took it from a Book) will apply a filter to the listview that searches only the body copy, excluding the list item titles from the search criteria
<body>
<div data-role=”page” id=”MY-page”>
<div data-role=”header”>
<h1>Sports</h1>
</div>
<div data-role=”content”>
<ul data-role=”listview” data-filter=”true”>
<li>Football</li>
<li>Basketball</li>
<li>Tennis</li>
<li>Volleyball</li>
</ul>
<!-- etc. -->
</body>
$(document).bind("mobileinit", function() {
$.mobile.listview.prototype.options.filterCallback = onlyBody;
});
function onlyBody(text, searchValue) {
var splitText = text.trim().split("\n");
console.log(" text: "+ splitText[1]);
return splitText[1].toLowerCase().indexOf( searchValue ) === -1;
};
String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g,””);
}
I didn't understand this piece of code
return splitText[1].toLowerCase().indexOf( searchValue ) === -1;
I know that indexOf returns a number representing the position where the specified searchvalue occurs for the first time, or -1 if it never occurs
and the === operator return a boolean. Why do we want to return a boolean?
Also, I didn't notice that the default filter in jQuery Mobile has changed after putting this code in a script tag before closing the body tag. How can I make sure that this code is working correctly?
Breaking it down to each step:
splitText[1]
Returns the second element of the splitText array (as array indexes are zero-based)
.toLowerCase()
The value of the array is a string, and this converts that value to be entirely lowercase.
.indexOf(searchValue) === -1;
indexOf() looks for a given value within the string/array it was called on and returns its position within the string as an integer. This integer is the starting index of the match. If no match was found it returns -1.
return splitText[1].toLowerCase().indexOf(searchValue) === -1;
Putting it all back together, this line of code is returning true if the searchValue is not found within the second item of the splitText array.
Unfortunately you haven't shown us enough code to know why this boolean value is returned, or how it's used. For that you would need to check the logic within the listView to see how the $.mobile.listview.prototype.options.filterCallback value is used.
I found an answer to my question : Why do we want to return a boolean?
To set a custom filtering function that will become the new default for all filterable widgets, override the filterCallback option in the filterable widget prototype in a "mobileinit" signal handler:
$( document ).one( "mobileinit", function() {
$.mobile.filterable.prototype.options.filterCallback = function( index, searchValue ) {
// In this function the keyword "this" refers to the element for which the
// code must decide whether it is to be filtered or not.
// A return value of true indicates that the element referred to by the
// keyword "this" is to be filtered.
// Returning false indicates that the item is to be displayed.
//
// your custom filtering logic goes here
});
});
source

What is a wrap method to wrap only a part of a string?

I have an HTML like this:
<div>
<h3>How are you? Fine?</h3>
</div>
I would like to turn that in something different, using two numbers n and m, something like:
if n=5 and m=12
<div>
<h3>How
<span class="yellow">are you?</span>
Fine?</h3>
</div>
In other words I would like to "highlight" only a part of a string using two numbers that specifies the start and the end of the "highlight" (in characters).
I tried this but it didn't work:
//in the previous example it will be a xpath selector for div/h3
var selection=$(document.body).xpath(selector).text();
//it will be "are you?"
var substring=selection.substring(n,m);
//it would make that i want, but it doesn't
$(document.body).xpath(selector).contents().filter(function() {
return substring;
}).wrap("<span class=\"yellow\"></span>");
}
One way to do it would be - instead of wrap, replace the content of the h3 with the slice up manipulated version.
See this JSFiddle http://jsfiddle.net/9LeL3f3n/21/
<div>
<h3>How are you? Fine?</h3>
</div>
$(document).ready(function() {
var highlight = function(str, start, end) {
return str.slice(0,start-1) +
'<span style="color:#ffff00">' +
str.substring(start-1, end) +
'</span>' +
str.slice(-1 * (str.length - end));
};
var n = 5;
var m = 12;
$('h3').html(highlight($('h3').html(),n,m));
});
I'd suggest writing your own plugin to achieve this, if only because it would appear to be something useful for repeated use. That said, I'd offer the following approach, which effectively splits the textNode nodes, using textNode.splitText() and appending the relevant portion to a created element before re-inserting that node, and surrounding textNodes back into the parent element:
// a simple plugin approach, taken from:
// https://learn.jquery.com/plugins/basic-plugin-creation/
(function ($) {
// defining the name of the plugin ('highlight'):
$.fn.highlight = function (opts) {
// the default settings, used if no arguments are
// passed into the plugin via the 'opts' Object:
var settings = {
'start' : 0,
'end' : 1,
'wrapper' : 'span',
'class' : 'highlight',
'css' : {
'color' : 'yellow'
}
},
// 'empty' declared variables for later use:
node, textMiddle, textEnd;
// iterating over the 'opts' Object using
// a for...in loop:
for (var prop in opts) {
// if the 'opts' Object has an own property 'prop'
// (not one inherited from its prototype chain):
if (opts.hasOwnProperty(prop)) {
// we update the 'settings' Object to
// be equal to the 'opts' Object property-value:
settings[prop] = opts[prop];
}
}
// using parseInt() to ensure that we're working
// with numbers, rather than strings, and that those
// numbers are in base-10:
settings.start = parseInt(settings.start, 10);
settings.end = parseInt(settings.end, 10);
// normalising the settings.wrapper string, ensuring that
// if the user passes in '<span>' (or '<<<<<span>', etc)
// we remove those (this could be made even safer by only
// allowing the first consecutive string of alphabetical
// characters):
settings.wrapper = settings.wrapper.replace(/<|>/g,'');
// here we iterate over, and return, the jQuery collection
// to allow for chaining to continue (here 'this' is the
// jQuery collection of nodes/elements):
return this.each(function () {
// here this is the DOM node from the collection.
// and here we iterate over the childNodes of each
// DOM node from that collection:
$(this).contents().each(function () {
// if the current childNode is nodeType 3 (a textNode)
// AND the length of the nodeValue (the text itself)
// is greater than, or equal to, the settings.end:
if (this.nodeType === 3 && this.nodeValue.length >= settings.end) {
// we create a new element equal to the
// settings.wrapper argument passed in by
// the user (or the default):
node = document.createElement(settings.wrapper);
// if we have a settings.css Object:
if (settings.css) {
// we iterate over that Object with a
// for...in loop (as above):
for (var prop in settings.css){
if (settings.css.hasOwnProperty(prop)) {
// setting the node's style property
// to be equal to the property-value
// of the settings.css Object:
node.style[prop] = settings.css[prop];
}
}
}
// if we have a settings.class:
if (settings.class) {
// we use Array.prototype.forEach
Array.prototype.forEach
// with Function.prototype.call()
// to iterate over the resulting array
// of splitting the settings.class
// String on white-space characters:
.call(settings.class.split(/\s+/),
// the 'classname' argument is the
// class-name from the string, now
// in the Array over which we're iterating:
function (classname) {
// here we add the trimmed classname
// string (removing the leading and
// trailing white=space) to the
// list of classes of the node:
node.classList.add(classname.trim());
});
}
// here we split the textNode (this) using
// Text.splitText(offset); which converts
// one textNode into two separate textNodes
// and returns the second (newly-created)
// textNode ('this' remains 'this' but with
// shortened text):
textMiddle = this.splitText(settings.start);
// and again, but this time we have to compensate
// for already shortening the textNode, and
// and so subtract the offset from the settings.end:
textEnd = textMiddle.splitText(settings.end - settings.start);
// appending the textNode to the created
// element:
node.appendChild(textMiddle);
// inserting the created node after the original
// textNode:
this.parentNode.insertBefore(node, this.nextSibling);
}
});
});
};
// passing jQuery into the plugin in order to allow us to use
// the $ alias within the plugin:
})(jQuery);
// using the plugin:
$('div h3').highlight({
// setting the 'start' offset:
'start' : 4,
// the end index/offset:
'end' : 12,
// specifying classes to add to the created element(s):
'class' : 'highlight classes',
// setting the CSS properties of the created element(s):
'css' : {
'color' : '#f89',
'text-decoration' : 'underline'
}
});
(function($) {
$.fn.highlight = function(opts) {
var settings = {
'start': 0,
'end': 1,
'wrapper': 'span',
'class': 'highlight',
'css': {
'color': 'yellow'
}
},
node, textMiddle, textEnd;
for (var prop in opts) {
if (opts.hasOwnProperty(prop)) {
settings[prop] = opts[prop];
}
}
settings.wrapper = settings.wrapper.replace(/<|>/g, '');
return this.each(function() {
$(this).contents().each(function() {
if (this.nodeType === 3 && this.nodeValue.length >= settings.end) {
node = document.createElement(settings.wrapper);
if (settings.css) {
for (var prop in settings.css) {
if (settings.css.hasOwnProperty(prop)) {
node.style[prop] = settings.css[prop];
}
}
}
if (settings.class) {
Array.prototype.forEach.call(settings.class.split(/\s+/), function(classname) {
node.classList.add(classname.trim());
});
}
textMiddle = this.splitText(settings.start);
textEnd = textMiddle.splitText(settings.end - settings.start);
node.appendChild(textMiddle);
this.parentNode.insertBefore(node, this.nextSibling);
}
});
});
};
})(jQuery);
$('div h3').highlight({
'start': 4,
'end': 12,
'class': 'highlight classes',
'css': {
'color': '#f89',
'text-decoration': 'underline'
}
});
.highlight {
font-style: italic;
}
.classes {
font-variant: small-caps;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
<h3>How are you? Fine?</h3>
</div>
External JS Fiddle demo.
References:
JavaScript:
Array.prototype.forEach().
document.createElement().
Element.classList.
For...in loop.
Function.prototype.call().
Node.appendChild().
Node.insertBefore().
Node.nodeType.
Node.nodeValue.
Node.parentNode.
Object.prototype.hasOwnProperty().
parseInt().
String.prototype.replace().
String.prototype.split().
Text.splitText().
jQuery:
contents().
each().
"How to Create a Basic Plugin."

Pushing data into an array of objects

Alright, I've got this blank array of objects.
I am dynamically finding every node in a web page and each node is going to have it's own object and properties.
I need a way to throw the values I need into their respective objects property
So, for example, I find the body node. I now have a special little object for this node. I need to throw pretty much everything about this little guy into his object's properties.
So I pretty much need it to render like this:
Turning this:
<html>
<body style="margin:0; padding:0;" title="My Title">
<p>some text</p>
<div class="wrapper"></div>
<footer></footer>
</body>
</html>
Into this:
this.nodesInfo = [ // All nodes in the page's DOM
{
type: 'body', // ex: body, section, aside, div, etc.
id: 'myID', // the Id of that element
class: ['myClass1', 'myClass2'], // the class/class' of that element
depth: '2', // the level in the page's DOM in which that element sits, this will be an integer
parent: 'html', // that elements direct parent Node
children:['div.wrapper', 'p', 'footer'], // any child Nodes that, that element may be a parent to
text: '', // the text inside that element if any exists
attributes: ["style=margin:0; padding:0;", "title='My Title'"] // all attributes of this node
}
];
It would of course cycle through each node it discovered and do this for each node accordingly, until it ran out of nodes.
The class, children, and attributes properties are arrays for the simple possibility of multiples of any of these. Everything else is just a string since a node can't have more than one ID, title, or direct parent tag.
If a node does not contain some of these properties then that property would remain blank/null/undefined.
My question is simple. Is this possible, if not would I instead have to create each object individually and the push them into my nodesInfo array?
I think the easiest way to go about this would be making an object of each Node and then pushing them all (once they are all created) into an array.
I was building something like this the other night. This should work and you can add more stuff easily. http://jsfiddle.net/elclanrs/UHbMa/
$.fn.buildTree = function() {
var tree = {};
this.find('*').andSelf().each(function(i, v) {
var parents = $(this).parents().length - 1,
depth = 0;
while (depth < parents) {
depth++;
}
tree[v.tagName.toLowerCase() + '(' + i + ')'] = {
id: (v.id) ? '#' + v.id : '',
className: (v.className) ? '.' + v.className.replace(' ', '.') : '',
depth: depth
};
});
return tree;
};
// Then you can do this...
var tree = $('#element').buildTree();
for (var tag in tree) {
// Get your variables
var tag.match(/\w+/), // Get rid of `(n)`
id = tree[tag].id,
className = tree[tag].className,
depth = tree[tag].depth;
html = 'something';
// Bla bla
}

Categories