Bagwell
Bagwell

Reputation: 2226

Placeholder for contenteditable div

I have the following: FIDDLE

The placeholder works fine and dandy until you type something, ctrl + A, and delete. If you do that, the placeholder disappears and never shows up again.

What's wrong? How can I have a placeholder for a contenteditable div?

HTML:

<div class="test" placeholder="Type something..." contenteditable="true"></div>


CSS:

.test {
    width: 500px;
    height: 70px;
    background: #f5f5f5;
    border: 1px solid #ddd;
    padding: 5px;
}

.test[placeholder]:empty:before {
    content: attr(placeholder);
    color: #555; 
}


Thanks.

Upvotes: 153

Views: 104841

Answers (15)

Igor S.
Igor S.

Reputation: 446

since none of the above solved my problem and after a week of suffering I found a solution

  '[contenteditable=true]:empty:before': {
            content: 'attr(placeholder)',
            opacity: 0.5,
            color: '#8099ac',
            position: 'absolute',
            top: '5px'
        },
  '[contenteditable=true]:focus:before': {
            content: '""',
            opacity: 0
        }

If you want full code with MUI and styled-components

<Box {...containerProps}>
            <Label {...labelProps} shrink htmlFor="input">
                {label} <span>{required && '*'}</span>
            </Label>

            <ContentEditableBox
                placeholder={placeholder}
                ref={divRef}
                id="editableDiv"
                contentEditable
                onInput={e => handleOnChange(e.target.innerHTML)}
                isOutlined={isOutlined}
                suppressContentEditableWarning={true}
            />
</Box>

const ContentEditableBox = styled(Box, {
    shouldForwardProp: prop => prop !== 'isOutlined'
})(({ isOutlined, theme }) => ({
    height: 'auto',
    minHeight: '38px',
    maxHeight: '100px',
    overflow: 'auto',
    width: '100%',
    lineHeight: '28px',
    padding: '4px 8px',
    display: 'inline-block',
    borderRadius: '4px',
    outline: isOutlined && `1px solid ${theme.palette.custom.beauBlue} !important`,
    fontSize: '14px',
    fontWeight: 'normal',
    fontStretch: ' normal',
    letterSpacing: 'normal',
    background: 'white',
    position: 'relative',
    MozAppearance: 'textfield-multiline',
    WebkitAppearance: 'textarea',
    resize: 'both',
    whiteSpace: 'pre-wrap',
    wordWrap: 'break-word',
    '&[contenteditable=true]:empty:before': {
        content: 'attr(placeholder)',
        opacity: 0.5,
        color: '#8099ac',
        position: 'absolute',
        top: '5px'
    },
    '&[contenteditable=true]:focus:before': {
        content: '""',
        opacity: 0
    }
}));

Upvotes: 0

pmad
pmad

Reputation: 182

let contenteditableDiv = document.getElementById('contenteditableDiv');

contenteditableDiv.addEventListener('focus', function() {
  let phs = this.querySelector('.placeholder-span');
  if (phs != null) {
    if (!this.hasOwnProperty('placeholderSpan')) {
      this.placeholderSpan = phs;
    }
    phs.remove();
    document.getSelection().setPosition(this, 0);
  }
});

contenteditableDiv.addEventListener('focusout', function() {
  if (this.textContent.trim().length == 0 && this.hasOwnProperty('placeholderSpan')) {
    this.replaceChildren(this.placeholderSpan);
  }
});
.placeholder-span {
  opacity: 0.5;
}
<div id="contenteditableDiv" contenteditable="true"><span class="placeholder-span">Type something...</span></div>

And if You want to avoid contenteditable HTML formatting problems (leading/trailing spaces) and write it like a normal person:

<div id="contenteditableDiv" contenteditable="true">
    <span class="placeholder-span">Type something...</span>
</div>

Then add:

window.addEventListener('load', function() {
    let contenteditableDiv = document.getElementById('contenteditableDiv');
    contenteditableDiv.innerHtml = contenteditableDiv.innerHtml.trim();
});

And if You want the placeholder to stay unitll there's input You need to put proper logic into mousedown, beforeinput and input event listeners.

Upvotes: 0

Seighth Hellsing
Seighth Hellsing

Reputation: 71

This happens because when you ctrl+A then delete, there is a <br> remaining in the innerHTML of the textarea. A simple jQuery/javascript solution can do the trick to empty out the textarea:

$(document).on('input','.test',function(){
    if(this.innerHTML == '<br>'){
        $(this).html('');
    }
});

Upvotes: 0

Mohamad Hamouday
Mohamad Hamouday

Reputation: 2743

This works for me and it's trim the long placeholder if the input is too small

[contenteditable="true"][placeholder]:empty:before {
    content: attr(placeholder);
    position: absolute;
    left: 5px;
    font-size: 13px;
    color: #aaa;
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
    max-width: 100%;
    direction: ltr;
}

Upvotes: 0

nicolas
nicolas

Reputation: 730

from Placeholder in contenteditable - focus event issue

[contenteditable=true]:empty:not(:focus):before{
  content:attr(data-ph);
  color:grey;
  font-style:italic;
}

Upvotes: 64

Sahil Ralkar
Sahil Ralkar

Reputation: 2424

This solution worked for me. I'd converted this solution from angular to pure javaScript

In .html

<div placeholder="Write your message.." id="MyConteditableElement" onclick="clickedOnInput = true;" contenteditable class="form-control edit-box"></div>

In .css

.holder:before {
    content: attr(placeholder);
    color: lightgray;
    display: block;
    position:absolute;    
    font-family: "Campton", sans-serif;
}

in js.

clickedOnInput:boolean = false;
charactorCount:number = 0;
let charCount = document.getElementsByClassName('edit-box')[0];

if(charCount){
this.charactorCount = charCount.innerText.length;
}

if(charactorCount > 0 && clickedOnInput){
document.getElementById("MyConteditableElement").classList.add('holder');
}

if(charactorCount == 0 && !clickedOnInput){
document.getElementById("MyConteditableElement").classList.remove('holder');
}

getContent(innerText){
  this.clickedOnInput = false;
}

Upvotes: 1

Ege Hurturk
Ege Hurturk

Reputation: 953

I got this solution from: https://codepen.io/flesler/pen/AEIFc

Basically put this css code:

[contenteditable=true]:empty:before{
  content: attr(placeholder);
  pointer-events: none;
  display: block; /* For Firefox */
}

And have the placeholder attribute in your contenteditable div.

Upvotes: 63

Fritz Lin
Fritz Lin

Reputation: 371

I've created a live demo: "Placeholder for content-editable divs", by HTML & CSS.
Also, Codepen: https://codepen.io/fritx/pen/NZpbqW
Ref: https://github.com/fritx/vue-at/issues/39#issuecomment-504412421

.editor {
  border: solid 1px gray;
  width: 300px;
  height: 100px;
  padding: 6px;
  overflow: scroll;
}
[contenteditable][placeholder]:empty:before {
  content: attr(placeholder);
  position: absolute;
  color: gray;
  background-color: transparent;
}
<textarea class="editor"
  placeholder="Textarea placeholder..."
></textarea>
<br/>
<br/>
<div class="editor"
  contenteditable
  placeholder="Div placeholder..."
  oninput="if(this.innerHTML.trim()==='<br>')this.innerHTML=''"
></div>

Upvotes: 35

Eternal1
Eternal1

Reputation: 5625

While searching for the same problem I worked out a simple mixed css-JavaScript solution I'd like to share:

CSS:

[placeholder]:empty::before {
    content: attr(placeholder);
    color: #555; 
}

[placeholder]:empty:focus::before {
    content: "";
}

JavaScript:

jQuery(function($){
    $("[contenteditable]").focusout(function(){
        var element = $(this);        
        if (!element.text().trim().length) {
            element.empty();
        }
    });
});

Updated fiddle

Upvotes: 121

comerc
comerc

Reputation: 133

some fixes:

1) $element.text().trim().length - it solved problems with <div><br/></div> and &nbsp;

2) data-placeholder attr instead of placeholder - it is true way

3) common selector $("[contenteditable]") - it is true way

4) display: inline-block; - fix for Chrome and Firefox

JavaScript:

jQuery(function($){
    $("[contenteditable]").blur(function(){
        var $element = $(this);
        if ($element.html().length && !$element.text().trim().length) {
            $element.empty();
        }
    });
});

HTML:

<div data-placeholder="Type something..." contenteditable="true"></div>

CSS:

[contenteditable]:empty:before {
    content: attr(data-placeholder);
    color: grey;
    display: inline-block;
}

Upvotes: 8

Bogdan Kuštan
Bogdan Kuštan

Reputation: 5577

It feels like I am repeating myself, but why not to check contenteditable element mutations? Trying to bind everything to event that are changing content are pain in the butt. What if You need to add button (For example paste), or change content dynamically (javascript). My approach would be using MutationObservers. Demo fiddle

HTML:

<div class="test" id="test" placeholder="Type something..." contenteditable="true"></div>

CSS:

.test {
    width: 500px;
    height: 70px;
    background: #f5f5f5;
    border: 1px solid #ddd;
    padding: 5px;
}

.test[placeholder]:empty:before {
    content: attr(placeholder);
    color: #555; 
}

JavaScript:

var target = document.querySelector('#test');
var observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
      if (target.textContent == '') {
          target.innerHTML = '';
      }
  });    
});
var config = { attributes: true, childList: true, characterData: true };
observer.observe(target, config);

Upvotes: 6

luisZavaleta
luisZavaleta

Reputation: 1168

I have this function, and I always use to prevent this kind of things.

I use my function in this way:

var notEmpty = {}

    notEmpty.selector = ".no-empty-plz"
    notEmpty.event = "focusout"
    notEmpty.nonEmpty = "---"


    neverEmpty(notEmpty)

And I just add the no-empty-plz to the Elements I that don't want to be empty.

/**
     * Used to prevent a element have a empty content, made to be used 
when we want to edit the content directly with the contenteditable=true 
because when a element is completely empty, it disappears U_U
     * 
     * @param selector
     * @param event
     * @param nonEmpty:
     *        String to be put instead empty
     */
function neverEmpty(params) {

    var element = $(params.selector)



    $(document).on(params.event, params.selector, function() {

        var text = $(this).html()
        text = hardTrim(text)

        if ($.trim(text)  == "") {
            $(this).html(params.nonEmpty)
        }
    });
}

params is actually a json, so selector = params.selector as you can see

And hardTrim is also another fucntion I created is like a trim but includs &nbsp and <br/>, etc

function hardTrim(text) {

    if (!exists(text)) {
        return ""
    }
    text = text.replace(/^\&nbsp\;|<br?\>*/gi, "").replace(/\&nbsp\;|<br?\>$/gi, "").trim();

    return text
}

Upvotes: 0

Francisco Presencia
Francisco Presencia

Reputation: 8841

Updating Christian Brink's answer, you could/should check for more events. You can do so by simply doing:

// More descriptive name
var $input = $(".placeholder");
function clearPlaceHolder() {
  if ($input.text().length == 0) {
    $input.empty();
    }
  }

// On each click
$input.keyup(clearPlaceHolder);

// Probably not needed, but just in case
$input.click(clearPlaceHolder);

// Copy/paste/cut events http://stackoverflow.com/q/17796731
$input.bind('input', (clearPlaceHolder));

// Other strange events (javascript modification of value?)
$input.change(clearPlaceHolder);

Finally, the updated JSFiddle

Upvotes: 2

Christian Brink
Christian Brink

Reputation: 663

As swifft said, you can fix this with some super simple JS. Using jQuery:

var $input = $(".test");
$input.keyup(function () {
    if ($input.text().length == 0) {
        $input.empty();
    }
});

On each keystroke it checks whether there's any input text present. If not, it whacks any child elements that may have been left behind by user interaction with the element -- e.g. the <div> swifft describes.

Upvotes: 1

swifft
swifft

Reputation: 121

I see what you mean. In your fiddle I typed in a few characters and deleted it using 'ctrl-a' and 'delete', and the placeholder reappeared.

However, it seems as if when you hit 'enter' within the contenteditabele div it creates a child div containing the line break <div><br></div> creating an issue with the :empty pseudo-class which only targets elements with no child elements.**

Check it out in chrome developer tools or whatever you use.

From developer.mozilla.org

The :empty pseudo-class represents any element that has no children at all. Only element nodes and text (including whitespace) are considered. Comments or processing instructions do not affect whether an element is considered empty or not.

Ctrl-a will delete the text, but leaves the child div. Might be able to fix this by adding some javascript.

Upvotes: 7

Related Questions