How to wait until some text on a page appear?
I am using this but it does not work:
function waitText() {
if (document.getElementsByTagName('p')[0].innerHTML == "Some Text"){
alert("text appears");
}else{
setTimeout(function() { waitText() }, 1000);
}
}
I would use MutationObserver instead of that dirty checking if you don't need to support legacy browsers.
var target = document.querySelector('p');
// create an observer instance
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
[].every.call(mutation.addedNodes, function(node) {
if (node.nodeType === 3 && node.textContent === text) { // optionally you can remove nodeType checking if you pass the text inside some element like div etc.
console.log("text appeared");
observer.disconnect();
return false;
}
return true;
});
});
});
observer.observe(target, { childList: true });
There are a few issues with your code,
Firstly, we don't want to repeatedly set intervals - this will cause a cascade and ultimately freeze the thread which is not fun
Second, getElementByTagName is not a method on document. You either have to index into item 0 of the result of document.getElementsByTagName, or use some other lookup such as document.querySelector
So an example of how you might do it follows
function waitForText(element, text, callback, freq) {
if (!element || !callback || typeof text !== 'string')
throw new TypeError('Bad value');
var interval = window.setInterval(test, freq || 200);
function test() {
if (!element.parentNode) // node detached, don't hold onto this
window.clearInterval(interval);
if (element.textContent === text) {
window.clearInterval(interval);
callback.call(element);
}
}
}
Then
// say you want the first <p> in the DOM tree
var elm = document.querySelector('p');
// attach the condition
waitForText(elm, 'some text', () => console.log('Text appears'));
And in the future..
window.setTimeout(() => elm.textContent = 'some text', 6e3);
// wait 6 seconds..
// callback fires
Related
Based on this Q&A, I try to write some native JS code (no use of libraries) that will have elements addition hierarchy/dependency, which means that I'll have to wait for some elements to show up/load before I go on with the function. I have the following function to create and use MutationObservers in order to track DOM changes:
/**
* Creates an observer that tracks added DOM elements and executes a function when they're added
* #param {HTMLElement} observedObject - the dom element to observe
* #param {Function} elementFunc - the function to execute when elements are added
* #param {Number} timeoutMs - the amount of miliseconds to wait before disconnecting the observer
*/
function observeDOM(observedObject, elementFunc, timeoutMs) {
var connected = false;
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (!mutation.addedNodes) return
for (var i = 0; i < mutation.addedNodes.length; i++) {
if(elementFunc(mutation.addedNodes[i])) {
// disconnect when the function returns true
observer.disconnect();
break;
}
}
})
});
observer.observe(observedObject, {
childList: true
, subtree: true
, attributes: false
, characterData: false
});
connected = true;
// disconnect the observer after N seconds
if(timeoutMs >= 0) {
setTimeout(function() {
if(connected) {
observer.disconnect();
connected = false;
}
}, timeoutMs);
}
}
And I use the function above like that:
// do some staff...
observeDOM(document.body, function(node) {
if(node.className === "class1") {
// do some staff and wait for another element
observeDOM(document.body, function(node) {
if(node.className === "class2") {
// do some staff and wait for another element
observeDOM(document.body, function(node) {
if(node.className === "class2") {
//[...]
return true;
}
return false;
}
return true;
}
return false;
}
return true; // stop the observer
}
return false; // go on listening to elements addition
}
Do you think there is more elegant way to achieve it? What bothers my eyes the most is the tonnes of nested block I'll create when I have a big amount of elements to wait for !
It's possible what I'm describing is ill-advised with more standard approaches, and if so that would be a valid answer.
Say one is creating their own custom event, but in order for it to work the on() and off() steps need to perform actions on the element.
In my example scenario I want to create a custom jquery event called 'multiclick' that fires when an element is clicked 1, 2, or 3 times within a 500 ms timespan.
My initial idea would be to extend the functionality of on, off, and trigger and add my own event like so:
(function($)
{
var oldOn = $.fn.on;
var oldOff = $.fn.off;
var oldTrigger = $.fn.trigger;
// events [, selector ] [, data ], handler
// events [, selector ] [, data ]
// Note: this does not implement selector or data for multiclick
$.fn.on = function(events, handler)
{
if (events.split(/\s+/).some(function(event) { return event == 'multiclick'; }))
{
$(this).each(function()
{
if (!Array.isArray($(this).data('multiclick-state')))
{
$(this).data('multiclick-state', []);
}
var clicks = 0;
var clicksTimer = null;
var clickHandler = function(e)
{
clicks++;
if (clicks == 1)
{
clicksTimer = setTimeout(function()
{
handler(e, clicks);
clicks = 0;
}, 500);
}
else if (clicks == 3)
{
clearTimeout(clicksTimer);
handler(e, clicks);
clicks = 0;
}
};
$(this).data('multiclick-state').push(
{
handler: handler,
clickHandler: clickHandler
});
$(this).click(clickHandler);
});
arguments[0] = arguments[0].replace('multiclick', '');
}
return oldOn.apply(this, arguments);
};
// events [, selector ] [, handler ]
// events [, selector ]
// events
// Note: this does not implement selector
$.fn.off = function(events, handler)
{
if (events.split(/\s+/).some(function(event) { return event == 'multiclick'; }))
{
$(this).each(function()
{
if (!Array.isArray($(this).data('multiclick-state')))
{
$(this).data('multiclick-state', []);
}
$(this).data('multiclick-state', $(this).data('multiclick-state').filter(function(state)
{
if (handler == undefined || state.handler === handler)
{
$(this).off('click', state.clickHandler);
return false;
}
return true;
}.bind(this)));
});
arguments[0] = arguments[0].replace('multiclick', '');
}
return oldOff.apply(this, arguments);
};
// eventType [, extraParameters ]
// event, [, extraParameters ]
// Note: I wrote this in case trigger is changed to handle multiple events. Currently it does not. Also I don't support the second overloaded signature
$.fn.trigger = function(events)
{
if (typeof events == 'string' && events.split(/\s+/).some(function(event) { return event == 'multiclick'; }))
{
$(this).each(function()
{
if (!Array.isArray($(this).data('multiclick-state')))
{
$(this).data('multiclick-state', []);
}
// This doesn't support disabling bubbling or stopPropogation, would have to think about it. Very ad-hoc.
$(this).data('multiclick-state').forEach(function(data)
{
data.clickHandler();
});
});
arguments[0] = arguments[0].replace('multiclick', '');
}
return oldTrigger.apply(this, arguments);
};
$.fn.multiclick = function(callback)
{
if (arguments.length == 1)
{
return $(this).on('multiclick', callback);
}
else
{
return $(this).trigger('multiclick');
}
return this;
};
}(jQuery));
I'm not really guaranteeing this is the most complete solution, but it shows the basic approach. Then it would be used like a normal jquery event:
function multiclick(e, clicks)
{
console.log(clicks);
}
// $(document).on('multiclick', multiclick);
$(document).multiclick(multiclick);
//$(document).off('multiclick');
//$(document).off('multiclick', multiclick);
jsfiddle example: https://jsfiddle.net/rhsswfho/1/
This all just seems like a lot of code for something that should be fairly straightforward or even built into jquery with helper functions, yet I find none. Not to mention it's somewhat fragile and adds a performance penalty to all on, off, and trigger calls.
What would be the standard approach to doing this, or the more standard approach for handling such a problem?
It is quite straightforward, like you said. You can use the .on() for any kind of event listening. So you could do something like:
$('body').on('takeMeToDinner', function() {
alert('who will pay the bills?');
});
and then trigger that event via
$('body').trigger('takeMeToDinner');
Jquery isn't designed around adding custom operations to on/off events since it's only implementing the DOM events. (Meaning there's no extra code being ran on a per event type basis). Rather than creating a localized solution on the elements this can be accomplished simply by creating a document or body listener like so:
var clicks = 0;
var lastTarget = null;
var clicksTimer = null;
$('body').click(function(e)
{
if (e.target !== lastTarget)
{
clearTimeout(clicksTimer);
lastTarget = e.target;
clicks = 0;
}
clicks++;
if (clicks == 1)
{
clicksTimer = setTimeout(function()
{
$(e.target).trigger('multiclick', clicks);
clicks = 0;
lastTarget = null;
}, 500);
}
else if (clicks == 3)
{
$(e.target).trigger('multiclick', clicks);
clearTimeout(clicksTimer);
clicks = 0;
lastTarget = null;
}
});
Then used like normal:
$('div').on('multiclick', function(e, clicks)
{
console.log(clicks);
});
$('span').on('multiclick', function(e, clicks)
{
console.log(clicks);
});
Example on jsfiddle: http://jsfiddle.net/f35u0fw2/
What's happening for this specific case is the user can click anywhere and then for elements that are listening they'll get the 'multiclick' event after 1, 2, or 3 clicks as expected.
The DOMTokenList and DOMSettableTokenList interfaces (MDN, WHATWG) provide methods for manipulating ordered sets of string tokens represented by space-delimited strings. They are most commonly used in the form of the Element.prototype.classList property, a DOMTokenList which reflects the class attribute of an associated element.
var div = document.createElement('div');
div.setAttribute('class', 'hello world goodnight moon');
var list = div.classList;
console.assert(list.length === 4);
console.assert(list[0] === 'hello');
console.assert(list.item(1) === 'world');
console.assert(list.contains('moon') === true);
console.assert(list.contains('mars') === false);
list.remove('world', 'earth', 'dirt', 'sand');
list.add('hello', 'mars');
list.toggle('goodnight');
console.assert(div.getAttribute('class') === 'hello moon mars');
I'm working on a custom element (HTML5Rocks, W3C Draft) which displays a real-time feed of the activity of specified Stack Overflow users. This list of users is specified in an ids attribute, and may be updated at any time.
<so-users ids="1114 22656 106224"></so-users>
document.querySelector('so-users').setAttribute('ids', '23354 115866');
Instead of requiring users to manipulate this attribute directly, I would like to have an .ids property providing a DOMTokenList that they can use instead. Ideally this would be directly associated with the attribute, but an unbound DOMSettableTokenList instance that I have to manually bind would also be fine.
document.querySelector('so-users').ids.add('17174');
Unfortunately, I have been unable to find any way to create a DOMTokenList instance. The definition is not a constructor, and directly creating an object using its prototype results in errors when I call any associated methods:
new DOMTokenList; // TypeError: Illegal constructor
new DOMSettableTokenList; // TypeError: Illegal constructor
var list = Object.create(DOMSettableTokenList.prototype, {
value: { value: 'hello world' }
});
console.assert(list instanceof DOMTokenList);
console.assert(list instanceof DOMSettableTokenList);
list.item(0); // TypeError: Illegal invocation
function TokenListConstructor() {
this.value = 'hello world';
}
TokenListConstructor.prototype = DOMSettableTokenList.prototype;
var list = new TokenListConstructor;
console.assert(list instanceof DOMTokenList);
console.assert(list instanceof DOMSettableTokenList);
list.add('moon'); // TypeError: Illegal invocation
How can I construct a new DOMTokenList or DOMSettableTokenList instance?
You cannot create an DOMTokenList or an DOMSettableTokenList directly. Instead you should use the class attribute to store and retrieve your data and perhaps map an ids attribute of your DOM element to the classList property.
var element = document.querySelector('so-users');
element.ids = element.classList;
You can use relList according to the documentation but classList is more supported, the only drawback is that you might run into issues if one of your ids matches a class name so set an inline style to hide the element just in case.
For a custom component compatibility should be a concern (classList is present in IE>=10, Firefox 3.6, Chrome 8, Opera 11.5 and Safari 5.1, see http://caniuse.com/#feat=classlist) so if compatibility is in your requirements use the another solution posted below.
If you cannot use clases or classList and/or must use the ids attribute you should implement a custom function according to the spec with the following properties as functions.
item()
contains()
add()
remove()
toggle()
This is an example implementation of such functionality.
var TokenList = function (ids) {
'use strict';
var idsArray = [],
self = this,
parse = function (id, functionName, cb) {
var search = id.toString();
if (search.split(' ').length > 1) {
throw new Error("Failed to execute '" + functionName + "' on 'TokenList': The token provided ('" + search + "') contains HTML space characters, which are not valid in tokens.');");
} else {
cb(search);
}
};
function triggerAttributeChange() {
if (self.tokenChanged && typeof self.tokenChanged === 'function') {
self.tokenChanged(idsArray.toString());
}
}
if (ids && typeof ids === 'string') {
idsArray = ids.split(' ');
}
self.item = function (index) {
return idsArray[index];
};
self.contains = function (id) {
parse(id, 'contains', function (search) {
return idsArray.indexOf(search) !== -1;
});
};
self.add = function (id) {
parse(id, 'add', function (search) {
if (idsArray.indexOf(search) === -1) {
idsArray.push(search);
}
triggerAttributeChange();
});
};
self.remove = function (id) {
parse(id, 'remove', function (search) {
idsArray = idsArray.filter(function (item) {
return item !== id;
});
triggerAttributeChange();
});
};
self.toggle = function (id) {
parse(id, 'toggle', function (search) {
if (!self.contains(search)) {
self.add(search);
} else {
self.remove(search);
}
});
};
self.tokenChanged = null;
self.toString = function () {
var tokens = '',
i;
if (idsArray.length > 0) {
for (i = 0; i < idsArray.length; i = i + 1) {
tokens = tokens + idsArray[i] + ' ';
}
tokens = tokens.slice(0, tokens.length - 1);
}
return tokens;
};
};
Set an 'ids' property in your element with a new instance of this function and finally you must bound the targeted attribute to the property listening to changes to the element and updating the property o viceversa. You can do that with a mutation observer.
See firing event on DOM attribute change and https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
var attachTokenList = function (element, prop, initialValues) {
'use strict';
var initValues = initialValues || element.getAttribute(prop),
MutationObserver = window.MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
observer,
config,
cancelMutation = false;
function createTokenList(values) {
var tList = new TokenList(values);
tList.tokenChanged = function () {
element.setAttribute(prop, element[prop].toString());
cancelMutation = true;
};
element[prop] = tList;
}
createTokenList(initValues);
observer = new MutationObserver(function (mutation) {
var i,
mutationrec,
newAttr;
if (mutation.length > 0 && !cancelMutation) {
for (i = 0; i < mutation.length; i = i + 1) {
mutationrec = mutation[i];
if (mutationrec.attributeName === prop && element[prop]) {
newAttr = element.getAttribute(prop);
createTokenList(newAttr);
}
}
}
cancelMutation = false;
});
config = {
attributes: true
};
observer.observe(element, config);
};
Testing to see if it works
<so-users ids="1234 5678"></so-users>
<button onclick="clickButton1()">Add 7890</button>
<button onclick="clickButton2()">Set to 3456</button>
<button onclick="clickButton3()">Add 9876</button>
Inside a script tag
var elem = document.querySelector('so-users');
attachTokenList(elem, 'ids')
function clickButton1 () {
elem.ids.add('7890');
}
function clickButton2 () {
elem.setAttribute('ids', '3456');
}
function clickButton3 () {
elem.ids.add('9876');
}
Clicking the buttons in sequence set the ids attribute to '3456 9876'
You can get an instance of DOMTokenList with this function:
function newDOMTokenList(initialTokens) {
const tmp = document.createElement(`div`);
const classList = tmp.classList;
if (initialTokens) {
initialTokens.forEach(token => {
classList.add(token);
});
}
return classList;
}
We can 'steal' the DOMTokenList from a div, since it does not affect the current document until you choose to insert the element (for example by using insertAdjacentElement) and it will be garbage collected since we do not keep any references to the variable tmp.
Then you can use your list:
var list = newDOMTokenList(['a', 'b']);
list.add('c');
list.contains('d'); // false
list.contains('b'); // true
list.item(1) // 'b'
list instanceof DOMTokenList // true
// etc...
// render it to a string
var soUsers = document.querySelector('so-users');
soUsers.setAttribute('ids', list.toString());
You can even add a MutationObserver to the tmp element and get callbacks whenever the classList changes:
function newDOMTokenList(initialTokens, changed) {
const tmp = document.createElement('div');
const classList = tmp.classList;
if (initialTokens) {
initialTokens.forEach(token => {
classList.add(token);
});
}
if (changed) {
const observer = new MutationObserver((mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.attributeName === 'class') {
changed();
}
}
});
observer.observe(tmp, {attributes: true});
}
return classList;
}
This, however, will cause the tmp div to never be garbage collected, since the MutationObserver needs to keep a reference to it.
Utilizing Custom Elements - Adding JS properties and methods initialization approach , HTMLElement.dataset
, try
var XFooProto = Object.create(HTMLElement.prototype);
// 1. Give x-foo a foo() method.
XFooProto.contains = function(id) {
var data = JSON.parse(this.dataset.ids);
return data.some(function(_id) {
return id == _id
})
};
XFooProto.add = function(id) {
var data = JSON.parse(this.dataset.ids);
if (!this.contains(id)) {
data.push(id);
};
return data
};
XFooProto.remove = function(id) {
var data = JSON.parse(this.dataset.ids);
if (this.contains(id)) {
for (var _id in data) {
if (data[_id] === id) {
data.splice(_id, 1)
}
};
};
return data
};
XFooProto.ids = function() {
return this.dataset.ids
};
// 2. Define a property read-only "bar".
// Object.defineProperty(XFooProto, "ids", {value: this});
// 3. Register x-foo's definition.
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});
// 4. Instantiate an x-foo.
var xfoo = document.createElement('x-foo');
xfoo.dataset.ids = '["23354", "115866"]';
// 5. Add it to the page.
document.body.appendChild(xfoo);
console.log(xfoo.add("123")); // `["23354", "115866", "123"]`
console.log(xfoo.remove("123")); // `["23354", "115866"]`
console.log(xfoo.contains("123")); // `false`
console.log(xfoo.contains("23354")); // `true`
console.log(xfoo.ids()); // `["23354", "115866"]` , type : `String`
var pre = document.getElementsByTagName("pre")[0]
pre.innerText = JSON.stringify(JSON.parse(xfoo.dataset.ids), null, 4);
<pre></pre>
I want to know, if it's possible, how to check in javascript if an element has changed or an attribute of it?
I mean something like window.onhashchange for an element something like:
document.getElementById("element").onElementChange = function();
As I know onchange is something like this, but will it work if I want to know in this way:
var element = {};
element.attribute = result;
element.attribute.onchange = function();
As far as I understand you want onChange on javascript object Properties. The answer is no, it doesn't exist as far as I know.
But you can make a setter function like this (As a proof of concept):
var element = {};
element.setProperty = function(property, value) {
if (typeof(element.onChange) === 'function') {
element.onChange(property, element[property], value);
}
element[property] = value;
};
element.onChange = function(property, oldValue, newValue) {
alert(property + ' changed from ' + oldValue + ' to ' + newValue);
};
element.setProperty('something', 'Hello world!');
now you get an alert box with 'something changed from undefined to Hello World!'. And (element.something === 'Hello World!') will return true.
if you now call:
element.setProperty('something', 'Goodbye world!');
you get an alert box with 'something changed from Hello World! to Goodbye World!'.
Off course you have to set the property only via the setProperty method in all of your code if you want to capture this event!
Edit:
At some time in the future, you might be able to use Object.observe().
Edit 2:
Now there's also proxies.
You might consider a mutation observer.
to do this you first create a callback (fired when the dom element changes)
assign it to an observer var observer = new MutationObserver(callback);
Then tell the observer what to watch observer.observe('<div></div>', observerOptions);
From:
Mozilla page on Mutation Observers
I guess you'd need a way to capture the event which triggered the change in attribute rather than the change in attribute. The change in attribute could only either be due to your CSS or your javascript, both being manifestations of the user's actions.
I believe there is no such event. However, you can use setInterval or setTimeout to watch for element changes and use it to react accordingly.
I did this. It works pretty well. I would have used the setProperty method if I had known.
function searchArray(searchValue, theArray, ChangeValue){
for (var i=0; i < theArray.length; i++) {
if (theArray[i].id === searchValue) {
theArray[i].changed = ChangeValue;
}
}
}
function getArrayIindex(elementid, theArray){
for (var i=0; i < theArray.length; i++) {
if (theArray[i].id === elementid) {
return i;
}
}
}
function setInputEvents(hiddenObject) {
var element;
for (var i = 0; i < document.forms[0].length; i++) {
element = document.forms[0].elements[i];
//Check to see if the element is of type input
if (element.type in { text: 1, password: 1, textarea: 1 }) {
arrFieldList.push({
id: element.id,
changed:false,
index: i,
});
element.onfocus = function () {
if (!arrFieldList[getArrayIindex(this.id, arrFieldList)].changed) {
hiddenObject.value = this.value;
this.value = '';
}
}
element.onblur = function () {
if (this.value == '') {
this.value = hiddenObject.value;
}
}
element.onchange = function () {
searchArray(this.id, arrFieldList, true);
}
}
}
The jQuery documentation for the .toggle() method states:
The .toggle() method is provided for convenience. It is relatively straightforward to implement the same behavior by hand, and this can be necessary if the assumptions built into .toggle() prove limiting.
The assumptions built into .toggle have proven limiting for my current task, but the documentation doesn't elaborate on how to implement the same behavior. I need to pass eventData to the handler functions provided to toggle(), but it appears that only .bind() will support this, not .toggle().
My first inclination is to use a flag that's global to a single handler function to store the click state. In other words, rather than:
$('a').toggle(function() {
alert('odd number of clicks');
}, function() {
alert('even number of clicks');
});
do this:
var clicks = true;
$('a').click(function() {
if (clicks) {
alert('odd number of clicks');
clicks = false;
} else {
alert('even number of clicks');
clicks = true;
}
});
I haven't tested the latter, but I suspect it would work. Is this the best way to do something like this, or is there a better way that I'm missing?
Seems like a reasonable way to do it... I'd just suggest that you make use of jQuery's data storage utilities rather than introducing an extra variable (which could become a headache if you wanted to keep track of a whole bunch of links). So based of your example:
$('a').click(function() {
var clicks = $(this).data('clicks');
if (clicks) {
alert('odd number of clicks');
} else {
alert('even number of clicks');
}
$(this).data("clicks", !clicks);
});
Here is a plugin that implements an alternative to .toggle(), especially since it has been removed in jQuery 1.9+.
How to use:
The signature for this method is:
.cycle( functions [, callback] [, eventType])
functions [Array]: An array of functions to cycle between
callback [Function]: A function that will be executed on completion of each iteration. It will be passed the current iteration and the output of the current function. Can be used to do something with the return value of each function in the functions array.
eventType [String]: A string specifying the event types to cycle on, eg. "click mouseover"
An example of usage is:
$('a').cycle([
function() {
alert('odd number of clicks');
}, function() {
alert('even number of clicks');
}
]);
I've included a demonstration here.
Plugin code:
(function ($) {
if (!Array.prototype.reduce) {
Array.prototype.reduce = function reduce(accumulator) {
if (this === null || this === undefined) throw new TypeError("Object is null or undefined");
var i = 0,
l = this.length >> 0,
curr;
if (typeof accumulator !== "function") // ES5 : "If IsCallable(callbackfn) is false, throw a TypeError exception."
throw new TypeError("First argument is not callable");
if (arguments.length < 2) {
if (l === 0) throw new TypeError("Array length is 0 and no second argument");
curr = this[0];
i = 1; // start accumulating at the second element
} else curr = arguments[1];
while (i < l) {
if (i in this) curr = accumulator.call(undefined, curr, this[i], i, this);
++i;
}
return curr;
};
}
$.fn.cycle = function () {
var args = Array.prototype.slice.call(arguments).reduce(function (p, c, i, a) {
if (i == 0) {
p.functions = c;
} else if (typeof c == "function") {
p.callback = c;
} else if (typeof c == "string") {
p.events = c;
}
return p;
}, {});
args.events = args.events || "click";
console.log(args);
if (args.functions) {
var currIndex = 0;
function toggler(e) {
e.preventDefault();
var evaluation = args.functions[(currIndex++) % args.functions.length].apply(this);
if (args.callback) {
callback(currIndex, evaluation);
}
return evaluation;
}
return this.on(args.events, toggler);
} else {
//throw "Improper arguments to method \"alternate\"; no array provided";
}
};
})(jQuery);