megclaypool
10/7/2019 - 7:58 PM

Sliding Form Labels scss + js

See the styling and js on teensource for latest

I've changed this yet again. As of now, what I've got is a function that can be called on as many or as few forms as desired. Each time you call it, you can set script parameters to match the styling of that specific form. (I've added some code to help match labels to inputs in poorly written forms, though it only works if the poorly-matched label and input are surrounded by a wrapper div. By properly matching them, clicking the label puts the focus on the input, which is necessary for the animation to work properly.)

For styling, there's a pair of mixins -- one with fairly generic form styling and one with animated label styling. Both have parameters that need to match the parameters passed in the script. There's a commented-out example of how to call the mixins at the top of the mixin file. Then each form can be individually styled to match the designs.

/*
(2020-01-23)
Ok. This function does a lot of stuff. 

* It checks each input to see whether its label is before or after the input and attaches a class of "label-before" or "label-after" to the label so it can be appropriately styled. (Note: labels that wrap around an input are rendered as before, at least in Chrome...)

* It checks the width of all inputs and checks to see whether their width is 100% of the containing element. If so, it attaches a class of "full-width" to both the input and its label.

* It finds each input of type text, email, or password and each textarea and adds a "placeholder" class to input and the matching label. ("Matching" labels can either use the "for" attribute or can wrap around the input.)

* It finds each textarea and adds a "textarea-label" class to its matching label.

* Each time focus is on one of the placeholder inputs a "busy" class is added.

* Each time focus moves away from one of the placeholder inputs, the "busy" class is removed. Meanwhile, if there is text in the input, a "filled" class is added. If there is no content, the "filled" class is removed.
*/



/*
 * How to call this function (from another file)
 */
 
// jQuery(document).ready(function($) {
//   var target = "form.mobilecommons";
//   fancyLabels(target, {
//     aggressiveMatch: true,
//     inputBoxHeight: 35
//   });
// });


/*
 * Finally, the fancy labels function you've all been waiting for:
 */

function fancyLabels(
  target,
  options
) {

  var options = options || {};
  options.fontSize = options.fontSize || 16;
  options.inputBoxHeight = options.inputBoxHeight || 53;
  options.lineHeight = options.lineHeight || 1.5;
  options.shrinkRatio = options.shrinkRatio || 0.8;
  // aggressiveMatch is for those exciting forms that don't properly link inputs and their labels (boo, hiss). Only enable this if the form uses containing divs to unite the inputs and their labels. otherwise you're just hosed on those forms :shrug:
  options.aggressiveMatch = options.aggressiveMatch || false;

  // note that:
  // inputBoxHeight should match the textbox-height variable in _forms.scss
  // lineHeight should match the label-line-height variable in _forms.scss
  // fontSize should match the lable-font-size variable in _forms.scss
  // shrinkRatio should match the label-shrink-ratio variable in _forms.scss

  // Find all the textareas
  var textareas = jQuery(target + " textarea");

  // Add a "textarea-label" class to each of those labels
  if (typeof textareas == "object") {
    Object.keys(textareas).forEach(function(textarea) {
      if (textarea != "length" && textarea != "prevObject") {
        addTextarea(textareas[textarea], options);
      }
    });
  } else if (typeof textareas == "array") {
    textareas.forEach(function(textarea) {
      addTextarea(textarea, options);
    });
  }

  // Find all the inputs
  var allInputs = jQuery(target + " input");
  var inputLength = allInputs.length;

  // add in the textareas
  for (var i = 0; i < textareas.length; i++) {
    allInputs[inputLength + i] = textareas[i];
  }
  allInputs.length += textareas.length;


  // check if the label is before or after the input, and if it's full width. Add descriptive classes.
  if (typeof allInputs == "object") {
    Object.keys(allInputs).forEach(function(input) {
      if (input != "length" && input != "prevObject") {
        checkIfLabelFirst(allInputs[input]);
        tagFullWidth(allInputs[input]);
      }
    });
  } else if (typeof allInputs == "array") {
    allInputs.forEach(function(input) {
      checkIfLabelFirst(input);
      tagFullWidth(input);
    });
  }

  // Find the text inputs that need placeholder labels
  var placeholderInputs = jQuery(target + " input[type='text']");

  // We're having trouble with textareas in safari on an iphone 7, so I'm commenting them out so they don't get added
  // // add in the textareas
  // var placeholderTextareas = jQuery(target + " textarea");
  // for (var i = 0; i < placeholderTextareas.length; i++) {
  //   placeholderInputs[placeholderInputs.length + i] =
  //     placeholderTextareas[i];
  // }
  // placeholderInputs.length += placeholderTextareas.length;

  // add in the email fields
  var placeholderEmails = jQuery(target + " input[type='email']");
  for (var i = 0; i < placeholderEmails.length; i++) {
    placeholderInputs[placeholderInputs.length + i] = placeholderEmails[i];
  }
  placeholderInputs.length += placeholderEmails.length;

  // add in the password fields
  var placeholderPasswords = jQuery(target + " input[type='password']");
  for (var i = 0; i < placeholderPasswords.length; i++) {
    placeholderInputs[placeholderInputs.length + i] =
      placeholderPasswords[i];
  }
  placeholderInputs.length += placeholderPasswords.length;

  // add in the telephone number fields
  var placeholderPhones = jQuery(target + " input[type='tel']");
  for (var i = 0; i < placeholderPhones.length; i++) {
    placeholderInputs[placeholderInputs.length + i] = placeholderPhones[i];
  }
  placeholderInputs.length += placeholderPhones.length;

  // Add a "placeholder" class to each of those labels
  // Also assign height and top position
  if (typeof placeholderInputs == "object") {
    Object.keys(placeholderInputs).forEach(function(input) {
      if (input != "length" && input != "prevObject") {
        addPlaceholder(placeholderInputs[input]);
        setLabelHeight(placeholderInputs[input]);
        setPlaceholderLabelPosition(placeholderInputs[input]);
      }
    });
  } else if (typeof placeholderInputs == "array") {
    placeholderInputs.forEach(function(input) {
      addPlaceholder(input);
      setLabelHeight(input);
      setPlaceholderLabelPosition(input);
    });
  }

  // These placeholders get cool effects on interaction
  placeholderInputs
    .on("focus", addBusy)
    .on("blur", removeBusy)
    .on("blur change", toggleFilled);

  // Lastly, check each input one last time.
  // If an input is somehow prefilled, set its class
  // to filled
  // If an input has focus on pageload, set its class
  // to busy and its top to 0
  if (typeof allInputs == "object") {
    Object.keys(allInputs).forEach(function(input) {
      if (input != "length" && input != "prevObject") {
        checkFilled(allInputs[input]);
        checkFocus(allInputs[input]);
      }
    });
  } else if (typeof allInputs == "array") {
    allInputs.forEach(function(input) {
      checkFilled(input);
      checkFocus(input);
    });
  }
// }


  function checkFocus(target) {
    if (document.activeElement === target && document.hasFocus()) {
      var label = getLabel(target);
      if (label !== undefined) {
        if (typeof label === "object" && label.length > 0) {
          label = label[0];
        }
        jQuery(label).addClass("busy");
        label.style.top = 0 + "px";
      }
    }
  }

  function addPlaceholder(input) {
    var label = getLabel(input);
    if (label !== undefined) {
      jQuery(input).addClass("placeholder");
      jQuery(label).addClass("placeholder");
    }
    var parent = getParent(input);
    if (parent !== undefined) {
      jQuery(parent).addClass("placeholder");
    }
  }

  function setLabelHeight(input) {
    var label = getLabel(input);
    if (label !== undefined) {
      if (typeof label === "object" && label.length > 0) {
        label = label[0];
      }
      label.style.height = label.offsetHeight + "px";
    }
  }

  function setPlaceholderLabelPosition(input) {
    var label = getLabel(input);
    if (label !== undefined) {
      if (typeof label === "object" && label.length > 0) {
        label = label[0];
      }
      // This vertically centers all labels inside their inputs
      // if (label.style !== undefined && label.style.height !== undefined) {
      //   var positionTop =
      //     (parseInt(label.style.height, 10) + input.offsetHeight) / 2;
      // } else {
      //   var positionTop = (label.offsetHeight + input.offsetHeight) / 2;
      // }
      // if (label.classList.contains("label-before")) {
      //   label.style.marginTop =
      //     -(1 - shrinkRatio) * label.offsetHeight + "px";
      // } else if (label.classList.contains("label-after")) {
      //   label.style.marginBottom =
      //     -(1 - shrinkRatio) * label.offsetHeight + "px";
      //   positionTop *= -1;
      // } else {
      //   var positionTop = 0;
      // }

      // This centers a single line label in a text input and puts it an equal amount down in a textarea
      if (label.classList.contains("label-before")) {
        if (label.style !== undefined && label.style.height !== undefined) {
          var positionTop =
            parseInt(label.style.height, 10) +
            (options.inputBoxHeight - options.lineHeight * options.fontSize) / 2;
        } else {
          var positionTop =
            label.offsetHeight +
            (options.inputBoxHeight - options.lineHeight * options.fontSize) / 2;
        }
        // label.style.marginTop =
        //   -(1 - shrinkRatio) * label.offsetHeight + "px";
      } else if (label.classList.contains("label-after")) {
        var positionTop =
          -1 * input.offsetHeight +
          (options.inputBoxHeight - options.lineHeight * options.fontSize) / 2;
        // label.style.marginBottom =
        //   -(1 - shrinkRatio) * label.offsetHeight + "px";
      } else {
        var positionTop = 0;
      }
      setLabelPosition(label, positionTop);
    }
  }

  function setLabelPosition(label, positionTop) {
    if (label !== undefined) {
      if (typeof label === "object" && label.length > 0) {
        label = label[0];
      }
      label.style.top = String(positionTop) + "px";
    }
  }

  function addTextarea(target) {
    var label = getLabel(target);
    if (label !== undefined) {
      jQuery(label).addClass("textarea-label");
    }
  }

  function toggleFilled(event) {
    var label = getLabel(event.target);
    if (label !== undefined) {
      if (event.target.value) {
        jQuery(label).addClass("filled");
        setLabelPosition(label, 0);
      } else {
        jQuery(label).removeClass("filled");
        setPlaceholderLabelPosition(event.target);
      }
    }
  }

  function checkFilled(element) {
    var label = getLabel(element);
    if (label !== undefined) {
      if (element.value) {
        jQuery(label).addClass("filled");
        setLabelPosition(label, 0);
      } else {
        jQuery(label).removeClass("filled");
        setPlaceholderLabelPosition(element);
      }
    }
  }

  function addBusy(event) {
    var label = getLabel(event.target);
    if (label !== undefined) {
      jQuery(label).addClass("busy");
      setLabelPosition(label, 0);
    }
  }

  function removeBusy(event) {
    var label = getLabel(event.target);
    if (label !== undefined) {
      jQuery(label).removeClass("busy");
    }
  }

  function getLabel(target) {
    var form = jQuery(target).closest("form");
    var type = target.getAttribute('type');
    if (type == 'hidden' || type == 'submit') {
      return undefined;
    }
    var id = target.getAttribute('id');
    // if the universe is a good and happy place, the form labels will be properly linked with their inputs
    if (id !== undefined && id != null && id !== "") {
      var label = form.find("label[for=" + id + "]");
    }

    // if label is still undefined here, then the universe is not a good and happy place. But there's still hope...
    if (label == undefined || label.length <= 0) {
      var parentElem = jQuery(target).parent();
      var parentTagName = parentElem.get(0).tagName.toLowerCase();

      // Sometimes labels are wrapped around their input, and so it's clear which label belongs to which input
      // I've tested in both Chrome and IE11 and clicking wrapped labels puts the focus on the input, so if this is the case we don't need to add a matching "for" attribute to the label
      if (parentTagName == "label") {
        label = parentElem;
      }
    }

    // ok, if that wasn't the case, then we've got one more thing to try
    if (label == undefined || label.length <= 0) {
      // this only works if labels are grouped with their inputs in containers. if that's the case, the aggressiveMatch setting can be set to true
      if (options.aggressiveMatch == true) {
        // look in the parent wrapper for a label. there should only be one!
        label = parentElem.find("label");

        // ok, assuming we just found the label, let's make sure it's set up so it's properly clickable!
        if (label != undefined || label.length > 0) {
          // first let's get the actual label
          if (typeof label === "object" && label.length > 0) {
            label = label[0];
          }

          // if the input already has an id, let's just use that
          if (id !== undefined && id !== "") {
            var labelFor = label.getAttribute("for");
            if( labelFor != id) {
              label.setAttribute("for", id);
            }
          } else {
            var timestamp = new Date().getTime();
            target.setAttribute('id', timestamp);
            label.setAttribute("for", timestamp);
          }
        } else {
          // we didn't find a label, so we're hosed
          return undefined;
        }
      } else {
        return undefined;
      }
      if (label == undefined || label.length <= 0) {
        return undefined;
      }
    }
    if (typeof label === "object" && label.length > 0) {
      label = label[0];
    }
    return label;
  }

  function getParent(target) {
    // we're looking for drupal webform parent divs
    return jQuery(target).parent('.form-item');
  }

  function checkIfLabelFirst(target) {
    var label = getLabel(target);

    var parent = getParent(target);

    if (label !== undefined) {
      if (typeof label === "object" && label.length > 0) {
        label = label[0];
      }
      if (label !== undefined) {
        // remember that compareDocumentPosition is a bitwise operator that adds up the various possibilities (like file permissions). So you get 10 when it spits out both 8 and 2
        switch (label.compareDocumentPosition(target)) {
          case 2: // input comes first
          case 8: // input contains label (???)
          case 10: // input contains label (yeah right!) & thus input comes first
            jQuery(target).addClass("label-after");
            jQuery(label).addClass("label-after");
            if (parent !== undefined) {
              jQuery(parent).addClass("label-after");
            }
            break;
          case 4: // label comes first
          case 16: // label contains input
          case 20: // label contains input & thus label is first
            jQuery(target).addClass("label-before");
            jQuery(label).addClass("label-before");
            if (parent !== undefined) {
              jQuery(parent).addClass("label-before");
            }
            break;
          default:
            console.log("I'm having trouble comparing the position of this input and its label:");
            console.log(target);
            console.log(label);
        }
      }
    }
  }

  function checkIfWidth100(target) {
    var parent = jQuery(target).parent()[0];
    var padding =
      parseInt(jQuery(parent).css("padding-left")) +
      parseInt(jQuery(parent).css("padding-right"));
    var percent = (
      (target.offsetWidth / (parent.offsetWidth - padding)) *
      100
    ).toFixed(0);

    if (percent == 100) {
      return true;
    } else {
      return false;
    }
  }

  function tagFullWidth(target) {
    if (checkIfWidth100(target)) {
      jQuery(target).addClass("full-width");
      var label = getLabel(target);
      if (label !== undefined) {
        jQuery(label).addClass("full-width");
      }
    }
  }
}
jQuery(document).ready(function($) {

  var target = "form.mobilecommons";
  fancyLabels(target, {
    aggressiveMatch: true,
    inputBoxHeight: 35
  });

});
...

form-labels:
  version: 1.x
  js:
    patterns/_patterns/10-atoms/61-formz/form-labels.js: {}
  dependencies:
    - core/jquery

hookup-form-labels:
  version: 1.x
  js:
    patterns/_patterns/30-organisms/98-hookup-form-block/_homepage-hookup-form.js: {}
  dependencies:
    - radicati_d8/form-labels
 
 ...
...

/**
 * Implements hook_preprocess_HOOK() for Block document templates.
 */
function radicati_d8_preprocess_block(&$variables) {
  if (!empty($variables['elements'])){
    if (!empty($variables['elements']['#id'])) {
      if ($variables['elements']['#id'] == 'teensource_system_main') {
        $variables['#attached']['library'][] = 'radicati_d8/hookup-form-labels';
      }
    }
  }
}

...
/*
 * Example use case in some scss file somewhere:
 */


// form.someform {

//   //this needs to match options.fontSize in the js
//   $label-font-size: 14px;
//   //this needs to match options.inputBoxHeight in the js
//   $input-box-height: 35px;
//   //this needs to match options.lineHeight in the js
//   $label-line-height: 1.5;
//   //this needs to match options.shrinkRatio in the js
//   $label-shrink-ratio: .8;

//   // the rest can be adjusted without messing with the js:
//   $form-border-width: 1px;
//   $input-background-color: white;
//   $input-border-radius: 3px;
//   $input-side-padding: 10px;
//   $input-top-padding: 10px;
//   $input-vertical-margin: 10px;
//   $label-placeholder-color: gray;
//   $label-regular-color: $color--primary;
//   $max-form-width: 100%;
//   $primary-form-color: $color--light-gray;
//   $secondary-form-color: $color--light-green;
//   $textarea-height: 100px;

//   @include basic-form-styling($form-border-width, $input-background-color, $input-border-radius, $input-side-padding, $input-vertical-margin, $label-font-size, $label-line-height, $label-regular-color, $max-form-width, $primary-form-color, $textarea-height, $input-box-height);

//   @include animated-form-labels($input-side-padding, $input-vertical-margin, $label-font-size, $label-placeholder-color, $label-regular-color, $label-shrink-ratio, $textarea-height, $input-box-height);


//   display: flex;
//   flex: 1 0 auto;
//   flex-wrap: wrap;

//    more form-specific styling here...

// }

@mixin basic-form-styling($form-border-width, $input-background-color, $input-border-radius, $input-side-padding, $input-vertical-margin, $label-font-size, $label-line-height, $label-regular-color, $max-form-width, $primary-form-color, $textarea-height, $textbox-height) {
  max-width: 100%;

  input,
  textarea {
    background-color: $input-background-color;
    color: $label-regular-color;
    display: inline-block;
    font-family: $font--primary;
    @include rem('font-size', $label-font-size);
    @include rem('padding', 5px $input-side-padding);
    @include rem('border-radius', $input-border-radius);
    width: 100%;
  }

  input:not([type='file']),
  textarea {
    border: $form-border-width solid $primary-form-color;
  }

  input,
  label,
  textarea {
    position: relative;
  }

  input[type="text"],
  input[type="email"],
  input[type="password"] {
    @include rem('height', $textbox-height);
  }

  textarea {
    @include rem('height', $textarea-height);
  }

  // add a bit of vertical margin around inputs that aren't checkboxes, radio buttons, or submit buttons
  input:not([type='checkbox']):not([type='radio']):not([type='submit']):not([type='image']):not([type='file']),
  textarea {
    @include rem('margin', $input-vertical-margin 0);

    &.label-before {
      margin-top: 0;
    }

    &.label-after {
      margin-bottom: 0;
    }
  }

  // however, if it's in a drupal webform div, apply the margin to the div instead of the input
  div.form-item {
    @include rem('margin', $input-vertical-margin 0);

    input:not([type='checkbox']):not([type='radio']):not([type='submit']):not([type='image']):not([type='file']),
    textarea {
      &.label-before {
        margin-bottom: 0;
      }

      &.label-after {
        margin-top: 0;
      }
    }
  }

  // set height of many inputs to be consistent
  input:not([type='checkbox']):not([type='radio']):not([type='text']):not([type='color']):not([type='submit']):not([type='image']) {
    @include rem('height', $textbox-height);
    // width: unset;
  }

  // here's where you set the width of most inputs
  input:not([type='checkbox']):not([type='radio']):not([type='color']):not([type='submit']):not([type='image']),
  textarea {}

  // and styling for drupal webforms
  div.form-item:not(.form-type-radio),
  .form-wrapper {
    width: 100%;
  }

  // add a little spacing between side-by-side items
  div.webform-options-display-side-by-side {
    display: flex;
    flex-wrap: wrap;

    .form-item {
      @include rem('margin-right', 20px);
      display: inline-flex;
      @include rem('flex', 1 0 200px);
    }
  }

  // general label styles
  label {
    color: $label-regular-color;
    display: inline-block;
    font-size: $label-font-size;
    line-height: $label-line-height;
    transition: all 0.5s ease-in-out;
    width: auto;
    z-index: 2;

    &::after {
      transition: all 0.5s ease-in-out;
    }

    @include breakpoint($breakpoint-lg) {
      // @include rem('left', 10px);
    }
  }

  // this fixes textarea scrollbars on ie and prevents the weird margin issue (which screws up the label) when textareas are resized horizontally
  textarea {
    overflow: auto;
    resize: vertical;
  }

  // massaging color input styling
  input[type='color'] {
    @include rem('height', $textbox-height);
    // @include rem('width', 2 * $textbox-height);
    height: 2em;
    @include rem('padding', 2px);
    width: 3em;
    // width: unset;
  }

  // oh buttons, let's make you pretty
  button[type='submit'],
  input[type='submit'] {
    //@include button($color--blue, white, $font--primary, $form-border-width, true);
    @include rem('padding', 5px 20px);
  }

  button[type='submit'],
  input[type='submit'],
  input[type='image'] {
    //@include rem('border-radius', 0);
    cursor: pointer;
    //font-size: $label-font-size;
    //@include rem('height', $textbox-height);
    //@include rem('margin', 5px 10px);
    vertical-align: middle;
    width: unset;

    &:active,
    &:focus,
    &:hover {
      //border-color: $color--blue;
    }
  }


  // If there's a list, it's probably being used to contain form elements...
  ul {
    list-style: none;
  }

  // Checkboxes and radio buttons look like shit if you don't fix the styling a bit. This method does depend on there being a container around your checkbox/radio button and its label.
  // In this example form, checkboxes and radio buttons are inside list elements
  li,
  .form-item.form-type-radio,
  .form-item.form-type-checkbox {

    align-items: center;
    display: flex;
    // display: inline-flex;
    flex-direction: row;

    input[type="checkbox"],
    input[type="radio"] {
      display: inline-block;
      @include rem('flex', 0 0 25px);
      @include rem('height', $textbox-height);

      &:checked+label {
        color: inherit;
      }
    }

    label {
      align-items: center;
      color: inherit;
      display: flex;
      flex-direction: row;
      flex: 1 1 auto;
      font-size: $label-font-size;
      font-weight: inherit;
      left: 0;
      letter-spacing: inherit;
      margin: 0;
      top: 0;
      width: auto;

      &::before {
        display: none;
      }
    }
  }

  // range inputs also have trouble lining up with their labels without a little bit of help. A wrapper element is necessary to make it pretty
  .input--range {
    align-items: center;
    display: flex;
    flex-direction: row;

    label {
      padding-right: 1ex;
    }
  }

  // Styling select inputs
  select {
    $svg-color: $primary-form-color;
    @import '00-base/mixins/inline-svg/_!_svgs/_fontawesome-chevron-down--solid.scss';

    border: $form-border-width solid $primary-form-color;
    border-radius: 0;
    color: $label-regular-color;
    display: block;
    font-size: $label-font-size;
    // line-height: $textbox-height;
    @include rem('height', $textbox-height);
    // margin: 0;
    max-width: 100%;
    @include rem('padding', 5px $input-side-padding);
    position: relative;
    width: 100%;

    background-image: inline-svg($svg-chevron-down--solid), linear-gradient(to bottom, $input-background-color 0%, $input-background-color 100%);

    background-repeat: no-repeat, repeat;
    background-position: right .7em top 50%, 0 0;
    background-size: .65em auto, 100%;

    // background-size: 1em auto, 100%;

    // undo the default select styling:
    -moz-appearance: none;
    -webkit-appearance: none;
    appearance: none;

    &::-ms-expand {
      display: none;
    }

    option {
      font-weight: normal;
    }
  }
}









/******* ********/








@mixin animated-form-labels($input-side-padding, $input-vertical-margin, $label-font-size, $label-placeholder-color, $label-regular-color, $label-shrink-ratio, $textarea-height, $textbox-height) {
  // Ok, I've got the javascript set up to detect if an input is full width, and to detect whether the input or the label comes first. Now I need to set up styling so that .label-before.full-width, .label-before:not(.full-width), .label-after.full-width, and .label-after:not(.full-width) all work correctly!

  label.placeholder {
    // border: 1px dashed blue;

    color: $label-placeholder-color;
    // @include rem('line-height', $textbox-height);
    @include rem('padding-left', $input-side-padding);
    @include rem('padding-right', $input-side-padding);

    // Here's the magic secret! Inline block elements aren't fully compatible with negative margins -- they work up to a point and then increasingly negative margins cease to have any effect. Meanwhile block level elements are 100% wide unless you explicitly set the width -- no good if you want your label to overlay the input border like the fieldset legends. *BUT* tables automatically shrink to fit their contents *AND* respond beautifully to negative margins :) :) :)
    // display: table-cell;
    display: flex;
    left: 0;
    width: 100%;

    // Make sure the required asterisk gets styled correctly!
    &::after {
      height: 100%;
      @include rem('margin', 0 10px);
      @include rem('min-width', 6px);
    }

    &.label-before {
      // @include rem('margin-bottom', -1 * $textbox-height);
      // @include rem('top', $input-vertical-margin);
      // top: calc(#{$textbox-height} / 2 + 1rem);
      // top: calc(#{$textbox-height} + #{$input-top-padding});
      // vertical-align: bottom;
      align-items: flex-end;

      // Make sure the required asterisk doesn't get subscripted!
      &::after {
        align-self: flex-end;
      }
    }

    &.label-after {
      // @include rem('margin-top', -1 * $textbox-height);
      // @include rem('bottom', $input-vertical-margin);
      // bottom: calc(#{$textbox-height} / 2 + 1rem);
      // vertical-align: top;
      align-items: flex-start;

      &::after {
        align-self: flex-start;
      }
    }

    // if the label's input is currently filled or being interacted with
    &.busy,
    &.filled {
      color: $label-regular-color;
      font-size: calc(#{$label-shrink-ratio} * #{$label-font-size});
      @include rem('left', -1 * $input-side-padding);
      // width: calc(100% * #{$label-shrink-ratio} + 5%);

      @include breakpoint($breakpoint-sm) {
        margin-right: auto;
      }

      &::after {
        height: calc(100% * #{$label-shrink-ratio});
      }

      &.label-before {
        // top: calc((-.5 * #{$textbox-height}) + #{$input-vertical-margin} - 2ex);
        top: 0;
      }

      &.label-after {
        bottom: calc((-.5 * #{$textbox-height}) + #{$input-vertical-margin} - 2ex);
        bottom: 0;
      }
    }

    // Textarea labels that come after the textarea need to use the textarea-height variable instead!
    &.textarea-label {

      &.label-after {
        bottom: calc(#{$textarea-height} + #{$input-vertical-margin} - #{$textbox-height});

        &.busy,
        &.filled {
          bottom: calc((-.5 * #{$textbox-height}) + #{$input-vertical-margin} - 2ex);
        }
      }
    }
  }

  input.placeholder,
  textarea.placeholder {
    display: block;
    //   &.label-before {
    //     overflow: visible;

    //     &::after {
    //       border: 1px dashed pink;
    //       content: 'asdf';
    //       padding-bottom: 1px;
    //       display: block;
    //       height: 30px;
    //       width: 100px;
    //       position: relative;
    //       z-index: 10;
    //       box-sizing: border-box;
    //       visibility: visible;
    //     }
    //   }
  }
}



$(".region-footer-right form input")
  .on("focus", addBusy)
  .on("blur", removeBusy)
  .on("blur", toggleFilled);

$(".webform-submission-form input:not([type=checkbox]), .webform-submission-form textarea")
  .on("focus", addBusy)
  .on("blur", removeBusy)
  .on("blur", toggleFilled);



function toggleFilled(event) {
  var $target = event.target;
  var $id = event.target.id;
  var $label = $("label[for=" + $id + "]");
  if ($target.value) {
    $($label).addClass("filled");
  } else {
    $($label).removeClass("filled");
  }
}

function addBusy(event) {
  var $id = event.target.id;
  var $label = $("label[for=" + $id + "]");
  $($label).addClass("busy");
}

function removeBusy(event) {
  var $id = event.target.id;
  var $label = $("label[for=" + $id + "]");
  $($label).removeClass("busy");
}
.region-footer-right {
    form {

      input,
      label {
        position: relative;
        display: inline-block;
        width: 100%;
      }

      input[type="text"],
      input[type="email"] {
        background: transparent;
        border: 1px solid $color--white;
        @include rem('border-radius', 4px);
        color: $color--white;
        @include rem('font-size', 16px);
        @include rem('height', 40px);
        @include rem('margin-bottom', 10px);
        @include rem('padding', 5px 10px);
        z-index: 10;
      }

      label {
        display: inline-block;
        margin: 0 auto;
        @include rem('top', 30px);
        transition: all 0.5s ease-in-out;
        z-index: 1;

        @include breakpoint($breakpoint-lg) {
          @include rem('left', 10px);
        }
      }

      input[type="checkbox"] {
        width: 20px;
        @include rem('flex', 0 0 20px);

        &+label {
          color: $color--white;
          font-size: inherit;
          font-weight: normal;
          letter-spacing: normal;

          &::before {
            display: none;
          }
        }
      }

      button[type="submit"] {
        cursor: pointer;
        @extend .button;
        @extend .button--red;
      }
    }

    .busy,
    .filled {
      label {
        top: 0;
        font-size: 0.8em;
      }
    }

    label.busy,
    label.filled {
      top: 0;
      font-size: 0.8em;
    }

    .consent-checkbox {
      align-items: center;
      display: flex;
      justify-content: center;
      @include rem('margin-top', 10px);

      @include breakpoint($breakpoint-lg) {
        justify-content: flex-start;
      }

      label {
        top: 0;
        flex: 0 0 auto;
        font-size: 1em;
        margin: 0;
        width: unset;
      }
    }
  }
form.webform-submission-form {
  $label-shrink-ratio: .8;
  $textbox-height: 40px;
  $input-side-padding: 10px;

  .form-type-textfield:not(.form-type-webform-multiple),
  .form-type-textarea,
  .form-type-email,
  .form-type-webform-email-multiple,
  .form-type-webform-autocomplete:not(.form-type-webform-multiple),
  .form-type-number:not(.form-type-webform-multiple),
  .form-type-search,
  .form-type-tel:not(.form-type-webform-multiple) {

    input,
    label,
    textarea {
      display: inline-block;
      position: relative;
      width: 100%;
    }

    // input[type="text"],
    // input[type="email"],
    input,
    textarea {
      // background: transparent;
      border: 1px solid $color--black;
      @include rem('border-radius', 4px);
      color: $color--black;
      display: inline-block;
      @include rem('font-size', 16px);
      @include rem('padding', 5px $input-side-padding);
      // z-index: 1;
    }

    input[type="text"],
    input[type="email"] {
      @include rem('height', $textbox-height);
    }

    label {
      // border: 1px dashed black;

      // align-items: stretch;
      // background: white;
      color: $color--black;
      // display: inline-flex;
      @include rem('left', $input-side-padding);
      @include rem('line-height', $textbox-height);
      @include rem('margin', (-.5 * $textbox-height) auto 0);
      // margin-bottom: -10px;
      padding: 0;
      // @include rem('padding', 0 $input-side-padding);
      @include rem('top', $textbox-height);
      transition: all 0.5s ease-in-out;
      width: auto;
      z-index: 2;

      @include breakpoint($breakpoint-lg) {
        // @include rem('left', 10px);
      }

      &.busy,
      &.filled {
        font-size: $label-shrink-ratio * 1em;
        left: 0;
        // @include rem('line-height', $label-shrink-ratio * $textbox-height);
        // top: $textbox-height / 2;
        @include rem('padding', 0 5px);
        @include rem('top', 5px);
      }
    }

    button[type="submit"] {
      cursor: pointer;
      @extend .button;
      @extend .button--red;
    }
  }

  // this :not() is site-specific...
  .form-type-checkbox:not(.form-item-terms-of-service) {

    align-items: center;
    display: flex;
    // width: 100%;

    input[type="checkbox"] {
      display: inline-block;
      @include rem('flex', 0 0 25px);
      @include rem('height', $textbox-height);
      // @include rem('width', 20px);

      &:checked+label {
        color: inherit;
      }
    }

    label {
      color: inherit;
      flex: 1 1 auto;
      font-size: inherit;
      font-weight: inherit;
      left: 0;
      letter-spacing: inherit;
      margin: 0;
      top: 0;
      width: auto;

      &::before {
        display: none;
      }
    }
  }

  .webform-type-webform-email-confirm {
    .fieldset-wrapper {
      display: flex;
      flex-wrap: wrap;
      justify-content: space-between;

      @include breakpoint($breakpoint-md) {
        flex-wrap: nowrap;
      }

      .form-type-email {
        flex: 1 0 100%;

        @include breakpoint($breakpoint-md) {
          flex: 0 0 calc(50% - 10px);
          margin: 0;
        }
      }
    }
  }

  .form-item-rating {
    display: flex;
  }
}
// This is the latest as of 1/8/20

/*
Ok. This script does a lot of stuff.

* It checks each input to see whether its label is before or after the input and attaches a class of "label-before" or "label-after" to the label so it can be appropriately styled. (Note: labels that wrap around an input are rendered as before, at least in Chrome...)

* It checks the width of all inputs and checks to see whether their width is 100% of the containing element. If so, it attaches a class of "full-width" to both the input and its label.

* It finds each input of type text, email, or password and each textarea and adds a "placeholder" class to input and the matching label. ("Matching" labels can either use the "for" attribute or can wrap around the input.)

* It finds each textarea and adds a "textarea-label" class to its matching label.

* Each time focus is on one of the placeholder inputs a "busy" class is added.

* Each time focus moves away from one of the placeholder inputs, the "busy" class is removed. Meanwhile, if there is text in the input, a "filled" class is added. If there is no content, the "filled" class is removed.
*/


jQuery(document).ready(function($) {
  // Find all the textareas
  var $textareas = $("form textarea");

  // Add a "textarea-label" class to each of those labels
  if (typeof $textareas == "object") {
    Object.keys($textareas).forEach( function(textarea) {
      if (textarea != "length" && textarea != "prevObject") {
        addTextarea($textareas[textarea]);
      }
    });
  } else if (typeof $textareas == "array") {
    $textareas.forEach(function(textarea) {
      addTextarea(textarea);
    });
  }

  // For each input...
  var $allInputs = $("form input, form textarea");

  // check if the label is before or after the input, and if it's full width. Add descriptive classes.
  if (typeof $allInputs == "object") {
    Object.keys($allInputs).forEach(function(input) {
      if (input != "length" && input != "prevObject") {
        checkIfLabelFirst($allInputs[input]);
        tagFullWidth($allInputs[input]);
      }
    });
  } else if (typeof $allInputs == "array") {
    $allInputs.forEach(function(input) {
      checkIfLabelFirst(input);
      tagFullWidth(input);
    });
  }

  // Find the inputs that need placeholder labels
  // We're having trouble with them in safari on an iphone 7, so I'm removing 'form textarea' from this variable...
  var $placeholderInputs = $(
    "form input[type='text'], form input[type='email'], form input[type='password'], form input[type='tel']"
  );

  // Add a "placeholder" class to each of those labels
  // Also assign height and top position
  if (typeof $placeholderInputs == "object") {
    Object.keys($placeholderInputs).forEach(function(input) {
      if (input != "length" && input != "prevObject") {
        addPlaceholder($placeholderInputs[input]);
        setLabelHeight($placeholderInputs[input]);
        setPlaceholderLabelPosition($placeholderInputs[input]);
      }
    });
  } else if (typeof $placeholderInputs == "array") {
    $placeholderInputs.forEach(function(input) {
      addPlaceholder(input);
      setLabelHeight(input);
      setPlaceholderLabelPosition(input);
    });
  }

  // These placeholders get cool effects on interaction
  $placeholderInputs
    .on("focus", addBusy)
    .on("blur", removeBusy)
    .on("blur change", toggleFilled);

  // Lastly, check each input one last time.
  // If an input is somehow prefilled, set its class
  // to filled
  // If an input has focus on pageload, set its class
  // to busy and its top to 0
  if (typeof $allInputs == "object") {
    Object.keys($allInputs).forEach(function(input) {
      if (input != "length" && input != "prevObject") {
        checkFilled($allInputs[input]);
        checkFocus($allInputs[input]);
      }
    });
  } else if (typeof $allInputs == "array") {
    $allInputs.forEach(function(input) {
      checkFilled(input);
      checkFocus(input);
    });
  }


  function checkFocus(target) {
    if (document.activeElement === target && document.hasFocus()) {
      var $label = getLabel(target);
      if ($label !== undefined) {
        if (typeof $label === "object" && $label.length > 0) {
          $label = $label[0];
        }
        $($label).addClass("busy");
        $label.style.top = 0 + 'px';
      }
    }
  }

  function addPlaceholder(input) {
    var $label = getLabel(input);
    if ($label !== undefined) {
      $(input).addClass("placeholder");
      $($label).addClass("placeholder");
    }
    var $parent = getParent(input);
    if ($parent !== undefined) {
      $($parent).addClass("placeholder");
    }
  }

  function setLabelHeight(input) {
    var $label = getLabel(input);
    if ($label !== undefined) {
      if (typeof $label === "object" && $label.length > 0) {
        $label = $label[0];
      }
      $label.style.height = $label.offsetHeight + 'px';
    }
  }

  function setPlaceholderLabelPosition(input) {
    // note that $inputBoxHeight should match the $textbox-height variable in _forms.scss
    var $inputBoxHeight = 53;
    // note that $lineHeight should match the $label-line-height variable in _forms.scss
    var $lineHeight = 1.5;
    // note that $fontSize should match the $lable-font-size variable in _forms.scss
    var $fontSize = 16;
    // note that $shrinkRatio should match the $label-shrink-ratio variable in _forms.scss
    var $shrinkRatio = 0.8;

    var $label = getLabel(input);
    if ($label !== undefined) {
      if (typeof $label === "object" && $label.length > 0) {
        $label = $label[0];
      }
      // This vertically centers all labels inside their inputs
      // if ($label.style !== undefined && $label.style.height !== undefined) {
      //   var $positionTop =
      //     (parseInt($label.style.height, 10) + input.offsetHeight) / 2;
      // } else {
      //   var $positionTop = ($label.offsetHeight + input.offsetHeight) / 2;
      // }
      // if ($label.classList.contains("label-before")) {
      //   $label.style.marginTop =
      //     -(1 - $shrinkRatio) * $label.offsetHeight + "px";
      // } else if ($label.classList.contains("label-after")) {
      //   $label.style.marginBottom =
      //     -(1 - $shrinkRatio) * $label.offsetHeight + "px";
      //   $positionTop *= -1;
      // } else {
      //   var $positionTop = 0;
      // }

      // This centers a single line label in a text input and puts it an equal amount down in a textarea
      if ($label.classList.contains("label-before")) {
        if ($label.style !== undefined && $label.style.height !== undefined) {
          var $positionTop =
            parseInt($label.style.height, 10) +
            ($inputBoxHeight - $lineHeight * $fontSize) / 2;
        } else {
          var $positionTop =
            $label.offsetHeight +
            ($inputBoxHeight - $lineHeight * $fontSize) / 2;
        }
        // $label.style.marginTop =
        //   -(1 - $shrinkRatio) * $label.offsetHeight + "px";
      } else if ($label.classList.contains("label-after")) {
        var $positionTop = -1 * input.offsetHeight + ($inputBoxHeight - $lineHeight * $fontSize) / 2;
        // $label.style.marginBottom =
        //   -(1 - $shrinkRatio) * $label.offsetHeight + "px";
      } else {
        var $positionTop = 0;
      }
      setLabelPosition($label, $positionTop);
    }
  }

  function setLabelPosition($label, $positionTop) {
    if ($label !== undefined) {
      if (typeof $label === "object" && $label.length > 0) {
        $label = $label[0];
      }
      $label.style.top = String($positionTop) + "px";
    }
  }

  function addTextarea(target) {
    var $label = getLabel(target);
    if ($label !== undefined) {
      $($label).addClass("textarea-label");
    }
  }

  function toggleFilled(event) {
    var $label = getLabel(event.target);
    if ($label !== undefined) {
      if (event.target.value) {
        $($label).addClass("filled");
        setLabelPosition($label, 0);
      } else {
        $($label).removeClass("filled");
        setPlaceholderLabelPosition(event.target);
      }
    }
  }

  function checkFilled(element) {
    var $label = getLabel(element);
    if ($label !== undefined) {
      if (element.value) {
        $($label).addClass("filled");
        setLabelPosition($label, 0);
      } else {
        $($label).removeClass("filled");
        setPlaceholderLabelPosition(element);
      }
    }
  }

  function addBusy(event) {
    var $label = getLabel(event.target);
    if ($label !== undefined) {
      $($label).addClass("busy");
      setLabelPosition($label, 0);
    }
  }

  function removeBusy(event) {
    var $label = getLabel(event.target);
    if ($label !== undefined) {
      $($label).removeClass("busy");
    }
  }

  function getLabel(target) {
    var $form = $(target).closest('form');
    var $id = target.id;
    if ($id !== undefined && $id !== "") {
      var $label = $form.find("label[for=" + $id + "]");
    }
    if ($label == undefined || $label.length <= 0) {
      var parentElem = $(target).parent();
      var parentTagName = parentElem.get(0).tagName.toLowerCase();

      if (parentTagName == "label") {
        $label = parentElem;
      }
    }

    if ($label == undefined || $label.length <= 0) {
      return undefined;
    } else {
      return $label;
    }
  }

  function getParent(target) {
    // we're looking for drupal webform parent divs
    return $(target).parent('.form-item');
  }

  function checkIfLabelFirst(target) {
    var $label = getLabel(target);

    var $parent = getParent(target);

    if ($label !== undefined) {
      if (typeof $label === "object" && $label.length > 0) {
        $label = $label[0];
      }
      if ($label !== undefined) {
        // remember that compareDocumentPosition is a bitwise operator that adds up the various possibilities (like file permissions). So you get 10 when it spits out both 8 and 2
        switch ($label.compareDocumentPosition(target)) {
          case 2: // input comes first
          case 8: // input contains label (???)
          case 10: // input contains label (yeah right!) & thus input comes first
            $(target).addClass("label-after");
            $($label).addClass("label-after");
            if ($parent !== undefined) {
              $($parent).addClass("label-after");
            }
            break;
          case 4: // label comes first
          case 16: // label contains input
          case 20: // label contains input & thus label is first
            $(target).addClass("label-before");
            $($label).addClass("label-before");
            if ($parent !== undefined) {
              $($parent).addClass("label-before");
            }
            break;
          default:
            console.log("I'm having trouble comparing the position of this input and its label:");
            console.log(target);
            console.log($label);
        }
      }
    }
  }

  function checkIfWidth100(target) {
    var parent = $(target).parent()[0];
    var padding =
      parseInt($(parent).css("padding-left")) +
      parseInt($(parent).css("padding-right"));
    var percent = (
      (target.offsetWidth / (parent.offsetWidth - padding)) *
      100
    ).toFixed(0);

    if (percent == 100) {
      return true;
    } else {
      return false;
    }
  }

  function tagFullWidth(target) {
    if (checkIfWidth100(target)) {
      $(target).addClass("full-width");
      var $label = getLabel(target);
      if ($label !== undefined) {
        $($label).addClass("full-width");
      }
    }
  }
});