Forum Moderators: open

Message Too Old, No Replies

All things contenteditable

Losing caret focus, insertHTML adding unexpected break, etc

         

csdude55

11:56 pm on Jun 27, 2018 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Instead of creating a bunch of threads on the same thing, I figured it would be easier to just create a running thread on contenteditable so maybe I can get this all straight :-)

I've had a contenteditable element since 2013, but I'm trying to improve it a little. I'm having 2 specific issues:

1. In some instances this one function makes me lose the focus and selected text, but in other instances it doesn't. I'm kinda confused here, because the function is basically the same.

I use this to change the font-family:

$.fn.toggleClick = function(e) {
if (this.attr('id')) toggle.el = '#' + this.attr('id');
else toggle.el = this;

this.toggle();

if (e.stopPropagation) e.stopPropagation();
e.cancelBubble = true;

return this;
}

function changeSpan(command, val) {
// there's a bit here that I'm removing that only applies to inserting an image,
// so it's irrelevant for this discussion

document.execCommand(command, false, val);
return;
}

$('[data-fontname]')
.on('mousedown', function() {
changeSpan('FontName', $(this).data('fontname'));
$('#font_fam').toggleClick(event);
});

// HTML
<button id="fontfamily"
onClick="$('#font_fam').toggleClick(event)">
Select Font
</button>

<div id="font_fam" style="position: absolute; display: none">
<div data-fontname="Arial" style="font-family: Arial">
Arial
</div>

<div data-fontname="Georgia" style="font-family: Georgia">
Georgia
</div>
</div>


So the user clicks "Select Font", and toggleClick() uses .show() to unhide the menu with all of the fonts available shown in children DIV elements. They select the font they want, and .on('mousedown'...) uses execCommand() to change the font family.

And this one works perfectly. After they click the font, it changes the font-family, keeps the text highlighted, and in focus.

But when I use this:

$('[data-richbutton=Bold]')
.on('mousedown', function() {
changeSpan($(this).data('richbutton'), false)
});

// HTML
<div data-richbutton="Bold">
<b>B</b>
</div>


it bolds the text correctly, but the text is no longer selected and I lose focus. Same if I use Italic or Underline.

So if the user wants to change the font, bold it, italicize it, and underline it then they can highlight the text and change the font, then they can click Bold because the text is still selected after changing the font. But then the selection is lost, so they have to highlight it again to italicize. Then it's lost again, so they have to highlight it again to underline.

It makes absolutely no sense to me! As far as I can tell, this:

<div data-fontname="Arial" style="font-family: Arial">
Arial
</div>


is virtually identical to this:

<div data-richbutton="Bold">
<b>B</b>
</div>


The only difference is in the .on('mousedown') function, the first one hides the #font-fam container. So why does the first one leave the text selected, while the second doesn't?

csdude55

1:15 am on Jun 28, 2018 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



In retrospect, the second issue is almost the same problem as I'm having with the Bold, Italics, and Underline.

I used to insert an IMG tag for emojis, but since I already have them set up as a sprite in the CSS I figured it would be better to just insert a DIV tag with the appropriate classes instead of setting up pages with thousands of HTTP connections. Like so:

// CSS
.sprite_em, .emoji {
background:url('https://images.example.com/emoticon.png');
width: 19px;
height: 19px
}

.AddEmoticons08011 {
background-position: 0 0
}

.emoji {
display: inline-block;
margin: 0 3px;
vertical-align: middle
}

// Javascript
function changeSpan(command, val) {
if (command == 'InsertImage') {
val = '<div class="emoji ' + val + '" ' +
"contenteditable='false' " +
"unselectable='on' " +
'></div>';

$('#comment').focus();
command = 'insertHTML';
}

document.execCommand(command, false, val);
return;
}

$('[data-emoji]')
.on('mousedown', function() {
changeSpan('InsertImage', $(this).data('emoji'));
});

// HTML
<div class="sprite_em AddEmoticons08011" data-emoji="AddEmoticons08011"></div>


But let's say I've typed in some text, then click an emoji. It inserts where I left off, which is good, but then I lose focus. I plugged in $('#comment').focus() (where #comment is the ID of the contenteditable element), but that puts the cursor back at the very beginning of the input. So if the user clicks the same emoji 3 or 4 times it ends up with 1 emoji where they want it, but then the next 2 or 3 at the beginning of the input.

How do I make it go back to where the user left off?

csdude55

12:35 am on Jun 29, 2018 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Just an update... this:

$('[data-richbutton=Bold]')
.on('mousedown', function() {
changeSpan($(this).data('richbutton'), false)
});

<div data-richbutton="Bold">
<b>B</b>
</div>


doesn't work on my Samsung S7 at all. I can highlight text and change the font family, size, or color using the same command, and I can use insertHTML to insert the emoji. But when I click Bold, Underline, or Italics the container #comment immediately loses focus so it doesn't work.

csdude55

7:22 am on Jul 3, 2018 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



For the sake of posterity, the solution was glaringly obvious... once I figured it out!

event.preventDefault();
$('#comment').focus();
document.execCommand(command, false, val);


event.preventDefault was all that I needed all along :-)

csdude55

7:51 am on Jul 5, 2018 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



I don't think anyone else is keeping up with this thread, but just in case...

Today, I discovered that what was working beautifully on Chrome for Desktop and my Samsung S7 has major problems on the iPhone :'-( And as of right now, I don't have a clue how to fix them.

The same problems are on Chrome or Safari on the iPhone with iOS 11.2.6.

1. when you click inside of the contenteditable, the iPhone zooms in for what appears to be no reason. The viewport is set to:

<meta name="viewport" content="width=device-width, initial-scale=1.0">


2. I have the emojis set as a sprite, so when someone clicks to insert an emoji it really inserts a DIV element with a background image. This works, but afterward I'm unable to move the cursor to the right of the emoji, or delete it in any way. Clicking after the emoji doesn't change the cursor position; it's as if the DIV is invisible to the browser. Which means that I can't select it to delete it, or backspace over it.

I tried changing it to the old format of using an actual IMG element instead of a DIV, but that wouldn't insert at all. It worked on my Samsung, but not on the iPhone.

// the image element
<img src='https://example.com/emoticons/AddEmoticons0801.gif' align='middle' border='0' unselectable='on' selectable='false' style='-moz-user-select: none' onSelectStart='return false' onDrag='return false' onDragStart='return false' onDragEnd='return false' onDblClick='return false' onFocus='blur()'>

// the DIV element
.emoji {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
display: inline-block;
margin: 0 3px;
vertical-align: middle
}

<div class="emoji" data-em="1" contenteditable="false" unselectable="on"></div>


It's possible that making it selectable will let me move the cursor, but I needed it to be unselectable... something to play with when I have an iPhone handy, I guess.

3. I can type in text just fine, but if I delete that text so that there's nothing there then the contenteditable loses focus, and it won't go back! For whatever reason, adding text and then deleting it blurs the element, and clicking back inside of it doesn't re-focus.

I also can't click on the emoji to force the focus back; clicking on it doesn't seem to do anything at this point.


There are a few other problems, but I think I might be able to fix those. These 3 have me totally stumped, though. So if you guys and gals have ANY idea where to even start to figure these out, I'm all ears! Well, I guess I'm all eyes... whatever, you know what I mean :-)

csdude55

2:12 am on Jul 6, 2018 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



1. when you click inside of the contenteditable, the iPhone zooms in for what appears to be no reason.

Well, I have an answer to this one.

iPhone likes to make a minimum text size for input elements of 16px, so if your input is less than that then it zooms in. Which is really funny, considering the font size when you text is less than 16px, but for some reason they think a website needs to be larger.

The original solution was to change the META tag to:

<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">


Which, honestly, wasn't a very good solution; I don't want to prevent the user from stretching it if they want, I just don't want it to zoom in automatically.

But that problem solved itself, because apparently iOS 10 stopped supporting the user-scalable tag, anyway.

So the only real solution is to change the font size of input elements to a minimum of 16px.

This really throws off my game because I DID have it set for the user to increase the font size on their own. But that's iPhone for you, I guess.

Now on to 2 and 3...

not2easy

5:38 am on Jul 6, 2018 (gmt 0)

WebmasterWorld Administrator 10+ Year Member Top Contributors Of The Month



Sorry I'm no help with javascript stuff, but I have been dropping in to watch as you've kept at it and resolved all these little issues.

I use the phone browser so seldom, I did not know that user scalability had been taken away. Now I'll need to go and check a few possible problems..

csdude55

7:06 am on Jul 6, 2018 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



I'm lucky that my girlfriend has an iPhone or I wouldn't have known, either! Usually I figure that Google is the most strict, so if I build for Google then it will be fine everywhere... nope :'-(

I'm probably going to focus on problems # 2 and 3 this weekend, so I'll post back with any updates. Glad someone is reading and enjoying my struggles! LOL

tangor

5:42 am on Jul 7, 2018 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



I, too, am following. Watching from the outside in that I have never placed that many restrictions (or OPTIONS) on a user. Different strokes and all that happy stuff. :) KISS for me...

csdude55

1:18 am on Jul 9, 2018 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Well, 2 steps forward and 1 step back...

Issues # 2 and 3 are fixed! I had to remove the whole user-select CSS from .emoji:

// Removed
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);


Since it's a background image, though, it doesn't look like the user can select it, anyway, so I guess that wasn't really necessary to begin with :-)

But now I've discovered yet another problem with iPhone :'-(

The iPhone has the option to copy and paste an image. I haven't messed with it too much, but the first thing my girlfriend did was try to paste a Bitmoji. It looks like it works in the contenteditable, but on the backend it really posts:

<img src="webkit-fake-url://35e268c3-4829-466a-b12a-ef216d6ca763/imagepng">


I've been researching, but I can't find where anyone has found a solution for this other than to just strip it from the text altogether, and maybe give the user an alert letting them know that it doesn't work:

// JavaScript
function editPaste() {
var a = $('#comment').html();

if (a.search(/<img src=("|')webkit-fake-url:[^>]+\1>/i)) {
alert("Sorry, you can't paste an image. Please use the Upload Image option instead");
a = a.replace(/<img src=("|')webkit-fake-url:[^>]+\1>/gi, '');

$('#comment').html(a);
}
}

// HTML
<div id="comment" contentEditable="true" onPaste="editPaste()"></div>


If you guys have any idea of how to convert that webkit-fake-url address in to something real, I'd love to know about it! I mean, it's obviously translated by the browser in some way or another, I just don't know how to decipher it.

not2easy

5:34 am on Jul 9, 2018 (gmt 0)

WebmasterWorld Administrator 10+ Year Member Top Contributors Of The Month



Many iPhone users have set up cloud storage for backups, sync and sharing - is that possibly what the URL refers to? I have no idea what the cloud URLs might look like, just a thought.

JAB Creations

2:13 am on Jul 17, 2018 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



Hi csdude55,

So more or less everything you're trying to do I've done and I've got some tips for you and code if you know how to look at profile links.

First, never ever use frameworks or libraries. There will be a new version and it will break everything. Write pure JavaScript only, it will dramatically reduce bandwidth and not only will you only have to deal with browser bugs though you'll force yourself to understand why you're writing code the way you are.

Secondly you absolutely shouldn't use images when you can use Unicode! Look at the Unicode page in my site's documentation. You may want to note that pretty much every browser doesn't know how to use the Variation Selector-15 character though never versions of operating systems are improving how that page renders.

As far as moving the caret around it was a very tricky feat to achieve and I've never ever seen any one post working code of it. Go to any page with the Rich Editor on my site (contact, blog comments, etc) and reload the page watching the Network Requests in the developer tools to find the file editor_rich.js and then in the file look at function editor_caret_move(n,p).

You may notice, if you choose to look at my code, that Gecko browsers (e.g. Waterfox, Firefox) has some serious content editable bugs and that I'm not even using the old API which is a total and complete mess. The code for this file itself needs a heavy audit though most everything works for most of my clients.

I can't help with anything iPhone related. I would recommend trying to either setup a VM for OS X and resizing the window down to very small resolutions (iPhones are actually terrible for hardware specs) and testing that way or buy a used Mac Mini. You could buy a used iPhone too, they can be very affordable though I don't know how up to date one needs to effectively test websites in general.

Lastly, this website is a great source for everything Unicode:
[unicode-table.com...]

Hope this helps, PM me if you have questions.

John

csdude55

3:59 am on Aug 2, 2018 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Further update, this:

event.preventDefault();


has been changed to:

event.preventDefault ? event.preventDefault() : event.returnValue = false;


IE9 had problems with it, so I had to modify it a little.

csdude55

4:22 pm on Aug 2, 2018 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



One other update...

Some of you know that I've been working with the fontSize command with execCommand(). This command only sends an integer 1-7, but since I let my users change the default font size of the site, I really wanted the font size to be a percentage.

As of today, I've spent a month on coding and it's still not right, so I've abandoned all of the coding (which I'll post below, in case anyone is interested).

Then, laying in bed last night, I realized a much, much simpler solution, and I'm kicking myself for not having thought of it before! I guess I had tunnel vision and couldn't see past what I was doing to think of a different solution.

Instead of trying to change the code on the page from <font size="3"> to <span style="font-size: 120%">, the solution is so simple...

/* CSS */
font[size='1'] { font-size: 80% }
font[size='2'] { font-size: 100% }
font[size='3'] { font-size: 120% }
...


I don't know if this will work for all browsers, but it's close enough for me! And now I can just use the same document.execCommand as everything else:

# note that I had to send event; IE9 threw an error if I didn't
function changeSpan(event, command, val, selector) {
var ele = $('#' + selector);

// keeps the cursor where it belongs
event.preventDefault ? event.preventDefault() : event.returnValue = false;
ele.focus();

document.execCommand(command, false, val);

return;
}


Before that, I was reading the selection, surrounding the contents with a custom HTML element with a marker and matching end tag (eg, <big-0></big-0>, <big-1></big-1>, and so on), applying a class to that marker (eg, rel-small, rel-medium, and so on), loading the entire text into a variable, using regex to delete the parent version of <big-x> if there's a child <big-x> present, then resetting the modified text.

This worked great in Chrome, Android, and iPhone, but not IE9 or Firefox. For IE9 and FF it would work the first time, but not if you change the font size a second time. IE9 was giving me an error (Range Exception: BAD_BOUNDARYPOINTS_ERR (1)); FF didn't give an error, it just didn't work.

So here's that code as of last night, which has since been abandoned; this was just above the event.preventDefault line:

var range, sel;

if (document.selection && document.selection.createRange)
sel = document.selection.createRange();

else if (window.getSelection)
sel = window.getSelection();

if (command == 'fontsize') {
if (window.getSelection && sel.rangeCount && sel.getRangeAt) {
range = sel.getRangeAt(0);

// not sure if this helped, I was trying to fix the IE9 error
sel.removeAllRanges();
sel.addRange(range);
}

else range = sel;

// generate unique marker, based on current time
var marker = (new Date).getTime();

// surround selected with <big>
var font = document.createElement('big-' + marker)
font.className = 'rel-' + val;
range.surroundContents(font);

var text = ele.html();

// remove parent <big-x> tags
var font_match = /(<(big-[0-9]+) [^>]*>)([\s\S]*)(<(big-[0-9]+) [^>]*>)([\s\S]*)(<\/\5>)([\s\S]*)(<\/\2>)/gim;

if (font_match.test(text))
text = text.replace(font_match, '$3$4$6$7$8');

// remove empty tags
var empty_font_match = /<(div|span|big-[0-9]+|font|b|i|u)(?: [^>])*>(<br>|\s)*<\/\1>/gi;
while (empty_font_match.test(text))
text = text.replace(empty_font_match, '$2');

ele.html(text);
}

csdude55

4:54 am on Aug 3, 2018 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



So, new issue. This is just a problem with FF.

I'm inserting emojis using a sprite, so it goes in like this:

<div data-em="1"></div>


When the user inserts emojis normally (using document.execCommand()), it works fine. But if they blur the contenteditable field, then click back in to it and insert another emoji, the caret goes inside of the DIV and the new emoji gets nested within the previous emoji. Like:

<div data-em="1"><div data-em="1"></div></div>


This causes the emojis to overlap instead of being inline. And if they try to type some text, it goes on top of the emoji because it's still nested within the DIV tags.

I tried setting contenteditable="false" on the DIV, but that just made things worse; it still gets nested, but then they can't get out of it unless they use the mouse to click on some previous text. Worse, they can't delete any of the emojis at all (backspace nor delete key).

I've also tried unselectable="on", selectable="false", and in the CSS I've tried:

-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);


but none of those seemed to have any impact at all.

Any suggestions on how I can prevent FF from putting the caret inside of the element?

My only thought is to run a script after each emoji insertion that reads the entire text, uses a regex to fix the nested emojis, then set it back in place. But I spent a dang month on that with fontsize, just to end up scrapping it! LOL So I'm not looking forward to doing that again!

csdude55

8:08 pm on Aug 13, 2018 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



For the problem with the nested DIV statements, the only solution I could come up with was to use a void element instead of a DIV... no ending tag means it can't put the caret inside of it, right? I ended up going back to using an IMG tag for the sprite... there's another thread on that if you want to check it out, in the CSS forum... subject is "Proper element for a sprite".

I've discovered another interesting little bug/glitch/feature with the contenteditable, though. This relates to the "display" property.

The default display is "block". But an issue with that is that every time you hit enter, it tries to wrap everything in DIV tags. Meaning, a simple <br> becomes <div><br></div>. Or worse, if I type "Test" and then enter a line break after the "e", I get:

<div>Te<br></div><div>st</div>

Then I tried inserting an emoji after the "e" and then hit enter, and I ended up with:

Te<img src="img.gif"><div>st</div>

So there's no apparent consistency to it; sometimes the opening text is wrapped in DIV, sometimes it's not; sometimes there's a <br>, and sometimes it just wraps the next text in DIV.

I've tried removing the <div></div> in post, but that gets very complicated (especially when there's a nested DIV that I want to keep), so I just end up with DIV-heavy text.

If I change the display of the contenteditable to "inline" or "inline-block" then it ALMOST solves the problem, but adds in a new one... now, if I type "Test" and want to use "insertHTML" function between the "e" and "s" (eg, inserting an emoji) then on Chrome and Android it plugs in a <br> after what I inserted.

So when using document.execCommand('insertHTML', false, '<b>foo</b>'); with the caret after the "e", "Test" becomes:

Te<b>foo</b><br>st

That's a lot easier to fix in post, but it still looks weird to the person typing it in.

It still looks like using "inline-block" would be the best solution, but only if I can figure out a way to not place a <br> after the insertHTML. If I can't resolve that then I'll be stuck using the DIV-heavy "block".

csdude55

4:48 am on Aug 15, 2018 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Overly-complicated solution to the above... instead of using document.execCommand('insertHTML', ...), I'm using:

// works for most browsers
if (window.getSelection)
insertHTML('b', 'foo');

// worst case scenario, just live with the trailing <br>
else
document.execCommand('insertHTML', false, '<b>foo</b>');

function insertHTML(node, val) {
var sel = window.getSelection().rangeCount;
var range = sel.getRangeAt(0);
var newVal;

range.collapse(true);

newVal = document.createElement(node);
newVal.textContent = val;

range.insertNode(newVal);

range.setStartAfter(newVal);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}

csdude55

1:44 am on Aug 20, 2018 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Any thoughts on an alternative to document.execCommand() that doesn't automatically "correct" the HTML that's pasted?

Here's where I am... when someone pastes something in, I wrap it in <blockquote data-quote>PASTE</blockquote>. I already have a function written so that when the user clicks or keys inside of the contenteditable then it will know what node their in, so I can easily find out when they've clicked within this blockquote.

What I want to do is, if they're within the blockquote and try to enter anything, then it will automatically break out of the blockquote... ipso facto, they can't pretend that the pasted data said something it didn't originally say.

I started with this:

var block = false;
$('#comment').on('keydown mouseup', function(e) {
// if they're within blockquote, block = true;
// else block = false;

if (block) {
e.preventDefault ? e.preventDefault() : e.returnValue = false;

var endBlock = '</blockquote><br><br><br><blockquote data-quote>';

// I'm getting here and endBlock is right...
// console.log('block3 - ' + endBlock);

// ...but here it's plugging in <br><br><br><blockquote data-quote=""><br></blockquote>
if (document.selection)
document.selection.createRange().pasteHTML(endBlock);

else
document.execCommand('insertHTML', false, endBlock);
}
}
});



The ideal solution would be something like I did in the previous post to this thread, using insertNode... but that auto-corrects the HTML, too.

My next idea is to use document.execCommand() to insert something that would be highly unlikely to be used, like <q data-mydomain="example"></q>, then load the whole thing to a variable, use regex to change <q data-mydomain="example"></q> to </blockquote><br><br><br><blockquote data-quote>, and then set it back... that would probably work, but of course I'd lose the caret location.

Any other ideas before I go that last route?