keyboard events on [non-] contenteditable HTML5 elements - javascript

I'm coding the MELT monitor (free software, alpha stage, related to the GCC MELT domain specific language to customize GCC). It is using libonion to behave as a specialized web server, and I want it to become a syntax directed editor of some DSL I am designing. I'm speaking of commit 97d60053 if that matters. You could run it as ./monimelt -Dweb,run -W localhost.localdomain:8086 then open http://localhost.localdomain:8086/microedit.html in your browser.
I am emitting (thru file webroot/microedit.html)
<h1>Micro Editing Monimelt</h1>
<div id='microedit_id' contenteditable='true'>*</div>
<hr/>
then some AJAX trickery is filling that #micredit_id element with something containing stuff similar to:
<dd class='statval_cl' data-forattr='notice'> ▵
<span class='momnode_cl'>*<span class='momconn_cl'>
<span class='momitemref_cl'>comment</span></span>
(“<span class='momstring_cl'>some simple notice</span>”
<span class='momnode_cl'>*<span class='momconn_cl'>
<span class='momitemref_cl'>web_state</span></span>
(<span class='momnumber_cl'>2</span>)</span>
<span class='momitemval_cl'>hashset</span>
<span class='momset_cl'>{<span class='momitemref_cl'>microedit</span>
<span class='momitemref_cl'>the_agenda</span>}</span>
<span class='momtuple_cl'>[<span class='momitemref_cl'>web_session</span>
<span class='momitemref_cl empty_cl'>~</span>
<span class='momitemref_cl'>the_system</span>]</span>)</span> ;</dd>
Now, I want every <span> of class momitemref_cl to be sensitive to some keyboard (and perhaps mouse) events. However, the contenteditable elements can be edited by many user actions (I don't even understand what is the entire list of such user actions....) and I only want these span elements to be responsive to a defined and restricted set of key presses (alphanumerical & space) and not be able to be user-changed otherwise (e.g. no punctuation characters, no "cut", no "paste", no backspace, no tab, etc...).
Is there a complete list of events (or user actions) that a contenteditable='true' element can get and is reacting to?
How to disable most of these events or user actions (on keyboard & mouse) and react only to some (well defined) keyboard events?
Apparently, a <span> element in a non-contenteditable element cannot get any keyboard user action (because it cannot get the focus)...
I am targeting only recent HTML5 browsers, such as Firefox 38 or 42, or Chrome 47 etc... on Debian/Linux/x86-64 if that matters (so I really don't care about IE9)
PS. this is a related question, but not the same one.
PS2: Found the why contenteditable is terrible blog page. Makes me almost cry... Also read about faking an editable control in browser Javascript (for CodeMirror). See also W3C draft internal document on Editing Explainer and edit events draft. Both W3C things are work in progress. W3C TR on UI events is still (nov.2015) a working draft. See also http://jsfiddle.net/8j6jea6p/ (which behaves differently in Chrome 46 and in Firefox 42 or 43 beta)
PS3: perhaps a contenteditable is after all a bad idea. I am (sadly) considering using a canvas (à la carota) and doing all the editing & drawing by hand-written javascript...
addenda:
(November 26th 2015)
By discussing privately with some Mozilla persons, I understood that:
contenteditable is messy (so I rather avoid it), and is not anymore worked much in Firefox (for instance, even recent beta Firefox don't know about contenteditable='events', see nsGenericHTMLElement.h file)
event bubbling and capturing matters a big lot
a normal <div> (or <span>) can be made focusable by giving it a tabindex property
text selection API could be useful (but has some recent bugs)
So I probably don't need contenteditable

You can do as such:
function validateInput(usrAct){
swich(usrAct){
case "paste":
// do something when pasted
break;
case "keydown":
// dosomething on keydown
break;
default:
//do something on default
break;
}
}
document.querySelectorAll('.momitemref_cl').addEventListener('input', function(e){
validateInput(e.type)
}, false);

This snippet could be what you are looking for, making span.momitemref_cl elements focusable but not tabbable and setting has contenteditable. But as i'm testing it on chrome, contenteditable inside any container with attribute contenteditable set to true, don't fire any keyboard event. So the trick could be on focus to set any container to not editable (and switch back on blur).
See e.g: (keypress and keydown events are both binded to handle some specific cases where keypress or keydown wouldn't be fired on specifc keys)
NOTE: has you seem to populate DIV with content dynamically, you could delegate it or bind event (& set tabindex attribute if changing it in HTML markup not a solution) once ajax request has completed.
$('#microedit_id .momitemref_cl').attr('tabindex', -1).prop('contenteditable', true).on('focusin focusout', function(e) {
$(this).parents('[contenteditable]').prop('contenteditable', e.type === "focusout");
}).on('keypress keydown paste cut', function(e) {
if (/[a-zA-Z0-9 ]/.test(String.fromCharCode(e.which))) return;
return false;
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<h1>Micro Editing Monimelt</h1>
<div id='microedit_id' contenteditable='true'>
<dd class='statval_cl' data-forattr='notice'>▵ <span class='momnode_cl'>*<span class='momconn_cl'>
<span class='momitemref_cl'>comment</span></span>(“<span class='momstring_cl'>some simple notice</span>” <span class='momnode_cl'>*<span class='momconn_cl'>
<span class='momitemref_cl'>web_state</span></span>(<span class='momnumber_cl'>2</span>)</span> <span class='momitemval_cl'>hashset</span>
<span class='momset_cl'>{<span class='momitemref_cl'>microedit</span>
<span class='momitemref_cl'>the_agenda</span>}</span> <span class='momtuple_cl'>[<span class='momitemref_cl'>web_session</span>
<span class='momitemref_cl empty_cl'>~</span>
<span class='momitemref_cl'>the_system</span>]</span>)</span>;</dd>
</div>
<hr/>

First, HTMLElements become contentEditableElements when you set their contentEditable attribute to true.
Now, the best way to do your parsing IMO is to listen to the inputEvent and check your element's textContent:
s.addEventListener('input', validate, false);
function validate(evt) {
var badValues = ['bad', 'content'];
var span = this;
badValues.forEach(function(v) {
if (span.textContent.indexOf(v) > -1) {
// that's bad m..key
span.textContent = span.textContent.split(v).join('');
}
});
};
<span id="s" contentEditable="true">Hello</span>
Unfortunately, the input event isn't widely supported
so you may need to add onkeydown and onpasteand maybe onclick event handlers to catch non-supporting browsers (a.k.a IE).

Edit:
(Handles only the spans with the said class. Also handles the case, where you could go back from another span into a previous one and could delete it. Incorporates the idea of #AWolff for switching the contenteditable attribute on focus)
The overall idea remains the same as that of the previous version.
Fiddle: http://jsfiddle.net/abhitalks/gb0mbwLu/
Snippet:
var div = document.getElementById('microedit_id'),
spans = document.querySelectorAll('#microedit_id .momitemref_cl'),
commands = ['paste', 'cut'],
// whitelist is the keycodes for keypress event
whitelist = [{'range': true, 'start': '97', 'end': '122'}, // lower-case
{'range': true, 'start': '65', 'end': '90'}, // upper-case
{'range': true, 'start': '48', 'end': '57' } // numbers
],
// specialkeys is the keycodes for keydown event
specialKeys = [8, 9, 13, 46] // backspace, tab, enter, delete
;
div.addEventListener('keydown', handleFromOutside, false);
[].forEach.call(spans, function(span) {
span.setAttribute('contenteditable', true);
span.setAttribute('tabindex', '-1');
span.addEventListener('focus', handleFocus, false);
span.addEventListener('blur', handleBlur, false);
commands.forEach(function(cmd) {
span.addEventListener(cmd, function(e) {
e.preventDefault(); return false;
});
});
span.addEventListener('keypress', handlePress, false);
span.addEventListener('keydown', handleDown, false);
});
function handleFocus(e) { div.setAttribute('contenteditable', false); }
function handleBlur(e) { div.setAttribute('contenteditable', true); }
function handlePress(e) {
var allowed = false, key = e.keyCode;
whitelist.forEach(function(range) {
if (key && (key != '') && (range.start <= key) && (key <= range.end)) {
allowed = true;
}
});
if (! allowed) { e.preventDefault(); return false; }
}
function handleDown(e) {
var allowed = false, key = e.keyCode;
specialKeys.forEach(function(spl) {
if (key && (spl == key)) { e.preventDefault(); return false; }
});
}
function handleFromOutside(e) {
var key = e.keyCode, node = window.getSelection().anchorNode, prev, next;
node = (node.nodeType == 3 ? node.parentNode : node)
prev = node.previousSibling; next = node.nextSibling;
if (prev || next) {
if (node.className == 'momitemref_cl') {
if (specialKeys.indexOf(key) >= 0) {
e.preventDefault(); return false;
}
}
}
}
<h1>Micro Editing Monimelt</h1>
<div id='microedit_id' contenteditable='true'>
<dd class='statval_cl' data-forattr='notice'> ▵
<span class='momnode_cl'>*<span class='momconn_cl'>
<span class='momitemref_cl'>comment</span></span>
(“<span class='momstring_cl'>some simple notice</span>”
<span class='momnode_cl'>*<span class='momconn_cl'>
<span class='momitemref_cl'>web_state</span></span>
(<span class='momnumber_cl'>2</span>)</span>
<span class='momitemval_cl'>hashset</span>
<span class='momset_cl'>{<span class='momitemref_cl'>microedit</span>
<span class='momitemref_cl'>the_agenda</span>}</span>
<span class='momtuple_cl'>[<span class='momitemref_cl'>web_session</span>
<span class='momitemref_cl empty_cl'>~</span>
<span class='momitemref_cl'>the_system</span>]</span>)</span> ;</dd>
</div>
<hr/>
Apart from the usual handling of events on the spans and preventing / allowing the keys and/or commands from the white-lists and balck-lists; what this code does is to also check if the cursor or editing is currently being done on other spans which are not constrained. When selecting or moving using arrow keys from there into the target spans, we dis-allow special keys to prevent deletion etc.
function handleFromOutside(e) {
var key = e.keyCode, node = window.getSelection().anchorNode, prev, next;
node = (node.nodeType == 3 ? node.parentNode : node)
prev = node.previousSibling; next = node.nextSibling;
if (prev || next) {
if (node.className == 'momitemref_cl') {
if (specialKeys.indexOf(key) >= 0) {
e.preventDefault(); return false;
}
}
}
}
I could not get much time, and thus one problem still remains. And, that is to disallow commands as well like cut and paste while moving into the target spans from outside.
Older version for reference only:
You could maintain a white-list (or blacklist if number of commands allowed are higher) of all keystrokes that you want to allow. Similarly, also maintain a dictionary of all events that you want to block.
Then wire up the commands on your div and use event.preventDefault() to reject that action. Next up, wire up the keydown event and use the whitelist to allow all keystrokes that are in the permissible ranges as defined above:
In the example below only numbers and alphabets will be allowed as per the first range and arrow keys (along with pageup/down and space) will be allowed as per the second range. Rest all actions are blocked / rejected.
You can then extend it further to your use-case. Try it out in the demo below.
Fiddle: http://jsfiddle.net/abhitalks/re7ucgra/
Snippet:
var div = document.getElementById('microedit_id'),
spans = document.querySelectorAll('#microedit_id span'),
commands = ['paste'],
whitelist = [ {'start': 48, 'end': 90}, {'start': 32, 'end': 40 }, ]
;
commands.forEach(function(cmd) {
div.addEventListener(cmd, function(e) {
e.preventDefault(); return false;
});
});
div.addEventListener('keydown', handleKeys, false);
function handleKeys(e) {
var allowed = false;
whitelist.forEach(function(range) {
if ((range.start <= e.keyCode) && (e.keyCode <= range.end)) {
allowed = true;
}
});
if (! allowed) { e.preventDefault(); return false; }
};
<h1>Micro Editing Monimelt</h1>
<div id='microedit_id' contenteditable='true'>
<dd class='statval_cl' data-forattr='notice'> ▵
<span class='momnode_cl'>*<span class='momconn_cl'>
<span class='momitemref_cl'>comment</span></span>
(“<span class='momstring_cl'>some simple notice</span>”
<span class='momnode_cl'>*<span class='momconn_cl'>
<span class='momitemref_cl'>web_state</span></span>
(<span class='momnumber_cl'>2</span>)</span>
<span class='momitemval_cl'>hashset</span>
<span class='momset_cl'>{<span class='momitemref_cl'>microedit</span>
<span class='momitemref_cl'>the_agenda</span>}</span>
<span class='momtuple_cl'>[<span class='momitemref_cl'>web_session</span>
<span class='momitemref_cl empty_cl'>~</span>
<span class='momitemref_cl'>the_system</span>]</span>)</span> ;</dd>
</div>
<hr/>
Edited, to fix the problem of not capturing special keys especially when shift was pressed and the same keyCode is generated for keypress. Added, keydown for handling special keys.
Note: This is assuming that to happen on the entire div. As I see in the question, there are only spans and that too nested ones. There are no other elements. If there are other elements involved and you want to exempt those, then you will need to bind the event to those elements only. This is because, the events on children are captured by the parent when parent is contenteditable and not fired on the children.

A straightforward solution to your problem would be to listen on the keydown event fired by the inner-most element and act accordingly. An exemplary code snippet can be found below:
HTML:
<div class="momitemref_cl" contenteditable="true">Foo Bar</div>
<input class="not-momitemref_cl"/>
<input class="momitemref_cl"/>
JS:
document.querySelectorAll('.momitemref_cl').forEach((el) => {
el.addEventListener('keydown', validateInput);
el.addEventListener('cut', e => e.preventDefault());
el.addEventListener('copy', e => e.preventDefault());
el.addEventListener('paste', e => e.preventDefault());
});
function validateInput(userAction) {
console.log(userAction);
if (userAction.ctrlKey) {
userAction.preventDefault();
return false;
}
let code = (userAction.keyCode ? userAction.keyCode : userAction.which);
if ((48 <= code && code <= 57 && !userAction.shiftKey) || (65 <= code && code <= 90) || (97 <= code && code <= 122) || code === 32) {
console.log(`Allowing keypress with code: ${code}`);
return true;
}
console.log(`Preventing keypress with code: ${code}`);
userAction.preventDefault();
return false;
}
This works for both <input> elements as well as elements with the contenteditable attribute set to true.
JS Fiddle: https://jsfiddle.net/rsjw3c87/22/
EDIT: Also added additional checks to prevent right-click & copy/cut/paste. Disabling right-click directly via the contextmenu event will not work as certain browsers & OSes disallow you from disabling that specific event.

Related

i want value from input field which is using template drive form of angular and giving me previously typed value [duplicate]

I am trying to get the text in a text box as the user types in it (jsfiddle playground):
function edValueKeyPress() {
var edValue = document.getElementById("edValue");
var s = edValue.value;
var lblValue = document.getElementById("lblValue");
lblValue.innerText = "The text box contains: " + s;
//var s = $("#edValue").val();
//$("#lblValue").text(s);
}
<input id="edValue" type="text" onKeyPress="edValueKeyPress()"><br>
<span id="lblValue">The text box contains: </span>
​
The code runs without errors, except that the value of the input text box, during onKeyPress is always the value before the change:
Question: How do I get the text of a text box during onKeyPress?
Bonus Chatter
There are three events related to "the user is typing" in the HTML DOM:
onKeyDown
onKeyPress
onKeyUp
In Windows, the order of WM_Key messages becomes important when the user holds down a key, and the key begins to repeat:
WM_KEYDOWN('a') - user has pushed down the A key
WM_CHAR('a') - an a character has been received from the user
WM_CHAR('a') - an a character has been received from the user
WM_CHAR('a') - an a character has been received from the user
WM_CHAR('a') - an a character has been received from the user
WM_CHAR('a') - an a character has been received from the user
WM_KEYUP('a') - the user has released the A key
Will result in five characters appearing in a text control: aaaaa
The important point being that the you respond to the WM_CHAR message, the one that repeats. Otherwise you miss events when a key is pressed.
In HTML things are slightly different:
onKeyDown
onKeyPress
onKeyDown
onKeyPress
onKeyDown
onKeyPress
onKeyDown
onKeyPress
onKeyDown
onKeyPress
onKeyUp
Html delivers an KeyDown and KeyPress every key repeat. And the KeyUp event is only raised when the user releases the key.
Take aways
I can respond to onKeyDown or onKeyPress, but both are still raised before the input.value has been updated
I cannot respond to onKeyUp, because it doesn't happen as the text in the text-box changes.
Question: How do I get the text of a text-box during onKeyPress?
Bonus Reading
Getting a form value with jQuery
Get the value in an input text box
Keep it simple. Use both onKeyPress() and onKeyUp():
<input id="edValue" type="text" onKeyPress="edValueKeyPress()" onKeyUp="edValueKeyPress()">
This takes care of getting the most updated string value (after key up) and also updates if the user holds down a key.
jsfiddle: http://jsfiddle.net/VDd6C/8/
Handling the input event is a consistent solution: it is supported for textarea and input elements in all contemporary browsers and it fires exactly when you need it:
function edValueKeyPress() {
var edValue = document.getElementById("edValue");
var s = edValue.value;
var lblValue = document.getElementById("lblValue");
lblValue.innerText = "The text box contains: " + s;
}
<input id="edValue" type="text" onInput="edValueKeyPress()"><br>
<span id="lblValue">The text box contains: </span>
I'd rewrite this a bit, though:
function showCurrentValue(event)
{
const value = event.target.value;
document.getElementById("label").innerText = value;
}
<input type="text" onInput="showCurrentValue(event)"><br>
The text box contains: <span id="label"></span>
the value of the input text box, during onKeyPress is always the value before the change
This is on purpose: This allows the event listener to cancel the keypress.
If the event listeners cancels the event, the value is not updated. If the event is not canceled, the value is updated, but after the event listener was called.
To get the value after the field value has been updated, schedule a function to run on the next event loop. The usual way to do this is to call setTimeout with a timeout of 0:
$('#field').keyup(function() {
var $field = $(this);
// this is the value before the keypress
var beforeVal = $field.val();
setTimeout(function() {
// this is the value after the keypress
var afterVal = $field.val();
}, 0);
});
Try here: http://jsfiddle.net/Q57gY/2/
Edit: Some browsers (e.g. Chrome) do not trigger keypress events for backspace; changed keypress to keyup in code.
keep it Compact.
Each time you press a key, the function edValueKeyPress() is called.
You've also declared and initialized some variables in that function - which slow down the process and requires more CPU and memory as well.
You can simply use this code - derived from simple substitution.
function edValueKeyPress()
{
document.getElementById("lblValue").innerText =""+document.getElementById("edValue").value;
}
That's all you want, and it's faster!
<asp:TextBox ID="txtMobile" runat="server" CssClass="form-control" style="width:92%; margin:0px 5px 0px 5px;" onkeypress="javascript:return isNumberKey(event);" MaxLength="12"></asp:TextBox>
<script>
function isNumberKey(evt) {
var charCode = (evt.which) ? evt.which : event.keyCode;
if (charCode > 31 && (charCode < 48 || charCode > 57)) {
return false;
}
return true;
}
</script>
None of the answers so far offer a complete solution. There are quite a few issues to address:
Not all keypresses are passed onto keydown and keypress handlers (e.g. backspace and delete keys are suppressed by some browsers).
Handling keydown is not a good idea. There are situations where a keydown does NOT result in a keypress!
setTimeout() style solutions get delayed under Google Chrome/Blink web browsers until the user stops typing.
Mouse and touch events may be used to perform actions such as cut, copy, and paste. Those events will not trigger keyboard events.
The browser, depending on the input method, may not deliver notification that the element has changed until the user navigates away from the field.
A more correct solution will handle the keypress, keyup, input, and change events.
Example:
<p><input id="editvalue" type="text"></p>
<p>The text box contains: <span id="labelvalue"></span></p>
<script>
function UpdateDisplay()
{
var inputelem = document.getElementById("editvalue");
var s = inputelem.value;
var labelelem = document.getElementById("labelvalue");
labelelem.innerText = s;
}
// Initial update.
UpdateDisplay();
// Register event handlers.
var inputelem = document.getElementById("editvalue");
inputelem.addEventListener('keypress', UpdateDisplay);
inputelem.addEventListener('keyup', UpdateDisplay);
inputelem.addEventListener('input', UpdateDisplay);
inputelem.addEventListener('change', UpdateDisplay);
</script>
Fiddle:
http://jsfiddle.net/VDd6C/2175/
Handling all four events catches all of the edge cases. When working with input from a user, all types of input methods should be considered and cross-browser and cross-device functionality should be verified. The above code has been tested in Firefox, Edge, and Chrome on desktop as well as the mobile devices I own.
I normally concatenate the field's value (i.e. before it's updated) with the key associated with the key event. The following uses recent JS so would need adjusting for support in older IE's.
Recent JS example
document.querySelector('#test').addEventListener('keypress', function(evt) {
var real_val = this.value + String.fromCharCode(evt.which);
if (evt.which == 8) real_val = real_val.substr(0, real_val.length - 2);
alert(real_val);
}, false);
Support for older IEs example
//get field
var field = document.getElementById('test');
//bind, somehow
if (window.addEventListener)
field.addEventListener('keypress', keypress_cb, false);
else
field.attachEvent('onkeypress', keypress_cb);
//callback
function keypress_cb(evt) {
evt = evt || window.event;
var code = evt.which || evt.keyCode,
real_val = this.value + String.fromCharCode(code);
if (code == 8) real_val = real_val.substr(0, real_val.length - 2);
}
[EDIT - this approach, by default, disables key presses for things like back space, CTRL+A. The code above accommodates for the former, but would need further tinkering to allow for the latter, and a few other eventualities. See Ian Boyd's comment below.]
easy...
In your keyPress event handler, write
void ValidateKeyPressHandler(object sender, KeyPressEventArgs e)
{
var tb = sender as TextBox;
var startPos = tb.SelectionStart;
var selLen= tb.SelectionLength;
var afterEditValue = tb.Text.Remove(startPos, selLen)
.Insert(startPos, e.KeyChar.ToString());
// ... more here
}
So there are advantages and disadvantages to each event. The events onkeypress and onkeydown don't retrieve the latest value, and onkeypress doesn't fire for non-printable characters in some browsers. The onkeyup event doesn't detect when a key is held down for multiple characters.
This is a little hacky, but doing something like
function edValueKeyDown(input) {
var s = input.value;
var lblValue = document.getElementById("lblValue");
lblValue.innerText = "The text box contains: "+s;
//var s = $("#edValue").val();
//$("#lblValue").text(s);
}
<input id="edValue" type="text" onkeydown="setTimeout(edValueKeyDown, 0, this)" />
seems to handle the best of all worlds.
By using event.key we can get values prior entry into HTML Input Text Box. Here is the code.
function checkText()
{
console.log("Value Entered which was prevented was - " + event.key);
//Following will prevent displaying value on textbox
//You need to use your validation functions here and return value true or false.
return false;
}
<input type="text" placeholder="Enter Value" onkeypress="return checkText()" />
Try to concatenate the event charCode to the value you get.
Here is a sample of my code:
<input type="text" name="price" onkeypress="return (cnum(event,this))" maxlength="10">
<p id="demo"></p>
js:
function cnum(event, str) {
var a = event.charCode;
var ab = str.value + String.fromCharCode(a);
document.getElementById('demo').innerHTML = ab;
}
The value in ab will get the latest value in the input field.
There is a better way to do this. Use the concat Method. Example
declare a global variable. this works good on angular 10, just pass it to Vanilla JavaScript. Example:
HTML
<input id="edValue" type="text" onKeyPress="edValueKeyPress($event)"><br>
<span id="lblValue">The text box contains: </span>
CODE
emptyString = ''
edValueKeyPress ($event){
this.emptyString = this.emptyString.concat($event.key);
console.log(this.emptyString);
}

How to copy an event listener in plain Javascript

So I am making some overrides on a Wordpress plugin. I need to copy the event listener on an element and then replace the element and add it back. The event listener is generated by the plugin.
I thought getEventListeners() would work but I have read that it only works in console. If that is this case I'm really astounded. We're in freaking 2020 and I am not finding an obvious solution to this.
What is the solution here people?
Below is the code I was trying to implement having assumed getEventListeners wasn't just a console function.
// Edit Affirm
(function replaceAffirm() {
if (document.querySelector(".affirm-modal-trigger")) {
const learnMore = document.querySelector("#learn-more");
const modalTrigger = document.querySelector(".affirm-modal-trigger");
const clickHandler = getEventListeners(modalTrigger).click[0].listener;
const substr = learnMore.innerHTML
.toString()
.substring(
learnMore.innerHTML.indexOf("h") + 2,
learnMore.innerHTML.length
);
learnMore.innerHTML = "Easy Financing with " + substr;
modalTrigger.addEventListener("click", clickHandler);
} else {
setTimeout(function () {
replaceAffirm();
}, 250);
}
})();
HTML
<p id="learn-more" class="affirm-as-low-as" data-amount="20000" data-affirm-color="white" data-learnmore-show="true" data-page-type="product">
Starting at
<span class="affirm-ala-price">$68</span>
/mo with
<span class="__affirm-logo __affirm-logo-white __ligature__affirm_full_logo__ __processed">Affirm</span>.
<a class="affirm-modal-trigger" aria-label="Prequalify Now (opens in modal)" href="javascript:void(0)">Prequalify now</a>
</p>
You can't copy event listeners, but it seems because of the structure of your HTML it's more likely that you shouldn't need to re-add it. Instead of editing the HTML and removing the event listener by doing so, the best bet would be to edit around it.
If you want to remove the text nodes you can iterate through childNodes and separate out what should be removed.
Then to rebuild the appropriate text where you want it you can use insertAdjacentText
if (document.querySelector(".affirm-modal-trigger")) {
const learnMore = document.querySelector("#learn-more");
const modalTrigger = document.querySelector(".affirm-modal-trigger");
const children = Array.from(learnMore.childNodes);
children.forEach(child => {
if (child.nodeType === Node.TEXT_NODE || child.matches(".affirm-ala-price")) {
if (learnMore.contains(child)) {
learnMore.removeChild(child);
}
}
});
learnMore.insertAdjacentText("afterBegin", "Easy Financing With ");
modalTrigger.insertAdjacentText("beforeBegin", " ");
} else {
setTimeout(function() {
replaceAffirm();
}, 250);
}
<p id="learn-more" class="affirm-as-low-as" data-amount="20000" data-affirm-color="white" data-learnmore-show="true" data-page-type="product">
Starting at
<span class="affirm-ala-price">$68</span> /mo with
<span class="__affirm-logo __affirm-logo-white __ligature__affirm_full_logo__ __processed">Affirm</span>.
<a class="affirm-modal-trigger" aria-label="Prequalify Now (opens in modal)" href="javascript:void(0)">Prequalify now</a>
</p>
Yes waiting for the Html element to be loaded and checking until it gets loaded is okay and this is one of the correct ways to wait for it.
As per my understanding of your issue, you just have to change the text of the learn-more element.
for that, it is not necessary to copy event listener and then again binding it.
Instead of replacing the whole element just change the text keeping the same element.
So it gets binded with the event listener by default.

Check that two elements aren't in focus

I have to input elements #Core and #Price that cannot share a parent element. I need to test that both of them are not in focus and when they are not run a function.
I thought I could just do a check to make sure when one blurs the other isn't given focus like so:
$('#Core').blur(function() {
if(!$("#Price").is(":focus")) {
getSellingPrice()
}
});$('#Price').blur(function() {
if(!$("#Core").is(":focus")) {
getSellingPrice()
}
});
But this seems to fire even when I give focus to the other input node. My guess is that when the blur happens it checks the seconds focus before its yet been selected. But I am uncertain as to how to change the code to get the desired behavior of making sure both elements aren't in focus before triggering the function call.
Any ideas on how I might accomplish this or insight into why this current code isn't working are greatly appreciated.
You can just check that the active element is neither
var elems = $('#Core, #Price').blur(function() {
setTimeout(function() {
if ( elems.toArray().indexOf(document.activeElement) === -1 ) {
getSellingPrice();
}
});
});
But you'll need a timeout to make sure the focus is set etc.
Blurring an element passes focus to the body element before transferring it to a sub element clicked. Horrible as may be a one shot timer can be used to decouple blurring with checking what element was clicked. Concept code triggered by blur events (translate to your coding standards, jQuery and application as needed):
function blurred(el)
{
setTimeout(checkFocus, 4); // ( 4 == Firefox default minimum)
}
function checkFocus()
{ var a = document.getElementById("a");
var b = document.getElementById("b");
var infocus = document.activeElement === a || document.activeElement === b;
if(infocus)
console.log( "One of them is in focus");
else
console.log(" Neither is in focus");
}
HTML
a: <input id="a" type="text" onblur="blurred(this)"><br>
b: <input id="b" type="text" onblur="blurred(this)">

jQuery highlight plugin cancels text selection, making copy impossible while also rendering links unclickable

I'm using the jQuery Highlight plugin to select some text on a web page.
I've hooked up selecting and deselecting with mouse events:
document.addEventListener('mouseup', doSelect);
document.addEventListener('mousedown', doDeselect);
The functions are:
function doSelect() {
var selectionRange = window.getSelection();
var selection = selectionRange.toString();
if (selection.trim().length > 0) {
$('body').highlight(selection);
}
}
function doDeselect() {
$('body').unhighlight();
}
Short and easy. The library searches for the selected text and wraps each occurrence in a <span> and so the text stands out.
It's working great, but I have two issues with how it behaves.
The problem is that once the span elements are applied, I cannot click hyperlinks (the ones that were found/selected), they don't react to clicks (I have to deselect the text first).
Once the span elements are added, the original selection is somehow lost, i.e. I cannot copy what I selected with CTRL+C.
These issues can be seen in this jsfiddle.
Why is this happening?
The code
The working demo is available here: jsfiddle
JavaScript
var $body = $('body');
var $copyArea = $('#copyArea');
document.addEventListener('mouseup', doSelect);
document.addEventListener('mousedown', doDeselect);
document.addEventListener('keydown', keyPressHandler);
function keyPressHandler(e) {
if(e.ctrlKey && e.keyCode == 67) {
$copyArea.focus().select();
}
}
function doSelect() {
var selectionRange = window.getSelection();
var selection = selectionRange.toString();
if (selection.trim().length > 0) {
$copyArea.val(selection);
$body.highlight(selection);
}
}
function doDeselect(e) {
var elem = $(e.target).parents('a');
if(elem.length == 0) {
$copyArea.val('');
$body.unhighlight();
}
}
HTML
Sample text to select.
<br/>Sample text to select.
<br/>Sample text to select.
<br/>google.com
google.com
<a href="http://google.com" target="_blank">
<span>
<span>google.com</span>
</span>
</a>
<textarea id="copyArea"></textarea>
CSS
.highlight {
background-color: #FFFF88;
}
#copyArea {
position:fixed;
top:-999px;
height:0px;
}
Part 1 - Clicking through the selection
Presumably, the reason clicking on a highlighted link doesn't work is because the process that disables the highlighting kicks in first and cancels the click.
To bypass that, we implement a condition that checks if the target element of the mousedown event has an a element as ancestor. If that is true, we simply do not execute $body.unhighlight();, allowing the click to pass through and open the link.
function doDeselect(e) {
var elem = $(e.target).parents('a');
if(elem.length == 0) {
$copyArea.val('');
$body.unhighlight();
}
}
Part 2 - Copying the selection
Presumably, again, the reason the selection is lost is because the document is modified by the highlighting, which introduces elements into the DOM.
My first idea was to reapply the selection after the modification was done. This became annoying and I went in a different direction, which allowed me to stumble upon this:
The Definitive Guide to Copying and Pasting in JavaScript
This offered a simple and efficient idea: using an hidden element that could contain selectable text.
Therefore, to allow copying the selected text that we highlighted despite having lost the original selection:
We add a hidden textarea element to our document.
<textarea id="copyArea"></textarea>
We get a reference to that element.
var $copyArea = $('#copyArea');
We add an event handler for the keydown event.
document.addEventListener('keydown', keyPressHandler);
We add the event handler.
function keyPressHandler(e) {
if(e.ctrlKey && e.keyCode == 67) {
$copyArea.focus().select();
}
}
We modify doSelect() to add some logic that will set the selection as the value of the textarea element, in the form of $copyArea.val(selection);.
function doSelect() {
var selectionRange = window.getSelection();
var selection = selectionRange.toString();
if (selection.trim().length > 0) {
$copyArea.val(selection);
$body.highlight(selection);
}
}
What does the handler do ? it captures the combination CTRL+C and focuses on the text in the hidden textarea, which ends up being copied by the keyboard command we just issued.

Touch events in JavaScript perform two different actions

Since I experienced a very strange issue with different touch event libraries (like hammer.js and quo.js) I decided to develop the events I need on my own. The Issue I'm talking about is that a touch is recognized twice if a hyperlink appears on the spot where I touched the screen. This happens on iOS as well as Android.
Imagine an element on a web page that visually changes the content of the screen if you touch it. And if the new page shows a hyperlink (<a href="">) at the same spot where I touched the screen before that new hyperlink gets triggered as well.
Now I developed my own implementation and I noticed: I'm having the same problem! Now is the question: Why?
What I do is the following (yes, I'm using jQuery):
(see source code below #1)
This function is only used with some special elements, not hyperlinks. So hyperlinks still have the default behavior.
The problem only affects hyperlinks. It doesn't occur on other elements that use the event methods showed above.
So I can imagine that not a click event is fired on the same element I touch but a click 'action' is performed at the same spot where I touched the screen after the touch event was processed. At least this is what it feels like. And since I only catch the click event on the element I actually touch I don't catch the click event on the hyperlink - and actually that shouldn't be necessary.
Does anyone know what causes this behavior?
Full source codes
#1 - attatch event to elements
$elements is a jQuery object returned by $( selector );
callback is the function that should be called if a tap is detected
helper.registerTapEvent = function($elements, callback) {
var touchInfo = {
maxTouches: 0
};
function evaluate(oe) {
var isSingleTouch = touchInfo.maxTouches === 1,
positionDifferenceX = Math.abs(touchInfo.startX - touchInfo.endX),
positionDifferenceY = Math.abs(touchInfo.startY - touchInfo.endY),
isAlmostSamePosition = positionDifferenceX < 15 && positionDifferenceY < 15,
timeDifference = touchInfo.endTime - touchInfo.startTime,
isShortTap = timeDifference < 350;
if (isSingleTouch && isAlmostSamePosition && isShortTap) {
if (typeof callback === 'function') callback(oe);
}
}
$elements
.on('touchstart', function(e) {
var oe = e.originalEvent;
touchInfo.startTime = oe.timeStamp;
touchInfo.startX = oe.changedTouches.item(0).clientX;
touchInfo.startY = oe.changedTouches.item(0).clientY;
touchInfo.maxTouches = oe.changedTouches.length > touchInfo.maxTouches ? oe.touches.length : touchInfo.maxTouches;
})
.on('touchend', function(e) {
var oe = e.originalEvent;
oe.preventDefault();
touchInfo.endTime = oe.timeStamp;
touchInfo.endX = oe.changedTouches.item(0).clientX;
touchInfo.endY = oe.changedTouches.item(0).clientY;
if (oe.touches.length === 0) {
evaluate(oe);
}
})
.on('click', function(e) {
var oe = e.originalEvent;
oe.preventDefault();
});
}
#2 - the part how the page transition is done
$subPageElem is a jQuery object of the page that should be displayed
$subPageElem.prev() returns the element of the current page that hides (temporarily) when a new page shows up.
$subPageElem.css({
webkitTransform: 'translate3d(0,0,0)',
transform: 'translate3d(0,0,0)'
}).prev().css({
webkitTransform: 'translate3d(-5%,0,-100px)',
transform: 'translate3d(-5%,0,-100px)'
});
I should also mention that the new page $subPageElem is generated dynamically and inserted into the DOM. I. e. that link that gets triggered (but shouldn't) doesn't even exist in the DOM when I touch/release the screen.

Categories

Resources