I have a div element with text and, possibly, other children tags inside it (imgs, spans, etc). I need the following - when user clicks somewhere within a div on a text, the child tag has to be inserted exactly in that position inside the text. Absolute positioning is not an option - I need to modify innerHTML of the div.
For instance, if the div is
<div>some text, more text</div>
And user clicks right after "more", my div should be modified as follows
<div>some text, more<span>new tag</span> text</div>
You could wrap each word/character in a span and then append the new tag after that one. LetteringJS (http://letteringjs.com/) can help you with that.
If you'd use inputs, you could use jCaret (http://www.examplet.org/jquery/caret.php) which looks quite fancy, judging from the examples.
As #LePhil suggested, I wrapped each word in a span. In the following example, the text is inserted after the word on which the mouse is clicked:
http://jsfiddle.net/LXZKA/2/
function parseHTML(str) {
var result = '';
function processText(text, i) {
if (text && text !== ' ') {
result += '<span data-begin-index=' + (i - text.length) + ' data-end-index=' + (i - 1) + '>' + text + '</span>';
}
}
function processTag(tag) {
result += tag;
}
var withinTag = false, withinText = false, text = '', tag = '';
for (var i = 0; i < str.length; i++) {
var ch = str.charAt(i);
if (ch === '<') {
withinText = false;
withinTag = true;
processText(text, i);
text = '';
tag = '<';
} else if (ch === '>') {
withinTag = false;
withinText = false;
processTag(tag + '>');
tag = '';
text = '';
} else if (ch === ' ' || ch == '\xA0') {
if (withinTag) {
tag += ch;
} else {
if (!text.replace(/\s+/g,'')) {
text += ch;
} else {
processText(text + ch, i + 1);
text = '';
}
}
} else {
if (withinTag) {
tag += ch;
} else {
text += ch;
}
}
}
processText(text, str.length);
return result;
}
function findNode(node, x, y) {
if (node.attributes['data-begin-index']) {
if (x >= node.offsetLeft && x <= node.offsetLeft + node.offsetWidth &&
y >= node.offsetTop && y <= node.offsetTop + node.offsetHeight)
{
return node;
}
} else {
for (var i = 0; i < node.childNodes.length; i++) {
var result = findNode(node.childNodes[i], x, y);
if (result) {
return result;
}
}
}
}
function clicked(e, node) {
console.log('clicked mouse');
var x = e.x - 100;
var y = e.y - 100;
console.log(x + ', ' + y);
var node = findNode(node, x, y);
if (node) {
var beginIndex = parseInt(node.getAttribute('data-begin-index'));
var endIndex = parseInt(node.getAttribute('data-end-index'));
newHTML = html.substring(0, endIndex + 1) + 'XXXXX ' + html.substring(endIndex + 1);
} else {
newHTML = html + 'XXXXX ';
}
document.getElementById('mydiv').innerHTML = parseHTML(html = newHTML);
}
document.getElementById('mydiv').innerHTML = parseHTML(html);
Related
So I have this code
let data.message = "message https://www.google.com/images/branding/googlelogo/2x/googlelogo_light_color_272x92dp.png"
$('<div/>').text(data.message).html().replace(/((http|https|ftp):\/\/[\w?=&.\/-;#~%-]+(?![\w\s?&.\/;#~%"=-]*>))/g, '<img src="$1">$1</a> ')
Idk why its not working it splits the line on my thing cause its a chat app but it splits the http s:// for some reason heres the code to go to next line:
function getName(name) {
let newName = name;
// offsets i to match the current string
let offset = 0;
for (let i = 1; i < name.length; i += 1) {
if (i % 35 === 0) {
if(newName.charAt(i) === ' ') {
// Your own code with the offset
newName = newName = `${newName.substr(0, i - offset)}\n${newName.substr(i - offset + 1)}`;
} else {
// looking back in the string until there is a space or the string "ends"
while(newName.charAt(i - offset) !== ' ' && offset <= 35) {
offset++;
}
// Only set newName if a space was found in the last 35
if(i - offset > 0) {
newName = `${newName.substr(0, i - offset)}\n${newName.substr(i - offset + 1)}`;
}
}
}
}
return newName;
}
I am moving elements using javascript and I need to create a logic for the combinations happening during the drag/drops
I'm trying to get details from the elements, a CSS like selector could be also good, but dunno if it is possible.. (like copy-selector in chrome dev tools)
document.onmouseup = function(e){
targetDest = e.target;
//console.log('targetDest: ', targetDest);
let
indexA = Array.from(targetCurr.parentNode.children).indexOf(targetCurr),
indexB = Array.from(targetDest.parentNode.children).indexOf(targetDest);
console.log(indexA, indexB);
if(targetDest != targetCurr){
if(targetDest == document.documentElement){
console.log('document');
}
else if(targetDest == undefined){
console.log('undefined');
}
else if(!targetDest){
console.log('!dest');
}
else if(targetDest == null){
console.log('null');
}
else if(targetDest == false){
console.log('false');
}
else{
console.log('else');
//targetCurr.parentNode.insertBefore(targetDest, targetCurr);
//console.log('...');
}
}else{
console.log('itself');
}
}
Keep in mind that this will not necessarily uniquely identify elements. But, you can construct that type of selector by traversing upwards from the node and prepending the element you're at. You could potentially do something like this
var generateQuerySelector = function(el) {
if (el.tagName.toLowerCase() == "html")
return "HTML";
var str = el.tagName;
str += (el.id != "") ? "#" + el.id : "";
if (el.className) {
var classes = el.className.split(/\s/);
for (var i = 0; i < classes.length; i++) {
str += "." + classes[i]
}
}
return generateQuerySelector(el.parentNode) + " > " + str;
}
var qStr = generateQuerySelector(document.querySelector("div.moo"));
alert(qStr);
body
<div class="outer">
div.outer
<div class="inner" id="foo">
div#foo.inner
<div class="moo man">
div.moo.man
</div>
</div>
</div>
I wouldn't suggest using this for much besides presenting the information to a user. Splitting it up and reusing parts are bound to cause problems.
My solution using :nth-child:
function getSelector(elm)
{
if (elm.tagName === "BODY") return "BODY";
const names = [];
while (elm.parentElement && elm.tagName !== "BODY") {
if (elm.id) {
names.unshift("#" + elm.getAttribute("id")); // getAttribute, because `elm.id` could also return a child element with name "id"
break; // Because ID should be unique, no more is needed. Remove the break, if you always want a full path.
} else {
let c = 1, e = elm;
for (; e.previousElementSibling; e = e.previousElementSibling, c++) ;
names.unshift(elm.tagName + ":nth-child(" + c + ")");
}
elm = elm.parentElement;
}
return names.join(">");
}
var qStr = getSelector(document.querySelector("div.moo"));
alert(qStr);
body
<div class="outer">
div.outer
<div class="inner" id="foo">
div#foo.inner
<div class="moo man">
div.moo.man
</div>
</div>
</div>
Please note it won't return the whole path if there's an element with ID in it - every ID should be unique on the page, as valid HTML requires.
I use output of this function in document.querySelector later in the code, because I needed to return focus to the same element after replaceChild of its parent element.
I hope CollinD won't mind I borrowed his markup for the code snippet :-)
I mixed the 2 solutions proposed to have a result readable by humans and which gives the right element if there are several similar siblings:
function elemToSelector(elem) {
const {
tagName,
id,
className,
parentNode
} = elem;
if (tagName === 'HTML') return 'HTML';
let str = tagName;
str += (id !== '') ? `#${id}` : '';
if (className) {
const classes = className.split(/\s/);
for (let i = 0; i < classes.length; i++) {
str += `.${classes[i]}`;
}
}
let childIndex = 1;
for (let e = elem; e.previousElementSibling; e = e.previousElementSibling) {
childIndex += 1;
}
str += `:nth-child(${childIndex})`;
return `${elemToSelector(parentNode)} > ${str}`;
}
Test with:
// Select an element in Elements tab of your navigator Devtools, or replace $0
document.querySelector(elemToSelector($0)) === $0 &&
document.querySelectorAll(elemToSelector($0)).length === 1
Which might give you something like, it's a bit longer but it's readable and it always works:
HTML > BODY:nth-child(2) > DIV.container:nth-child(2) > DIV.row:nth-child(2) > DIV.col-md-4:nth-child(2) > DIV.sidebar:nth-child(1) > DIV.sidebar-wrapper:nth-child(2) > DIV.my-4:nth-child(1) > H4:nth-child(3)
Edit: I just found the package unique-selector
Small improvement of the #CollinD answer :
1/ Return value when the selector is unique
2/ Trim classes value (classes with end blanks make errors)
3/ Split multiple spaces between classes
var getSelector = function(el) {
if (el.tagName.toLowerCase() == "html")
return "html";
var str = el.tagName.toLowerCase();
str += (el.id != "") ? "#" + el.id : "";
if (el.className) {
var classes = el.className.trim().split(/\s+/);
for (var i = 0; i < classes.length; i++) {
str += "." + classes[i]
}
}
if(document.querySelectorAll(str).length==1) return str;
return getSelector(el.parentNode) + " > " + str;
}
Based on previous solutions, I made a typescript solution with a shorter selector and additional checks.
function elemToSelector(elem: HTMLElement): string {
const {
tagName,
id,
className,
parentElement
} = elem;
let str = '';
if (id !== '' && id.match(/^[a-z].*/)) {
str += `#${id}`;
return str;
}
str = tagName;
if (className) {
str += '.' + className.replace(/(^\s)/gm, '').replace(/(\s{2,})/gm, ' ')
.split(/\s/).join('.');
}
const needNthPart = (el: HTMLElement): boolean => {
let sib = el.previousElementSibling;
if (!el.className) {
return true;
}
while (sib) {
if (el.className !== sib.className) {
return false;
}
sib = sib.previousElementSibling;
}
return false;
}
const getNthPart = (el: HTMLElement): string => {
let childIndex = 1;
let sib = el.previousElementSibling;
while (sib) {
childIndex++;
sib = sib.previousElementSibling;
}
return `:nth-child(${childIndex})`;
}
if (needNthPart(elem)) {
str += getNthPart(elem);
}
if (!parentElement) {
return str;
}
return `${elemToSelector(parentElement)} > ${str}`;
}
I have posted a similar question before, but it was too static. Now I want to make changes such that the code is dynamic.
OBJECTIVE
I want words that begin with "t" to be highlighted as a user types. If the word does not begin with "t" then do nothing. Basically, the user would have a normal typing experience but "t" words will be highlighted.
VERSION DETAILS
I have a version that "works" onmousemove, but this is annoying to a
user. I don't want them to have to move a mouse to get text to
highlight.
I have a version that "works" onkeypress, but the issue is that the
cursor always returns to initial position (which causes the text to be entered in reverse) and once the highlighting starts, it does not stop.
VERSION 1: event = onmousemove
//highlight ANY word that starts with t
function highlighter(ev) {
var content = ev.innerHTML;
var tokens = content.split(" ");
for (var i = 0; i < tokens.length; i++) {
if (tokens[i][0] == 't') {
tokens[i] = "<mark style='background-color:red; color:white;'>" + tokens[i] + "</mark>";
}
}
ev.innerHTML = tokens.join(" ");
}
/* NOT REQUIRED AT ALL, JUST TO MAKE INTERACTION MORE PLEASANT */
.container {
outline: none;
border: 3px solid black;
height: 100px;
width: 400px;
}
<div class="container" onmousemove=highlighter(this) contenteditable>
</div>
VERSION 2: event = onkeypress
//highlight ANY word that starts with t
function highlighter(ev) {
var content = ev.innerHTML;
var tokens = content.split(" ");
for (var i = 0; i < tokens.length; i++) {
if (tokens[i][0] == 't') {
tokens[i] = "<mark style='background-color:red; color:white;'>" + tokens[i] + "</mark>";
}
}
ev.innerHTML = tokens.join(" ");
}
/* NOT REQUIRED AT ALL, JUST TO MAKE INTERACTION MORE PLEASANT */
.container {
outline: none;
border: 3px solid black;
height: 100px;
width: 400px;
}
<div class="container" onkeypress=highlighter(this) contenteditable>
</div>
Here are draft example. I've used caret get/set snippet from this gist. Basically idea is simple - get caret position, do modification, set it back. Also swapped your innerHTML method to innerText, because you don't need to parse HTML code in your t-finder logic.
function highlighter(ev) {
// Get current cursor position
const currpos = getSelectionDirection(ev) !== 'forward' ? getSelectionStart(ev) : getSelectionEnd(ev);
// Change innerHTML to innerText, you
// dont need to parse HTML code here
var content = ev.innerText;
var tokens = content.split(" ");
for (var i = 0; i < tokens.length; i++) {
if (tokens[i][0] == 't') {
tokens[i] = "<mark style='background-color:red; color:white;'>" + tokens[i] + "</mark>";
}
}
ev.innerHTML = tokens.join(" ");
// Set cursor on it's proper position
setSelectionRange(ev, currpos, currpos);
}
/* NOT REQUIRED AT ALL, JUST TO MAKE INTERACTION MORE PLEASANT */
.container {
outline: none;
border: 3px solid black;
height: 100px;
width: 400px;
}
<div class="container" onkeypress=highlighter(this) contenteditable>
</div>
<script>
// Usage:
// var x = document.querySelector('[contenteditable]');
// var caretPosition = getSelectionDirection(x) !== 'forward' ? getSelectionStart(x) : getSelectionEnd(x);
// setSelectionRange(x, caretPosition + 1, caretPosition + 1);
// var value = getValue(x);
// it will not work with "<img /><img />" and, perhaps, in many other cases.
function isAfter(container, offset, node) {
var c = node;
while (c.parentNode != container) {
c = c.parentNode;
}
var i = offset;
while (c != null && i > 0) {
c = c.previousSibling;
i -= 1;
}
return i > 0;
}
function compareCaretPositons(node1, offset1, node2, offset2) {
if (node1 === node2) {
return offset1 - offset2;
}
var c = node1.compareDocumentPosition(node2);
if ((c & Node.DOCUMENT_POSITION_CONTAINED_BY) !== 0) {
return isAfter(node1, offset1, node2) ? +1 : -1;
} else if ((c & Node.DOCUMENT_POSITION_CONTAINS) !== 0) {
return isAfter(node2, offset2, node1) ? -1 : +1;
} else if ((c & Node.DOCUMENT_POSITION_FOLLOWING) !== 0) {
return -1;
} else if ((c & Node.DOCUMENT_POSITION_PRECEDING) !== 0) {
return +1;
}
}
function stringifyElementStart(node, isLineStart) {
if (node.tagName.toLowerCase() === 'br') {
if (true) {
return '\n';
}
}
if (node.tagName.toLowerCase() === 'div') { // Is a block-level element?
if (!isLineStart) { //TODO: Is not at start of a line?
return '\n';
}
}
return '';
}
function* positions(node, isLineStart = true) {
console.assert(node.nodeType === Node.ELEMENT_NODE);
var child = node.firstChild;
var offset = 0;
yield {node: node, offset: offset, text: stringifyElementStart(node, isLineStart)};
while (child != null) {
if (child.nodeType === Node.TEXT_NODE) {
yield {node: child, offset: 0/0, text: child.data};
isLineStart = false;
} else {
isLineStart = yield* positions(child, isLineStart);
}
child = child.nextSibling;
offset += 1;
yield {node: node, offset: offset, text: ''};
}
return isLineStart;
}
function getCaretPosition(contenteditable, textPosition) {
var textOffset = 0;
var lastNode = null;
var lastOffset = 0;
for (var p of positions(contenteditable)) {
if (p.text.length > textPosition - textOffset) {
return {node: p.node, offset: p.node.nodeType === Node.TEXT_NODE ? textPosition - textOffset : p.offset};
}
textOffset += p.text.length;
lastNode = p.node;
lastOffset = p.node.nodeType === Node.TEXT_NODE ? p.text.length : p.offset;
}
return {node: lastNode, offset: lastOffset};
}
function getTextOffset(contenteditable, selectionNode, selectionOffset) {
var textOffset = 0;
for (var p of positions(contenteditable)) {
if (selectionNode.nodeType !== Node.TEXT_NODE && selectionNode === p.node && selectionOffset === p.offset) {
return textOffset;
}
if (selectionNode.nodeType === Node.TEXT_NODE && selectionNode === p.node) {
return textOffset + selectionOffset;
}
textOffset += p.text.length;
}
return compareCaretPositons(selectionNode, selectionOffset, contenteditable, 0) < 0 ? 0 : textOffset;
}
function getValue(contenteditable) {
var value = '';
for (var p of positions(contenteditable)) {
value += p.text;
}
return value;
}
function setSelectionRange(contenteditable, start, end) {
var selection = window.getSelection();
var s = getCaretPosition(contenteditable, start);
var e = getCaretPosition(contenteditable, end);
selection.setBaseAndExtent(s.node, s.offset, e.node, e.offset);
}
//TODO: Ctrl+A - rangeCount is 2
function getSelectionDirection(contenteditable) {
var selection = window.getSelection();
var c = compareCaretPositons(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);
return c < 0 ? 'forward' : 'none';
}
function getSelectionStart(contenteditable) {
var selection = window.getSelection();
var c = compareCaretPositons(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);
return c < 0 ? getTextOffset(contenteditable, selection.anchorNode, selection.anchorOffset) : getTextOffset(contenteditable, selection.focusNode, selection.focusOffset);
}
function getSelectionEnd(contenteditable) {
var selection = window.getSelection();
var c = compareCaretPositons(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);
return c < 0 ? getTextOffset(contenteditable, selection.focusNode, selection.focusOffset) : getTextOffset(contenteditable, selection.anchorNode, selection.anchorOffset);
}
</script>
I'm trying to make a palindrome checker that changes the currently compared letters as it recurs.
Essentially, callback will do:
r aceca r
r a cec a r
ra c e c ar
rac e car
My JS Bin shows that the compared letters change green sometimes, but if you run it again, the letters won't change. Why is there a difference in results? It seems to sometimes run in Chrome, more often in FireFox, but it's all intermittent.
Code if needed (also available in JS Bin):
var myInterval = null;
var container = [];
var i, j;
var set = false;
var firstLetter, lastLetter;
$(document).ready(function(){
$("#textBox").focus();
$(document).click(function() {
$("#textBox").focus();
});
});
function pal (input) {
var str = input.replace(/\s/g, '');
var str2 = str.replace(/\W/g, '');
if (checkPal(str2, 0, str2.length-1)) {
$("#textBox").css({"color" : "green"});
$("#response").html(input + " is a palindrome");
$("#palindromeRun").html(input);
$("#palindromeRun").lettering();
if (set === false) {
callback(str2);
set = true;
}
}
else {
$("#textBox").css({"color" : "red"});
$("#response").html(input + " is not a palindrome");
}
if (input.length <= 0) {
$("#response").html("");
$("#textBox").css({"color" : "black"});
}
}
function checkPal (input, i, j) {
if (input.length <= 1) {
return false;
}
if (i === j || ((j-i) == 1 && input.charAt(i) === input.charAt(j))) {
return true;
}
else {
if (input.charAt(i).toLowerCase() === input.charAt(j).toLowerCase()) {
return checkPal(input, ++i, --j);
}
else {
return false;
}
}
}
function callback(input) {
$("#palindromeRun span").each(function (i, v) {
container.push(v);
});
i = 0;
j = container.length - 1;
myInterval = setInterval(function () {
if (i === j || ((j-i) === 1 && input.charAt(i) === input.charAt(j))) {
set = false;
window.clearInterval(myInterval);
container = [];
}
console.log(i + ' : ' + j);
$(container[i]).css({"color": "green"});
$(container[j]).css({"color": "green"});
i++;
j--;
}, 1000);
}
HTML:
<input type="text" id="textBox" onkeyup="pal(this.value);" value="" />
<div id="response"></div>
<hr>
<div id="palindromeRun"></div>
I directly pasted the jsLettering code in the JSBin, but here is the CDN if needed:
<script src="http://letteringjs.com/js/jquery.lettering-0.6.1.min.js"></script>
Change:
myInterval = setInterval(function () {
if (i === j) {
set = false;
window.clearInterval(myInterval);
container = [];
}
console.log(i + ' : ' + j);
to:
myInterval = setInterval(function () {
if (i >= j) {//Changed to prevent infinite minus
set = false;
window.clearInterval(myInterval);
container = [];
}
console.log(i + ' : ' + j);
demo
I'm trying to implement snippets into an html textarea. You write a certain word and it will look through key value object and expand the text if it exists. Here is what I have done:
var textarea = document.getElementById("whatever");
var snippets = {
'hello': 'Hello and welcome to my great site'
}
var prepend = "";
var checkCaps = function(e){
if (e.keyCode != 9) return;
e.preventDefault();
var string = "";
var pos = textarea.selectionStart;
var text = textarea.value.split("");
while (pos) {
char = text.pop(pos);
prepend = (char == " ") ? " ": "";
if (char == " ") break;
string += char
pos -= 1;
}
if (snippets[string.reverse()]) {
textarea.value = text.join("")
textarea.value += prepend + snippets[string.reverse()]
}
}
textarea.addEventListener("keydown", checkCaps, false);
String.prototype.reverse=function(){return this.split("").reverse().join("");}
http://jsfiddle.net/JjTmd/
The problem is that the snippet only works in the last word of the textarea's value, and I can't seem to pinpoint where the problem is.
Array.pop doesn't accept a parameter. It removes and returns the last item from the array. Use splice to remove an item at a particular index.
I modified your function as follows and it seems to have the desired behavior:
var checkCaps = function(e){
if (e.keyCode != 9) return;
e.preventDefault();
var string = "";
var pos = textarea.selectionStart;
var text = textarea.value.split("");
while (pos) {
char = text.splice(pos-1,1);
prepend = (char == " ") ? " ": "";
if (char == " ") break;
string += char
pos -= 1;
}
if (snippets[string.reverse()]) {
var start = text.splice(0, pos);
var end = text.splice(pos + string.length);
textarea.value = start.join("") + snippets[string.reverse()] + prepend + text.join("") + end.join("");
}
}
http://jsfiddle.net/JjTmd/1/
Also, you'll probably want to check for new line characters as well:
while (pos) {
char = text.splice(pos-1,1);
if (char == " " || char == "\n") {
prepend = char;
break;
}
string += char
pos -= 1;
}
http://jsfiddle.net/JjTmd/2/