Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

contenteditable single-line input

For an application we're developing at the company where I work, we need an input that supports inserting emoticons inside our JS-based web app. We're currently using an input with the emoticon shortcodes (ie ':-)') and would like to switch to inserting actual, graphical images.

Our original plan was to use a contenteditable <div>. We're using listeners for the paste event as well as the different key/mouse interactions to ensure no unwanted markup enters the contenteditable (we strip text out of its container tags and leave only image tags that we inserted ourselves).

However, the problem right now is that the div resizes if you put in enough content (ie its height increases). We don't want this to happen, nor is it acceptable for the text to just be hidden (ie plain overflow: hidden). So:

Is there a way to make the contenteditable div behave like a single-line input?

I'd like it best if there is a relatively simple attribute/css property that I've missed that will do what I want, but if necessary CSS+JS suggestions will also be appreciated.

like image 220
Gijs Avatar asked Jul 26 '11 14:07

Gijs


People also ask

What does Contenteditable attribute do?

The contenteditable global attribute is an enumerated attribute indicating if the element should be editable by the user. If so, the browser modifies its widget to allow editing.

How do you use Contenteditable in HTML?

To edit the content in HTML, we will use contenteditable attribute. The contenteditable is used to specify whether the element's content is editable by the user or not. This attribute has two values. true: If the value of the contenteditable attribute is set to true then the element is editable.

How do I add Contenteditable?

You can add the contenteditable="true" HTML attribute to the element (a <div> for example) that you want to be editable.

How do you use Contenteditable in react?

contentEditable prop in ReactWhen you add contentEditable=true to a component you see a warning from React: Warning: A component is `contentEditable` and contains `children` managed by React. It is now your responsibility to guarantee that none of those nodes are unexpectedly modified or duplicated.


2 Answers

[contenteditable="true"].single-line {      white-space: nowrap;      width:200px;      overflow: hidden;  }   [contenteditable="true"].single-line br {      display:none;    }  [contenteditable="true"].single-line * {      display:inline;      white-space:nowrap;  }
<div contenteditable="true" class="single-line">      This should work.  </div>​
like image 93
Alessio Avatar answered Sep 21 '22 02:09

Alessio


Other answers are wrong and contain few mistakes (on 2019-05-07). Other solutions suggest to use "white-space: nowrap" (prevents carrying to another line) + "overflow: hidden" (prevents long text going beyond the field) + hiding <br> and other.

First mistake in that solutions is "overflow: hidden" also prevents scrolling the text. User will not be able to scroll the text by:

  • Pressing mouse middle button
  • Selecting the text and moving mouse pointer to the left or right
  • Using horizontal mouse scroll (when user have such a thing)

The only way he can scroll is using keyboard arrows.

You can solve this problem by using "overflow: hidden" and "overflow: auto" (or "scroll") at the same time. You should create parent div with "overflow: hidden" to hide content user should not see. This element must have input borders and other design. And you should create child div with "overflow-x: auto" and "contenteditable" attribute. This element will have scrollbar so user can scroll it without any limitations and he will not see this scrollbar because of hiding overflow in parent element.

Example of solution:

document.querySelectorAll('.CETextInput').forEach(el => {  	//Focusing on child element after clicking parent. We need it because parent element has bigger width than child.  	el.parentNode.addEventListener('mousedown', function(e) {  		if (e.target === this) {  			setTimeout(() => this.children[0].focus(), 0);  		}  	});  	  	//Prevent Enter. See purpose in "Step 2" in answer.  	el.parentNode.addEventListener('keydown', function(e) {  		if (e.keyCode === 13)  			e.preventDefault();  	});  });
.CETextInputBorder { /*This element is needed to prevent cursor: text on border*/  	display: inline-block;  	border: 1px solid #aaa;  }    .CETextInputCont {  	overflow: hidden;  	cursor: text; /*You must set it because parent elements is bigger then child contenteditable element. Also you must add javascript to focus child element on click parent*/  	  	/*Style:*/  	width: 10em;  	height: 1em;  	line-height: 1em;  	padding: 5px;  	font-size: 20px;  	font-family: sans-serif;  }    .CETextInput {  	white-space: pre; /*"pre" is like "nowrap" but displays all spaces correctly (with "nowrap" last space is not displayed in Firefox, tested on Firefox 66, 2019-05-15)*/  	overflow-x: auto;  	min-height: 100%; /*to prevent zero-height with no text*/  	  	/*We will duplicate vertical padding to let user click contenteditable element on top and bottom. We would do same thing for horizontal padding but it is not working properly (in all browsers when scroll is in middle position and in Firefox when scroll is at the end). You can also replace vertical padding with just bigger line height.*/  	padding: 5px 0;  	margin-top: -5px;  	  	outline: none; /*Prevent border on focus in some browsers*/  }
<div class="CETextInputBorder">  	<div class="CETextInputCont">  		<div class="CETextInput" contenteditable></div>  	</div>  </div>


Step 2: Solving problem with <br> and other:

Also there is a problem that user or extensions can paste

  • <br> (can be pasted by user)
  • <img> (may have big size) (can be pasted by user)
  • elements with another "white-space" value
  • <div> and other elements that carry text to another line
  • elements with unsuitable "display" value

But advise to hide all <br> is wrong too. That is because Mozilla Firefox adds <br> element to empty field (I guess it may be workaround of bug with text cursor disappearing after deleting last character; checked in Firefox 66 released on 2019-03-19). If you hide this element then when user moves focus to field caret will be set in this hidden <br> element and text cursor will be hidden too (always).

You can fix this if you will be <br> when you know field is empty. You need some javascript here (you cannot use :empty selector because field contains <br> elements and not empty). Example of solution:

document.querySelectorAll('.CETextInput').forEach(el => {  	//OLD CODE:  	  	//Focusing on child element after clicking parent. We need it because parent element has bigger width than child.  	el.parentNode.addEventListener('mousedown', function(e) {  		if (e.target === this) {   	 	 	setTimeout(() => this.children[0].focus(), 0);   	 	}  	});  	  	//Prevent Enter to prevent blur on Enter  	el.parentNode.addEventListener('keydown', function(e) {  		if (e.keyCode === 13)  			e.preventDefault();  	});  	  	//NEW CODE:  	  	//Update "empty" class on all "CETextInput" elements:  	updateEmpty.call(el); //init  	el.addEventListener('input', updateEmpty);    	function updateEmpty(e) {  		const s = this.innerText.replace(/[\r\n]+/g, ''); //You must use this replace, see explanation below in "Step 3"  		this.classList.toggle('empty', !s);  	}  });
/*OLD CODE:*/    .CETextInputBorder { /*This element is needed to prevent cursor: text on border*/  	display: inline-block;  	border: 1px solid #aaa;  }    .CETextInputCont {  	overflow: hidden;  	cursor: text; /*You must set it because parent elements is bigger then child contenteditable element. Also you must add javascript to focus child element on click parent*/  	  	/*Style:*/  	width: 10em;  	height: 1em;  	line-height: 1em;  	padding: 5px;  	font-size: 20px;  	font-family: sans-serif;  }    .CETextInput {  	white-space: pre; /*"pre" is like "nowrap" but displays all spaces correctly (with "nowrap" last space is not displayed in Firefox, tested on Firefox 66, 2019-05-15)*/  	overflow-x: auto;  	min-height: 100%; /*to prevent zero-height with no text*/  	  	/*We will duplicate vertical padding to let user click contenteditable element on top and bottom. We would do same thing for horizontal padding but it is not working properly (in all browsers when scroll is in middle position and in Firefox when scroll is at the end). You can also replace vertical padding with just bigger line height.*/  	padding: 5px 0;  	margin-top: -5px;  	  	outline: none; /*Prevent border on focus in some browsers*/  }    /*NEW CODE:*/    .CETextInput:not(.empty) br,  .CETextInput img { /*We hide <img> here. If you need images do not hide them but set maximum height. User can paste image by pressing Ctrl+V or Ctrl+Insert.*/  	display: none;  }    .CETextInput * {  	display: inline;  	white-space: pre;  }
<!--OLD CODE:-->    <div class="CETextInputBorder">  	<div class="CETextInputCont">  		<div class="CETextInput" contenteditable></div>  	</div>  </div>


Step 3: Solving problem with getting value:

We hided <br> elements so "innerText" value will not contain them. But:

  1. When "empty" class is set result may contain <br> elements.
  2. Your other styles or extensions may override "display: none" by "!important" mark or by rule with higher priority.

So when you get value you should make replace to avoid accidental getting line breaks:

s = s.replace(/[\r\n]+/g, ''); 


Do not use javascript for hiding <br>

Also you could solve the problem with <br> by removing them by javascript but this is very bad solution because after every removing user cannot use "undo" action anymore for canceling changes was made before removing.

Also you could use document.execCommand('delete') to delete <br> but it is hard to implement + user can undo your deletion and restore <br> elements.


Adding placeholder

It was not asked in question but I guess many people using single-line contenteditable elements will need it. Here is example how to make placeholder using css and "empty" class we talked above:

//OLD CODE:    document.querySelectorAll('.CETextInput').forEach(el => {  	//Focusing on child element after clicking parent. We need it because parent element has bigger width than child.  	el.parentNode.addEventListener('mousedown', function(e) {  		if (e.target === this) {   	 	 	setTimeout(() => this.children[0].focus(), 0);   	 	}  	});  	  	//Prevent Enter to prevent blur on Enter  	el.parentNode.addEventListener('keydown', function(e) {  		if (e.keyCode === 13)  			e.preventDefault();  	});  	  	//Update "empty" class on all "CETextInput" elements:  	updateEmpty.call(el); //init  	el.addEventListener('input', updateEmpty);    	function updateEmpty(e) {  		const s = this.innerText.replace(/[\r\n]+/g, ''); //You must use this replace, see explanation below in "Step 3"  		this.classList.toggle('empty', !s);  		  		//NEW CODE:  		  		//Make element always have <br>. See description in html. I guess it is not needed because only Firefox has bug with bad cursor position but Firefox always adds this element by itself except on init. But on init we are adding it by ourselves (see html).  		if (!s && !Array.prototype.filter.call(this.children, el => el.nodeName === 'BR').length)  			this.appendChild(document.createElement('br'));  	}  });
/*OLD CODE:*/    .CETextInputBorder { /*This element is needed to prevent cursor: text on border*/  	display: inline-block;  	border: 1px solid #aaa;  }    .CETextInputCont {  	overflow: hidden;  	cursor: text; /*You must set it because parent elements is bigger then child contenteditable element. Also you must add javascript to focus child element on click parent*/  	  	/*Style:*/  	width: 10em;  	height: 1em;  	line-height: 1em;  	padding: 5px;  	font-size: 20px;  	font-family: sans-serif;  }    .CETextInput {  	white-space: pre; /*"pre" is like "nowrap" but displays all spaces correctly (with "nowrap" last space is not displayed in Firefox, tested on Firefox 66, 2019-05-15)*/  	overflow-x: auto;  	min-height: 100%; /*to prevent zero-height with no text*/  	  	/*We will duplicate vertical padding to let user click contenteditable element on top and bottom. We would do same thing for horizontal padding but it is not working properly (in all browsers when scroll is in middle position and in Firefox when scroll is at the end). You can also replace vertical padding with just bigger line height.*/  	padding: 5px 0;  	margin-top: -5px;  	  	outline: none; /*Prevent border on focus in some browsers*/  }    .CETextInput:not(.empty) br,  .CETextInput img { /*We hide <img> here. If you need images do not hide them but set maximum height. User can paste image by pressing Ctrl+V or Ctrl+Insert.*/  	display: none;  }    .CETextInput * {  	display: inline;  	white-space: pre;  }    /*NEW CODE:*/    .CETextInput[placeholder].empty::before { /*Use ::before not ::after or you will have problems width first <br>*/  	content: attr(placeholder);  	display: inline-block;  	width: 0;  	white-space: nowrap;  	pointer-events: none;  	cursor: text;  	color: #b7b7b7;  	  	padding-top: 8px;  	margin-top: -8px;  }
<!--OLD CODE:-->    <div class="CETextInputBorder">  	<div class="CETextInputCont">  		<div class="CETextInput" placeholder="Type something here" contenteditable><br></div>  	</div>  </div>    <!--We manually added <br> element for Firefox browser because Firefox (tested on 2019-05-11, Firefox 66) has bug with bad text cursor position in empty contenteditable elements that have ::before or ::after pseudo-elements.-->


Solution with only one div and "scrollbar-width"

You can also use only one div by setting "overflow-x: auto", "overflow-y: hidden" and "scrollbar-width: none". But "scrollbar-width" is new property and works only in Firefox 64+ and no other browsers yet.

You can also add:

  • webkit-prefixed version: "-webkit-scrollbar-width: none"
  • non-standardized ".CETextInput::-webkit-scrollbar { display: none; }" (for webkit-based browsers)
  • "-ms-overflow-style: none"

I would not recommend to use this solution, but here is example:

//OLD CODE:    document.querySelectorAll('.CETextInput').forEach(el => {  	//Focusing on child is not needed anymore  	  	//Prevent Enter to prevent blur on Enter  	el.addEventListener('keydown', function(e) {  		if (e.keyCode === 13)  			e.preventDefault();  	});  	  	//Update "empty" class on all "CETextInput" elements:  	updateEmpty.call(el); //init  	el.addEventListener('input', updateEmpty);    	function updateEmpty(e) {  		const s = this.innerText.replace(/[\r\n]+/g, ''); //You must use this replace, see explanation below in "Step 3"  		this.classList.toggle('empty', !s);  	}  });
/*NEW CODE:*/    .CETextInput {  	white-space: pre; /*"pre" is like "nowrap" but displays all spaces correctly (with "nowrap" last space is not displayed in Firefox, tested on Firefox 66, 2019-05-15)*/  	overflow-x: auto; /*or "scroll"*/  	overflow-y: hidden;  	-webkit-scrollbar-width: none; /*Chrome 4+ (probably), webkit based*/  	scrollbar-width: none; /*FF 64+, Chrome ??+, webkit based, Edge ??+*/  	-ms-overflow-style: none; /*IE ??*/  	  	/*Style:*/  	width: 10em;  	height: 1em;  	line-height: 1em;  	padding: 5px;  	border: 1px solid #aaa;  	font-size: 20px;  	font-family: sans-serif;  }    .CETextInput::-webkit-scrollbar {  	display: none; /*Chrome ??, webkit based*/  }    /*OLD CODE:*/    .CETextInput:not(.empty) br,  .CETextInput img { /*We hide <img> here. If you need images do not hide them but set maximum height. User can paste image by pressing Ctrl+V or Ctrl+Insert.*/  	display: none;  }    .CETextInput * {  	display: inline;  	white-space: pre;  }
<!--NEW CODE:-->    <div class="CETextInput" contenteditable></div>

This solution has 3 problems with paddings:

  1. In Firefox (tested on 2019-05-11, Firefox 66) there is no right padding when long text is typed. That is because Firefox does not display bottom or right padding when using padding in the same element that have scrollbar and when content is scrolled to the end.
  2. In all browsers there is no padding when scrolling long text in middle position. It looks worse. <input type="text"> does not have this problem.
  3. When user press home or end browsers scroll to place paddings are not visible.

To solve these problems you need use 3 elements like we used before but in this case you don't need use scrollbar-width. Our solution with 3 elements does not have these problems.


Other problems (in every solution):

  • Blur on pasting text ends with line break. I will think how to fix it.
  • When using paddings this.children[0].focus() is not enough in webkit-based browsers (cursor position is not where user clicked). I will think how to fix it.
  • Firefox (tested on 2019-05-11, Firefox 66): When short text is typed user cannot select last word by double clicking on the right of it. I will think about it.
  • When user starts text selection in the page he can end it in our field. Usual <input type="text"> does not have this behavior. But I don't think it is critical.
like image 39
vitaliydev Avatar answered Sep 18 '22 02:09

vitaliydev