0

I have created a flexbox container that acts as a form control with two input fields, representing a range between two dates. If the user has entered something into any of the two fields a cross appears next to that field. The user can click on this cross to clear the field.

The cross is an :after pseudo-element, created and interacted with, with some nifty css and JavaScript, largely based on this answer on Stack Overflow. Here's my implementation (somewhat simplified, but not much):

$('.field.clearable').on('change input', 'input', function($e) {
    $(this).parent().toggleClass('non-empty', !!this.value);
  })
  .on('mousemove', 'span.non-empty', function($e) {
    var $this = $(this);
    $this.toggleClass('onX', $this.width() < $e.clientX - $this.offset().left);
    $this.hasClass('onX') ? $this.attr('title', 'Clear field') : $this.removeAttr('title');
  })
  .on('mouseleave', 'span.non-empty', function($e) {
    var $this = $(this);
    $this.removeClass('onX')
      .removeAttr('title');
  })
  .on('click', 'span.non-empty.onX', function($e) {
    $e.preventDefault();
    var $this = $(this);
    $this.removeClass('non-empty onX')
      .removeAttr('title')
      .children('input')
      .val('')
      .trigger('clear');
  });
/* reset and cosmetic css */

* {
  margin: 0;
  padding: 0;
  color: inherit;
  font-family: inherit;
  font-size: inherit;
}

::-webkit-input-placeholder {
  color: #ccc;
  font-size: 80%;
  opacity: .8;
}

:-moz-placeholder {
  color: #ccc;
  font-size: 80%;
  opacity: .8;
}

::-moz-placeholder {
  color: #ccc;
  font-size: 80%;
  opacity: .8;
}

:-ms-input-placeholder {
  color: #ccc;
  font-size: 80%;
  opacity: .8;
}

:focus::-webkit-input-placeholder {
  opacity: .5;
}

:focus:-moz-placeholder {
  opacity: .5;
}

:focus::-moz-placeholder {
  opacity: .5;
}

:focus:-ms-input-placeholder {
  opacity: .5;
}

html {
  font-family: Arial;
  font-size: 10px;
  font-weight: normal;
  text-align: left;
  height: 100%;
}

body {
  margin: 4em auto;
  width: 400px;
  color: #2c5ba0;
  font-size: 1.5rem;
  line-height: 1.5em;
  background-color: #fff;
}


/* relevant css */

div.field {
  display: inline-block;
  margin: .4em 0;
}

div.field>label {
  display: block;
  margin: 0 0 .3em .2em;
  font-size: 80%;
  line-height: 1em;
}

div.field>span {
  position: relative;
  display: -webkit-flex;
  display: flex;
  -webkit-align-items: center;
  align-items: center;
  -webkit-justify-content: space-between;
  justify-content: space-between;
  box-sizing: border-box;
  height: 2em;
  border: .1em solid #ddd;
  background-color: #fff;
}

div.field>span>span {
  -webkit-flex: 1;
  flex: 1;
}

div.field>span>span>input {
  width: 100%;
  border: none;
}

div.field.range>span input {
  text-align: center;
}

div.field.range>span label {
  -webkit-flex: 0;
  flex: 0;
  margin: 0 .2em;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

div.field.clearable span.non-empty {
  position: relative;
  padding-right: 2em;
}

div.field.clearable span.non-empty:after {
  position: absolute;
  right: 0;
  top: 0;
  content: 'x';
  display: inline-block;
  width: 2em;
  height: 2em;
  color: #ddd;
  vertical-align: middle !important;
  text-align: center !important;
  font-style: normal !important;
  font-weight: normal !important;
  text-transform: none !important;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  cursor: pointer;
}

div.field.clearable span.non-empty.onX:after {
  color: #f37e31;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="field clearable range">
  <label for="start">created</label>
  <span>
    <span>
      <input type="text" id="start" placeholder="from">
    </span>
    <label for="end">–</label>
    <span>
      <input type="text" id="end" placeholder="until">
    </span>
  </span>
</div>

When both <input> fields contain values and thus have a cross next to them, their width-ratios cancel each other out nicely and nothing is wrong. However, when only one of the <input> fields contains a value and the cross appears next to only that <input> field, the cross / <input> field nudges the other content aside.

This is not supposed to happen. I suspect the <input> fields are the culprit. Even though the container div.field should keep its display: block flexibility, the ratios of the inner content should stay constant.

Here are the relevant bits I thought I could achieve this with:

/* the span that acts as the flexbox container */
div.field>span {
  position: relative;
  display: -webkit-flex;
  display: flex;
  -webkit-align-items: center;
  align-items: center;
  -webkit-justify-content: space-between;
  justify-content: space-between;
  box-sizing: border-box;
  height: 2em;
  border: .1em solid #ddd;
}

/* let the spans, containing the <input> fields, take available space */
div.field>span>span {
  -webkit-flex: 1;
  flex: 1;
}

/*
  let <input> fields take 100% content space of span

  I thought this should take into account any padding on its parent
*/
div.field>span>span>input {
  width: 100%;
  border: none;
}

/* let center label (the dash) not be flexible */
div.field.range>span label {
  -webkit-flex: 0;
  flex: 0;
  margin: 0 .2em;
}

/* if .non-empty class was set by JavaScript */

/*
  make room for :after by setting padding-right accordingly

  but this is the issue:
  I thought this would also make the <input> shrink accordingly,
  but it appears it doesn't shrink the amount I anticipated
*/
div.field.clearable span.non-empty {
  position: relative;
  padding-right: 2em;
}

/* show :after pseudo-element */
div.field.clearable span.non-empty:after {
  position: absolute;
  right: 0;
  top: 0;
  content: 'x';
  display: inline-block;
  width: 2em;
  height: 2em;
  /* etc. */
}

Do you know how I can let the complete control element maintain its flexibility, including its :after trickery, but have the <input>s shrink accordingly, when one of the crosses appear?

Or put more simply, I guess: is there a way to let two flex-items, with flex: 1, both take up an equal amount of space, at any given time?


The reason I'm using :after elements and not background-image like in the original answer I took the inspiration from, is because I'm using a custom icon font and not icon images.

Decent Dabbler
  • 22,532
  • 8
  • 74
  • 106
  • Where should the pseudo go? ... on top of the input? – Asons Apr 11 '17 at 09:06
  • @LGSon No, the pseudo (a child of the `` containing the input) should go next to the input (otherwise input text could appear underneath the cross, which I don't want to happen). The pseudo is `2em` wide and I therefore make room for it by setting `padding-right: 2em` on the ``. I thought the input would then shrink accordingly, but it doesn't appear to do that. – Decent Dabbler Apr 11 '17 at 09:11
  • @LGSon You've actually made me reconsider it. I just realized the text will appear to nudge with the padding anyway, regardless of my current problem, since the text is center aligned. But if I ditch the padding and just set a `background-color` on the pseudo element and put it on top of the text field, my problem will likely be solved. I'll have to tweak the JavaScript that detects whether the cross is being clicked to account for this, though. But thank you for your question. That has helped me attain a new perspective. Cheers! – Decent Dabbler Apr 11 '17 at 09:39

2 Answers2

2

However, when only one of the fields contains a value and the cross appears next to only that field, the cross / field nudges the other content aside.

This is not supposed to happen. I suspect the fields are the culprit. Even though the container div.field should keep its display: block flexibility, the ratios of the inner content should stay constant.

This is because of the way your elements are structured for the close button trickery. Typically the padding on spans which then causes the flex items to gain or lose their size and affect the flex layout. But, if you remove the padding, then you would have a hard time calculating the offset for click-to-close.

Also, you have to keep on tinkering with the width and computing the position of mouse cursor on mousemove to keep the close button working.

One of the possible better solutions would be to use padding on inputs instead, and use only the position and opacity of the pseudo-element to work out the appearance and disappearance of the close button. This will keep the flex working without problems.

Then, rather than tracking the mousemove, you simply check the click on the span.

Example Fiddle (no jQuery): http://jsfiddle.net/abhitalks/4fnvukc2/

Example Snippet:

var closers = document.querySelectorAll('.closer'), 
    inputs = document.querySelectorAll('.closer input');

for (i=0; i < closers.length; i++) {
  closers[i].addEventListener('click', clearer);
}
for (i=0; i < inputs.length; i++) {
  inputs[i].addEventListener('input', closer);
}

function clearer(e) {
  if (e.target.tagName == 'SPAN') {
    e.target.firstElementChild.value = '';
    e.target.classList.remove('dirty');
  }
}
function closer(e) {
  e.stopPropagation();
  e.target.parentElement.classList.add('dirty');
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-size: 1em; font-family: sans-serif;}
div.field { 
  margin: 4em; 
  display: flex; justify-content: space-between;
}
span.closer {
  display: inline-block; position: relative;
  flex: 1 1 auto;
}
span.sep { flex: 0 1 auto; margin: 0 1em;}
span.closer::before {
  content: '×'; font-weight: bold;
  position: absolute; 
  right: -10px; top: 4px; opacity: 0;
  transition: all 0.2s ease-out;
}
span.closer.dirty::before { 
  right: 6px; opacity: 1; cursor: pointer;
}
span.closer.dirty:hover::before { color: #00f; }
.closer input { padding: 3px 16px 3px 4px; width: 100%; }
::-webkit-input-placeholder { color: #ccc; }
:-moz-placeholder { color: #ccc; }
<div class="field range">
  <span class="closer">
    <input type="text" id="start" placeholder="from" />
  </span>
  <span class="sep">–</span>
  <span class="closer">
    <input type="text" id="end" placeholder="until" />
  </span>    
</div>
Abhitalks
  • 27,721
  • 5
  • 58
  • 81
  • This looks like a very nifty and much cleaner solution (than my own initial trickery, I should add :)). Thanks! The `mousemove` trickery was actually bugging me. Just for my understanding though: will `e.target.tagName == 'SPAN'` yield true because `::before` is considered a part of the `` by the DOM? – Decent Dabbler Apr 11 '17 at 10:26
  • Yes @DecentDabbler, it is part of span. – Abhitalks Apr 11 '17 at 10:27
  • @LGSon Yep, even though I thoroughly appreciated your input, I may have to select this one as best answer. This really looks very clean. – Decent Dabbler Apr 11 '17 at 10:27
  • @DecentDabbler I would pick this one too :) – Asons Apr 11 '17 at 10:28
  • 2
    LGSon was the first to find the problem though. Thanks @LGSon indeed!' – Abhitalks Apr 11 '17 at 10:28
1

If you drop the padding-rightand resize the input it will work

You need to fix the click handler though

div.field.clearable span.non-empty {
  position: relative;
}

div.field.clearable span.non-empty input {
  width: calc(100% - 2em);
}

$('.field.clearable').on('change input', 'input', function($e) {
    $(this).parent().toggleClass('non-empty', !!this.value);
  })
  .on('mousemove', 'span.non-empty', function($e) {
    var $this = $(this);
    $this.toggleClass('onX', $this.width() < $e.clientX - $this.offset().left);
    $this.hasClass('onX') ? $this.attr('title', 'Clear field') : $this.removeAttr('title');
  })
  .on('mouseleave', 'span.non-empty', function($e) {
    var $this = $(this);
    $this.removeClass('onX')
      .removeAttr('title');
  })
  .on('click', 'span.non-empty.onX', function($e) {
    $e.preventDefault();
    var $this = $(this);
    $this.removeClass('non-empty onX')
      .removeAttr('title')
      .children('input')
      .val('')
      .trigger('clear');
  });
/* reset and cosmetic css */

* {
  margin: 0;
  padding: 0;
  color: inherit;
  font-family: inherit;
  font-size: inherit;
}

::-webkit-input-placeholder {
  color: #ccc;
  font-size: 80%;
  opacity: .8;
}

:-moz-placeholder {
  color: #ccc;
  font-size: 80%;
  opacity: .8;
}

::-moz-placeholder {
  color: #ccc;
  font-size: 80%;
  opacity: .8;
}

:-ms-input-placeholder {
  color: #ccc;
  font-size: 80%;
  opacity: .8;
}

:focus::-webkit-input-placeholder {
  opacity: .5;
}

:focus:-moz-placeholder {
  opacity: .5;
}

:focus::-moz-placeholder {
  opacity: .5;
}

:focus:-ms-input-placeholder {
  opacity: .5;
}

html {
  font-family: Arial;
  font-size: 10px;
  font-weight: normal;
  text-align: left;
  height: 100%;
}

body {
  margin: 4em auto;
  width: 400px;
  color: #2c5ba0;
  font-size: 1.5rem;
  line-height: 1.5em;
  background-color: #fff;
}


/* relevant css */

div.field {
  display: inline-block;
  margin: .4em 0;
}

div.field>label {
  display: block;
  margin: 0 0 .3em .2em;
  font-size: 80%;
  line-height: 1em;
}

div.field>span {
  position: relative;
  display: -webkit-flex;
  display: flex;
  -webkit-align-items: center;
  align-items: center;
  -webkit-justify-content: space-between;
  justify-content: space-between;
  box-sizing: border-box;
  height: 2em;
  border: .1em solid #ddd;
  background-color: #fff;
}

div.field>span>span {
  -webkit-flex: 1;
  flex: 1;
}

div.field>span>span>input {
  width: 100%;
  border: none;
}

div.field.range>span input {
  text-align: center;
}

div.field.range>span label {
  -webkit-flex: 0;
  flex: 0;
  margin: 0 .2em;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

div.field.clearable span.non-empty {
  position: relative;
}

div.field.clearable span.non-empty input {
  width: calc(100% - 2em);
}

div.field.clearable span.non-empty:after {
  position: absolute;
  right: 0;
  top: 0;
  content: 'x';
  display: inline-block;
  width: 2em;
  height: 2em;
  color: #ddd;
  vertical-align: middle !important;
  text-align: center !important;
  font-style: normal !important;
  font-weight: normal !important;
  text-transform: none !important;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  cursor: pointer;
}

div.field.clearable span.non-empty.onX:after {
  color: #f37e31;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="field clearable range">
  <label for="start">created</label>
  <span>
    <span>
      <input type="text" id="start" placeholder="from">
    </span>
    <label for="end">–</label>
    <span>
      <input type="text" id="end" placeholder="until">
    </span>
  </span>
</div>
Asons
  • 84,923
  • 12
  • 110
  • 165
  • Yep, this is actually a viable solution to my initial problem, as well. I'll have to fiddle with the dimensions a bit though; `2em` actually seems a bit much, now that I've tried it, but that's irrelevant to the actual problem. Thanks again! – Decent Dabbler Apr 11 '17 at 09:56
  • 1
    @DecentDabbler Glad to help a fellow developer – Asons Apr 11 '17 at 10:00