Javascript - Get element behind caret in child elements of [contenteditable] - javascript

I am building a simplistic and easy-to-use text editor in Javascript, basically a WYSIWYG editor. I will be using the contenteditable attribute to the main wrapper (.wrapper). When you press enter inside the .wrapper, a new <p> element with a unique id gets appended to the wrapper.
I need a way to fetch which child element of the .wrapper that is currently selected (i.e., being focused or having the caret/text marker inside of it).
I've searched for days without any results, and I've tried using document.elementFromPoint() but without any proper results.
When using $(":focus"), I get the entire .wrapper and not the specific child element.
Edit:
Example HTML structure:
<div class="container t-wrapper" contenteditable>
</div>
Example Javascript code:
$(document).ready(function() {
$currentElement = $("[contenteditable]");
$(".t-wrapper").each(function() {
var id = generateIdentifier(6); // Ignore this
/* This creates the initial <p> child element, where you start typing. */
$(this).html('<p class="t-empty t-first" id="' + id + '"></p>');
$(this).on("mouseup", function() {
/* $currentElement = whatever element the caret is inside. */
});
$(this).on("keyup", function() {
/* $currentElement = whatever element the caret is inside. */
});
));
});
Edit 2:
I managed to get it fairly working with the mouseup event, since you're actually clicking on something. But I need this to work when moving the caret using the keyboard. Alternatively, I need some way to get the position of the caret in pixels, and then use document.elementFromPoint() to get the specific element.

:focus doesn't select your elements because they are not focusable.
You can make them focusable by adding tabindex="-1" in HTML, or tabIndex = -1 in JS.
var generateIdentifier = Math.random;
var currentElement = document.querySelector("[contenteditable]");
[].forEach.call(document.querySelectorAll(".t-wrapper"), function(el) {
var first = document.createElement('p');
first.className = "t-empty t-first";
first.id = generateIdentifier(6);
first.textContent = 'Press enter to create new paragraphs';
first.tabIndex = -1;
el.appendChild(first);
});
.container > :focus {
border: 1px solid blue;
}
<div class="container t-wrapper" contenteditable></div>
It seems that if you add it to the first paragraph, new paragraphs obtain it automatically. But if you want to be sure, I guess you could use a mutation observer or a keyup event listener to detect paragraphs without tabindex, and add it:
el.addEventListener("keyup", function(e) {
var newChild = el.querySelector('p:not([tabindex])');
if(newChild) newChild.tabIndex = -1;
});

document.activeElement will return the currently focused element.

Related

How do I dispatch a mousedown/click event on the window in JavaScript? [duplicate]

I want a function that tells me which element the mouse cursor is over.
So, for example, if the user's mouse is over this textarea (with id wmd-input), calling window.which_element_is_the_mouse_on() will be functionally equivalent to $("#wmd-input").
DEMO
There's a really cool function called document.elementFromPoint which does what it sounds like.
What we need is to find the x and y coords of the mouse and then call it using those values:
document.addEventListener('mousemove', e => {
console.clear()
console.log( document.elementFromPoint(e.clientX, e.clientY) )
}, {passive: true})
[class^='level']{
width: 100px;
height: 100px;
padding: 15px;
background: #00000033;
}
<div class='level-1'>
<div class='level-2'>
<div class='level-3'>
Hover
</div>
</div>
</div>
document.elementFromPoint
jQuery event object
In newer browsers, you could do the following:
document.querySelectorAll( ":hover" );
That'll give you a NodeList of items that the mouse is currently over in document order. The last element in the NodeList is the most specific, each preceding one should be a parent, grandparent, and so on.
Although the following may not actually answering the question, since this is the first result of googling (the googler may not asking exactly the same question:), hope it will provide some extra input.
There are actually two different approaches to get a list of all elements the mouse is currently over (for newer browsers, perhaps):
The "structural" approach - Ascending DOM tree
As in dherman's answer, one can call
var elements = document.querySelectorAll(':hover');
However, this assumes that only children will overlay their ancestors, which is usually the case, but not true in general, especially when dealing with SVG where element in different branches of the DOM tree may overlap each other.
The "visual" approach - Based on "visual" overlapping
This method uses document.elementFromPoint(x, y) to find the topmost element, temporarily hide it (since we recover it immediately in the same context, the browser will not actually renders this), then go on to find the second topmost element... Looks a little hacky, but it returns what you expect when there are, e.g., siblings elements in a tree occluding each other. Please find this post for more details,
function allElementsFromPoint(x, y) {
var element, elements = [];
var old_visibility = [];
while (true) {
element = document.elementFromPoint(x, y);
if (!element || element === document.documentElement) {
break;
}
elements.push(element);
old_visibility.push(element.style.visibility);
element.style.visibility = 'hidden'; // Temporarily hide the element (without changing the layout)
}
for (var k = 0; k < elements.length; k++) {
elements[k].style.visibility = old_visibility[k];
}
elements.reverse();
return elements;
}
Try both, and check their different returns.
elementFromPoint() gets only the first element in DOM tree. This is mostly not enough for developers needs. To get more than one element at e.g. the current mouse pointer position, this is the function you need:
document.elementsFromPoint(x, y) . // Mind the 's' in elements
This returns an array of all element objects under the given point.
Just pass the mouse X and Y values to this function.
More information is here: DocumentOrShadowRoot.elementsFromPoint()
For very old browsers which are not supported, you may use this answer as a fallback.
The following code will help you to get the element of the mouse pointer. The resulted elements will display in the console.
document.addEventListener('mousemove', function(e) {
console.log(document.elementFromPoint(e.pageX, e.pageY));
})
Mouseover events bubble, so you can put a single listener on the body and wait for them to bubble up, then grab the event.target or event.srcElement:
function getTarget(event) {
var el = event.target || event.srcElement;
return el.nodeType == 1? el : el.parentNode;
}
<body onmouseover="doSomething(getTarget(event));">
You can look at the target of the mouseover event on some suitable ancestor:
var currentElement = null;
document.addEventListener('mouseover', function (e) {
currentElement = e.target;
});
Here’s a demo.
Demo :D
Move your mouse in the snippet window :D
<script>
document.addEventListener('mouseover', function (e) {
console.log ("You are in ", e.target.tagName);
});
</script>
<!-- One simple solution to your problem could be like this: -->
<div>
<input type="text" id="fname" onmousemove="javascript: alert(this.id);" />
<!-- OR -->
<input type="text" id="fname" onclick="javascript: alert(this.id);" />
</div>
<!-- Both mousemove over the field & click on the field displays "fname"-->
<!-- Works fantastic in IE, FireFox, Chrome, Opera. -->
<!-- I didn't test it for Safari. -->
You can use this selector to undermouse object and then manipulate it as a jQuery object:
$(':hover').last();
2022 Update:
document.elementsFromPoint() (Note the 's' in elements) is compatible with all major browsers. It basically does the same thing that elementFrompoint does, but retrieves all the elements in DOM order.
Mozilla has a good example of this:
HTML
<div>
<p>Some text</p>
</div>
<p>Elements at point 30, 20:</p>
<div id="output"></div>
JavaScript
let output = document.getElementById("output");
if (document.elementsFromPoint) {
let elements = document.elementsFromPoint(30, 20);
for (var i = 0; i < elements.length; i++) {
output.textContent += elements[i].localName;
if (i < elements.length - 1) {
output.textContent += " < ";
}
}
} else {
output.innerHTML = "<span style=\"color: red;\">" +
"Browser does not support <code>document.elementsFromPoint()</code>" +
"</span>";
}
Output
Some text
Elements at point 30, 20:
p < div < body < html
https://developer.mozilla.org/en-US/docs/Web/API/Document/elementsFromPoint
The target of the mousemove DOM event is the top-most DOM element under the cursor when the mouse moves:
(function(){
//Don't fire multiple times in a row for the same element
var prevTarget=null;
document.addEventListener('mousemove', function(e) {
//This will be the top-most DOM element under cursor
var target=e.target;
if(target!==prevTarget){
console.log(target);
prevTarget=target;
}
});
})();
This is similar to #Philip Walton's solution, but doesn't require jQuery or a setInterval.
Here's a solution for those that may still be struggling. You want to add a mouseover event on the 'parent' element of the child element(s) you want detected. The below code shows you how to go about it.
const wrapper = document.getElementById('wrapper') //parent element
const position = document.getElementById("displaySelection")
wrapper.addEventListener('mousemove', function(e) {
let elementPointed = document.elementFromPoint(e.clientX, e.clientY)
console.log(elementPointed)
});
Demo on CodePen
Let me start out by saying that I don't recommend using the method I'm about to suggest. It's much better to use event driven development and bind events only to the elements you're interested in knowing whether or not the mouse is over with mouseover, mouseout, mouseenter, mouseleave, etc.
If you absolutely must have the ability to know which element the mouse is over, you'd need to write a function that binds the mouseover event to everything in the DOM, and then store whatever the current element is in some variable.
You could so something like this:
window.which_element_is_the_mouse_on = (function() {
var currentElement;
$("body *").on('mouseover', function(e) {
if(e.target === e.currentTarget) {
currentElement = this;
}
});
return function() {
console.log(currentElement);
}
}());
Basically, I've created an immediate function which sets the event on all elements and stores the current element within the closure to minimize your footprint.
Here's a working demo that calls window.which_element_is_the_mouse_on every second and logs what element the mouse is currently over to the console.
http://jsfiddle.net/LWFpJ/1/

How to set caret to newly added element in contentEditable element?

I am working on a WYSIWYG editor in which I want to add <p>'s when hit Enter / Return key and then the user will write to this new <p>.
Right now I am having issue setting the caret to this new <p>.
$('#content').on('keypress', function(e){
if(e.which === 13){
console.log('enter pressed');
e.preventDefault();
var range = window.getSelection().getRangeAt(0);
var element = document.createElement('p');
// element.textContent = 'lorem'; // gets added in the right position
range.insertNode(element);
// range.setStart(element); // doesn't work
}
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p id='content' contentEditable=true>test</p>
I need to get this working in Chrome for now.
How do I fix this?
A simple solution if you don't absolutely need the top contenteditable element to be a p element, is to add a contenteditable div as parent of your p element. Enter will automatically add p elements.
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id='content' contentEditable=true><p>test</p></div>

How to highlight an editable word in dynamically generated text?

Intro
I am creating a content editor in which I want to add the functionality to choose a word which you would like to be highlighted while typing your content.
At this moment I achieved to search any word chosen in the #dynamicWord and then typed in #contentAreaContainer and give it a red border by adding em around the keyword and style the em trough CSS:
Part of the Code:
<div class="word">
Dynamic word to highlight: <input name="dynamic_word" id="dynamicWord" value="Enter word..">
</div>
<div id="contentAreaContainer" oninput="highlighter()">
<textarea id="contentArea"></textarea>
</div>
function highlighter()
{
var contentAreaContainer = document.getElementById('contentAreaContainer');
var dynamicWord = document.getElementById('dynamicWord').value;
wrapWord(contentAreaContainer, dynamicWord);
};
wrapWord() does:
function wrapWord(el, word)
{
var expr = new RegExp(word, "i");
var nodes = [].slice.call(el.childNodes, 0);
for (var i = 0; i < nodes.length; i++)
{
var node = nodes[i];
if (node.nodeType == 3) // textNode
{
var matches = node.nodeValue.match(expr);
if (matches)
{
var parts = node.nodeValue.split(expr);
for (var n = 0; n < parts.length; n++)
{
if (n)
{
var em = el.insertBefore(document.createElement("em"), node);
em.appendChild(document.createTextNode(matches[n - 1]));
}
if (parts[n])
{
el.insertBefore(document.createTextNode(parts[n]), node);
}
}
el.removeChild(node);
}
}
else
{
wrapWord(node, word);
}
}
}
em{border: 1px solid red;}
The problem:
Now at this moment every time on input in #contentAreaContainer the keyword chosen is highlighted a short period in the #contentAreaContainer, because highlighter() is triggered on input. But it should stay highlighted after finding it instead of only oninput.
I need oninput to search for the #dynamicWord value with wrapWord() while some one is typing;
Any time the #dynamicWord value was found it should permanently get an em
So how can I sort of 'save' the found keywords and permanently give them the element until the dynamic keyword gets edited?
Check the DEMO version
Solved:
Using setTimeout() instead of oninput I managed to make the highlight look constant. The change:
function highlighter()
{
var contentAreaContainer = document.getElementById('contentAreaContainer');
var mainKeyword = document.getElementById('main_keyword').value;
wrapWord(contentAreaContainer, mainKeyword);
repeater = setTimeout(highlighter, 0.1);
}
highlighter();
I removed oninput="highlighter()" from #contentAreaContainer.
You are trying to highlight words in a textarea. As far as I know a textarea does not support html elements inside. If you do it would simply display them as text.
Therefore you need to use an editable div. This is a normal div but if you add the attribute:
contentEditable="true"
the div acts like a textarea with the only difference it now process html elements. I also needed to change the onchange event into the onkeyup event. The editable div does not support onchange events so the highlight would not be triggered. The HTML for this div looks like:
<div contentEditable="true" id="contentArea">Test text with a word in it</div>
Here is the working code in a fiddle: http://jsfiddle.net/Q6bGJ/ When you enter a new character in the textarea your keyword gets highlighted.
However there is still a problem left. You surround the keyword with an em element. This results in surrounding it on every keystroke. Now you end up width many em's around the keyword. How to solve this, I leave up to you as a challenge.

replace specific tag name javascript

I want to know if we can change tag name in a tag rather than its content. i have this content
< wns id="93" onclick="wish(id)">...< /wns>
in wish function i want to change it to
< lmn id="93" onclick="wish(id)">...< /lmn>
i tried this way
document.getElementById("99").innerHTML =document.getElementById("99").replace(/wns/g,"lmn")
but it doesnot work.
plz note that i just want to alter that specific tag with specific id rather than every wns tag..
Thank you.
You can't change the tag name of an existing DOM element; instead, you have to create a replacement and then insert it where the element was.
The basics of this are to move the child nodes into the replacement and similarly to copy the attributes. So for instance:
var wns = document.getElementById("93");
var lmn = document.createElement("lmn");
var index;
// Copy the children
while (wns.firstChild) {
lmn.appendChild(wns.firstChild); // *Moves* the child
}
// Copy the attributes
for (index = wns.attributes.length - 1; index >= 0; --index) {
lmn.attributes.setNamedItem(wns.attributes[index].cloneNode());
}
// Replace it
wns.parentNode.replaceChild(lmn, wns);
Live Example: (I used div and p rather than wns and lmn, and styled them via a stylesheet with borders so you can see the change)
document.getElementById("theSpan").addEventListener("click", function() {
alert("Span clicked");
}, false);
document.getElementById("theButton").addEventListener("click", function() {
var wns = document.getElementById("target");
var lmn = document.createElement("p");
var index;
// Copy the children
while (wns.firstChild) {
lmn.appendChild(wns.firstChild); // *Moves* the child
}
// Copy the attributes
for (index = wns.attributes.length - 1; index >= 0; --index) {
lmn.attributes.setNamedItem(wns.attributes[index].cloneNode());
}
// Insert it
wns.parentNode.replaceChild(lmn, wns);
}, false);
div {
border: 1px solid green;
}
p {
border: 1px solid blue;
}
<div id="target" foo="bar" onclick="alert('hi there')">
Content before
<span id="theSpan">span in the middle</span>
Content after
</div>
<input type="button" id="theButton" value="Click Me">
See this gist for a reusable function.
Side note: I would avoid using id values that are all digits. Although they're valid in HTML (as of HTML5), they're invalid in CSS and thus you can't style those elements, or use libraries like jQuery that use CSS selectors to interact with them.
var element = document.getElementById("93");
element.outerHTML = element.outerHTML.replace(/wns/g,"lmn");
FIDDLE
There are several problems with your code:
HTML element IDs must start with an alphabetic character.
document.getElementById("99").replace(/wns/g,"lmn") is effectively running a replace command on an element. Replace is a string method so this causes an error.
You're trying to assign this result to document.getElementById("99").innerHTML, which is the HTML inside the element (the tags, attributes and all are part of the outerHTML).
You can't change an element's tagname dynamically, since it fundamentally changes it's nature. Imagine changing a textarea to a select… There are so many attributes that are exclusive to one, illegal in the other: the system cannot work!
What you can do though, is create a new element, and give it all the properties of the old element, then replace it:
<wns id="e93" onclick="wish(id)">
...
</wns>
Using the following script:
// Grab the original element
var original = document.getElementById('e93');
// Create a replacement tag of the desired type
var replacement = document.createElement('lmn');
// Grab all of the original's attributes, and pass them to the replacement
for(var i = 0, l = original.attributes.length; i < l; ++i){
var nodeName = original.attributes.item(i).nodeName;
var nodeValue = original.attributes.item(i).nodeValue;
replacement.setAttribute(nodeName, nodeValue);
}
// Persist contents
replacement.innerHTML = original.innerHTML;
// Switch!
original.parentNode.replaceChild(replacement, original);
Demo here: http://jsfiddle.net/barney/kDjuf/
You can replace the whole tag using jQuery
var element = $('#99');
element.replaceWith($(`<lmn id="${element.attr('id')}">${element.html()}</lmn>`));
[...document.querySelectorAll('.example')].forEach(div => {
div.outerHTML =
div.outerHTML
.replace(/<div/g, '<span')
.replace(/<\/div>/g, '</span>')
})
<div class="example">Hello,</div>
<div class="example">world!</div>
You can achieve this by using JavaScript or jQuery.
We can delete the DOM Element(tag in this case) and recreate using .html or .append menthods in jQuery.
$("#div-name").html("<mytag>Content here</mytag>");
OR
$("<mytag>Content here</mytag>").appendTo("#div-name");

How to point to or select an arbitary element with content in CKEditor?

I need to add CSS classes to basically any element that the user selects or points to in CKEditor. For example if there is a table that contains an list, my element structure and elementspath might look like this under the cursor:
body > table > tbody > tr > td > ul > li
Out of those, the user needs to be able to somehow select or point to the table, tr, td, ul or li element. After the user selected / pointed to the element they want to add a class to they would get a dialog to choose a class.
How would I detect which element the user wants to point to?
I'm open to any and all suggestions, even if that means hacking the core. For example I would think that this is doable with elementspath having a context menu where I could click something like "properties" for the element and that would bring up a dialog where the user could choose a class, but I have no idea how or even if it is possible to add a context menu to elementspath!
In Firefox this kind of works out of the box in that when a user clicks an element in elementspath I can GetSelectedElement and it returns what the user has clicked, but it doesn't work in IE/Chrome.
I kind hacked together a horrible but somewhat functional solution. In elementspath plugin.js I added
function onContextMenu(elementIndex, ev) {
editor.focus();
var element = editor._.elementsPath.list[elementIndex];
editor.execCommand('elementspathContextmenuForElement', ev, element);
}
var onContextMenuHanlder = CKEDITOR.tools.addFunction(onContextMenu);
And then where the elementspath item html is generated I added:
oncontextmenu="return CKEDITOR.tools.callFunction(', onContextMenuHanlder, ',', index, ', event );"
And then I made a plugin that creates a html "context menu"
CKEDITOR.plugins.add('elementspathcontextmenu', {
init: function (editor) {
editor.addCommand('elementspathContextmenuForElement', {
exec: function (editor, event, element) {
debug(element);
var tempX = event.pageX + 'px';
var tempY = event.pageY + 'px';
window.newdiv = document.createElement('div');
window.newdiv.setAttribute('id', "tmpContextMenuDiv");
window.newdiv.style.width = 300 + 'px';
window.newdiv.style.height = 300 + 'px';
window.newdiv.style.position = "absolute";
window.newdiv.style.left = tempX;
window.newdiv.style.top = tempY;
window.newdiv.innerHTML = '<p>Do something</p>';
document.body.appendChild(window.newdiv);
},
canUndo: false // No support for undo/redo
});
}
});
I feel a bit dirty hacking the core like that and creating a div element for the context menu in that way but it kind of works for me. This is by no means the final code but it gets the point across.

Categories

Resources