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.
Related
I need to wrap specific terms in a string of text with <span> tags only if the term isn't already wrapped in a <span> tag.
For example I have a string of text:
Test string of text containing foo bar and baz.
And an object with key value pairs to search for in the string:
toolTips = {
foo: 'tooltip for foo',
bar: 'problematic tooltip that also contains baz',
baz: 'tooltip for baz'
}
I need to iterate over the object keys and wrap matching terms with <span> tags to add the tool tip text.
So after the first iteration of the loop the string would be:
Test string of text containing
<span class="tooltip">foo
<span class="tooltip-text">tooltip for foo</span>
</span>
bar and baz.
After the second it would be:
Test string of text containing
<span class="tooltip">foo
<span class="tooltip-text">tooltip for foo</span>
</span>
<span class="tooltip">bar
<span class="tooltip-text">problematic tooltip that also contains baz</span>
</span>
and baz.
And after the third it would be:
Test string of text containing
<span class="tooltip">foo
<span class="tooltip-text">tooltip for foo</span>
</span>
<span class="tooltip">bar
<span class="tooltip-text">problematic tooltip that also contains baz</span>
</span>
and
<span class="tooltip">baz
<span class="tooltip-text">tooltip for baz</span>
</span>
.
I've tried doing this with string.replace() and with various regex patterns but I haven't been able to get it to fully work. Either the text inside of a previously added <span> gets matched and replaced or I do a negative look ahead for a closing </span tag in the regex and then text that comes before any span doesn't get matched.
Would appreciate ideas on how to handle this.
It is not the most efficient idea, but you can try using placeholders, in order to not have phrases which overlap themselves.
let string = "Test string of text containing foo bar and baz.";
const toolTips = {
foo: 0,
bar: 1,
baz: 2
}
const toolTipsPlaceholders = {
0: {key: 'foo', value: 'tooltip for foo'},
1: {key: 'bar', value: 'problematic tooltip that also contains baz'},
2: {key: 'baz', value: 'tooltip for baz'}
}
const keys = Object.keys(toolTips)
keys.forEach(k => string = string.replaceAll(k, toolTips[k]))
const keysPlaceholders = Object.keys(toolTipsPlaceholders)
keysPlaceholders.forEach(k => string = string.replaceAll(k, `<span class="tooltip">${toolTipsPlaceholders[k].key}<span class="tooltip-text">${toolTipsPlaceholders[k].value}</span></span>`))
document.getElementById("test").innerHTML = string;
.tooltip {
color: red;
}
.tooltip-text {
color: blue;
}
<div id="test"></div>
Below is an approach that should let you take care of it.
You can use a really complicated regex to check if the key is already in elements, but that can be a pain to write, understand, and maintain.
Instead, the trick is to loop over each node in an element. If it is a text node, you know there is no HTML in there, so any replacements are safe. If it is an element node, then recurse through, looking for text nodes, and skip over tooltip elements.
This, in my opinion, makes it easier to understand and maintain going forward.
In the replacement, I just replaced the key with the tooltip value, which isn't exactly what you want, but can easily be tweaked to your liking.
document.querySelector('button').addEventListener('click', () => {
const root = document.querySelector('div');
applyTooltips(root);
});
const tooltips = {
foo: 'hello bar and baz',
bar: 'goodbye',
baz: 'cake and foo'
};
// Regex that can match all of the keys at the same time
// so we don't risk getting weirdness if one tooltip
// contains another key
const matchRegex = new RegExp(`(${Object.keys(tooltips).join('|')})`, 'g');
const tooltipSelector = '.tooltip';
const applyTooltips = element => {
// Loop over each childNode, which might be a text or element node.
[...element.childNodes].forEach(child => {
// If it is a text node, we'll apply the tooltip logic
if (child.nodeType === Node.TEXT_NODE) {
// Get the text
const text = child.wholeText;
// Replace the text with the HTML
newText = text.replaceAll(matchRegex, key => `<div class="tooltip">${tooltips[key]}</div>`);
// Create a temp element we can assign the
// HTML text to to get actual elements
const temp = document.createElement('div');
temp.innerHTML = newText;
// Apply each new node before the text child
[...temp.childNodes].forEach(node =>
element.insertBefore(node, child)
);
// Remove the old text child
element.removeChild(child);
} else if (!child.matches(tooltipSelector)) {
// If it is an element that isn't a tooltip element, we'll recurse on it.
applyTooltips(child);
}
});
};
.tooltip { color: #F00; }
<div>
Test string of text containing foo bar and baz.
This <div class="tooltip">foo</div> is already wrapped and won't get wrapped again.
</div>
<button>Apply</button>
I have an array that looks like:
var testArr = ["40", "A1", "B9", "58"]
I want to loop over all div elements of a certain class and return only the elements where the data attribute matches ANY of the items in that array.
If I do something like this:
$("div.prodCodes").filter(function(e) {
var x1 = $(this);
var x2 = $(this).data("prodCode");
testArr.forEach(function(e) { if (e == x2) { console.log("MATCH"); } });
});
That console outputs the correct number of matches, but I cannot return those elements from the filter function.
What on earth am I missing here? I've tried creating a new array and pushing each item onto it and returning that, but it's always empty. I'm sure I'm missing something obvious here. I've also tried rewriting this using .grep() and getting nowhere. Help is appreciated.
You need to return a truthy value in filter() to have an item included.
Try :
$("div.prodCodes").filter(function(e) {
return testArr.indexOf($(this).attr('data-prodCode')) >-1;
}).doSomething();
Without a return all items will be excluded
I would use a Set for constant-time lookup.
Be aware that jQuery reads the attribute value "58" as a number when using the data method, so it won't match unless you make sure the data type is the same:
// Use a set
var testSet = new Set(["40", "A1", "B9", "58"]);
var texts = $("div.prodCodes").filter(function() {
var x = $(this).data("prodCode").toString(); // data type must match
// Return a boolean to indicate whether the div element should be kept
return testSet.has(x); // Set#has() is fast
}).map(function(){
// For demo only: get the text content of the matching div elements
return $(this).text();
}).get(); // convert that to a plain array
console.log(texts);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="prodCodes" data-prod-code="A1">Hello</div>
<div class="prodCodes" data-prod-code="XX">Not this one</div>
<div class="prodCodes" data-prod-code="58">There</div>
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.
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
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."