github.com/pbberlin/go-pwa@v0.0.0-20220328105622-7c26e0ca1ab8/app-bucket/js/validation.js (about)

     1  /* 
     2      using a factory function
     3      https://isamatov.com/encapsulation-in-javascript-es6/
     4      first concept
     5  
     6  */
     7  function Validator(argForm) {
     8  
     9      this.PublicVar = "we dont want public vars"
    10  
    11      let form = argForm;
    12  
    13      // internet suggests three modes to accomplish suppression of builtin bubbles
    14      // see if() branches below
    15      let suppressBuiltinBubbles = 3;  
    16      
    17      let onFocusRemove          = false; // unexported parameter
    18      let onInputRemove          = true;
    19      let onInputShowAndRemove   = false;
    20      let lockFocus              = false;
    21  
    22  
    23      // default is showing custom popups for every faulty input
    24      // onlyOne changes this
    25      let onlyOne = true;
    26  
    27  
    28      // custom popups are sized and positioned relative 
    29      // to the input's parent or grandparent.
    30      // Parent or grandparent provide us with a reliable width 
    31      // that does not overflow the screen width;
    32      // unexported parameter
    33      let attachGrandparent = true;
    34  
    35  
    36      this.SetOnInputRemove = function(newVal) {
    37          onInputRemove = newVal;
    38      }
    39  
    40      this.SetOnInputShowAndRemove = function(newVal) {
    41          onInputShowAndRemove = newVal;
    42      }
    43  
    44      this.SetLockFocus = function(newVal) {
    45          lockFocus = newVal;
    46      }
    47  
    48      this.SetOnlySingleCustomPopup = function(newVal) {
    49          onlyOne = newVal;
    50      }
    51  
    52      // "exporting" the func
    53      //  keeping the internal version, because internal callers have another 'this'
    54      this.ShowCustomPopup = showPopup;
    55  
    56  
    57      // if any input elements are not clean yet 
    58      //   => dont show any compound errors
    59      this.IsCleanForm = function(event) {
    60  
    61          let frmLoc = null;
    62          if (event.target.tagName == "FORM") {
    63              frmLoc = event.target;
    64          } else {
    65              frmLoc = event.target.form;
    66          }
    67  
    68          try {
    69              if (!frmLoc.checkValidity()) {
    70                  return false;
    71              }
    72          } catch (error) {
    73              logFn("Exception: isCleanForm() was fired for non-form-element; event.target: ", event.target.tagName)
    74          }
    75          return true;
    76      }
    77  
    78  
    79      function hasPopup(el) {
    80          var elErrors = el.parentNode.querySelectorAll(":scope > .popup-invalid-anchor");
    81          if (attachGrandparent) {
    82              elErrors = el.parentNode.parentNode.querySelectorAll(":scope > .popup-invalid-anchor");
    83          }
    84          for (var i = 0; i < elErrors.length; i++) {
    85              // console.log(`found-a ${i + 1}of${elErrors.length} - oldID${oldChild.getAttribute('id')} `);
    86              return true;
    87          }
    88          return false;
    89      }
    90  
    91  
    92      // removing previous message from element el
    93      function clearPopup(el) {
    94          var elErrors = el.parentNode.querySelectorAll(":scope > .popup-invalid-anchor");
    95          if (attachGrandparent) {
    96              elErrors = el.parentNode.parentNode.querySelectorAll(":scope > .popup-invalid-anchor");
    97          }
    98          for (var i = 0; i < elErrors.length; i++) {
    99              var oldChild = elErrors[i].parentNode.removeChild(elErrors[i]);
   100              // console.log(`removed-a ${i + 1}of${elErrors.length} - oldID${oldChild.getAttribute('id')} `);
   101          }
   102      }
   103  
   104  
   105      // removing any previous custom messages
   106      function clearAllPopups() {
   107          var errorMessages = form.querySelectorAll(".popup-invalid-anchor");
   108          for (var i = 0; i < errorMessages.length; i++) {
   109              var oldChild = errorMessages[i].parentNode.removeChild(errorMessages[i]);
   110              // console.log(`removed-b ${i + 1}of${errorMessages.length} - oldID${oldChild.getAttribute('id')} `);
   111          }
   112      }
   113  
   114      // clearing and re-creating a custom message 
   115      // right-beside or -below DOM element el
   116      function showPopup(el, msg, overrideCheckValidity) {
   117  
   118          if (!el) {
   119              console.log("flagInvalid() el not defined - return ");
   120              return;
   121          }
   122  
   123          if (msg === undefined) {  // typeof msg == "undefined"
   124              msg = el.dataset.validation_msg
   125              if (msg === undefined) {
   126                  msg = el.validationMessage // not localized, too mathematical
   127              }
   128          }
   129  
   130          if (onlyOne) {
   131              clearAllPopups();
   132          } else {
   133              clearPopup(el);
   134          }
   135  
   136          if (!el.checkValidity() || overrideCheckValidity === true) {
   137              var parent = el.parentNode;
   138              if (attachGrandparent) {
   139                  parent = el.parentNode.parentNode;
   140              }
   141              // el.validationMessage is mathematical has is always in browser local
   142              parent.insertAdjacentHTML(
   143                  "beforeend",
   144                  `<div class='popup-invalid-anchor'  id='err-${el.getAttribute('name')}' >
   145                      <div class='popup-invalid-content'>
   146                      ${msg}
   147                      </div>
   148                  </div>`
   149              );
   150          }
   151  
   152      }
   153  
   154      // for onsubmit
   155      // for each invalid input element of a form
   156      // a custom popup message is displayed right-next or -below
   157      function onSubmitCustomPopupsForInvalids(event) {
   158  
   159          clearAllPopups();
   160  
   161          // insert new messages at the end of parent
   162          // `this` to select descendents of <form> - excluding invalid <form> itself 
   163          var invalidFields = this.querySelectorAll(":invalid");
   164          for (var i = 0; i < invalidFields.length; i++) {
   165              showPopup(invalidFields[i]);
   166              if (onlyOne) {
   167                  break;
   168              }
   169          }
   170  
   171          // focus first invalid field
   172          if (invalidFields.length > 0) {
   173              invalidFields[0].focus();
   174          }
   175  
   176          if (invalidFields.length > 0) {
   177              return false;
   178          }
   179          return true;
   180      }
   181  
   182  
   183  
   184  
   185  
   186      this.ValidateFormWithCustomPopups = function() {
   187  
   188  
   189          if (suppressBuiltinBubbles == 1) {
   190              // => form.submit() no longer works; only submit buttons clicks still effect a submission
   191              form.addEventListener(
   192                  "invalid",
   193                  function (event) {
   194                      console.log("form invalid: ", event.target.getAttribute("name"), " - default prevented");
   195                      event.preventDefault();
   196                  },
   197                  true
   198              );
   199          }
   200  
   201  
   202          if (suppressBuiltinBubbles == 2) {
   203              // => form.submit() no longer works; only submit buttons clicks still effect a submission
   204              var inputs = form.querySelectorAll("input[type=number]");
   205              for (var i = 0; i < inputs.length; i++) {
   206                  var inp = inputs[i];
   207                  var funcInv = function (event) {
   208                      console.log("input invalid: ", event.target.getAttribute("name"), " - default prevented");
   209                      event.preventDefault();
   210                  };
   211                  inp.addEventListener("invalid", funcInv, true);
   212              }
   213          }
   214  
   215  
   216          if (suppressBuiltinBubbles == 3) {
   217  
   218              // disable form validation
   219              // form.submit() validation disabled
   220              // form.submit() works and still goes through
   221              form.setAttribute("novalidate", true);
   222  
   223  
   224              // "re-enable" validation of inputs using an explicit event handler
   225              form.addEventListener(
   226                  "submit",
   227                  function (event) {
   228                      if (!this.checkValidity()) {
   229                          var name = event.target.getAttribute("name");
   230                          console.log(`prevented submitting form ${name}: invalid inputs`);
   231                          // emulating cancellation of form.submit() on invalid input
   232                          event.preventDefault();
   233                      }
   234                  },
   235                  true
   236              );
   237              // form submit now stalls on invalid
   238              // but without any bubbles nor any other messages
   239              console.log(`suppressBuiltinBubbles complete`);
   240          }
   241  
   242          // add custom popups
   243          form.addEventListener(
   244              "submit",
   245              onSubmitCustomPopupsForInvalids,
   246              true
   247          );
   248          console.log(`on submit: custom popups for invalids attached`);
   249  
   250  
   251      }
   252  
   253  
   254  
   255      // showing custom popups on form submit is too late?
   256      //   => show them on blur
   257      this.ShowPopupOnBlurOrInput = function() {
   258  
   259          // if we dont apply ValidateFormWithCustomPopups(),
   260          // then we still need this for standalone functionality
   261          form.setAttribute("novalidate", true);
   262  
   263          var funcReport = function (event) {
   264              // event.target.reportValidity();
   265              if (onInputRemove && event.type == "input") {
   266                  if (event.target.checkValidity()) {
   267                      clearPopup(event.target);
   268                  }
   269              } else {
   270                  showPopup(event.target);
   271              }
   272              var lgMsg = "blur";
   273              if (onInputShowAndRemove || onInputRemove) {
   274                  lgMsg = "blur+input";
   275              }
   276              console.log(`  ${lgMsg} inp.reportValidity() ${event.target.getAttribute('name')} ${event.target.checkValidity()}`);
   277              if (lockFocus) {
   278                  if (event.type == "blur") {
   279                      if (!event.target.checkValidity()) {
   280                          event.target.focus();
   281                          console.log(`  blur focus reclaimed ${event.target.getAttribute('name')}`);
   282                      }
   283                  }
   284              }
   285          };
   286  
   287          var inputs = form.querySelectorAll("input[type=number]");
   288          for (var i = 0; i < inputs.length; i++) {
   289              var inp = inputs[i];
   290              inp.addEventListener("blur", funcReport);  // blur does not bubble up
   291              if (onInputShowAndRemove || onInputRemove) {
   292                  inp.addEventListener("input", funcReport);
   293              }
   294  
   295              if (onFocusRemove) { // remove on entering input
   296                  var removeOnEntering = function (event) {
   297                      clearPopup(event.target);
   298                  };
   299                  inp.addEventListener("focus", removeOnEntering);    
   300              } else {
   301                  var flagOnEntry = function (event) {
   302                      if (!event.target.checkValidity()) {
   303                          // console.log(`  show popup on focus -  ${event.target.name}`)
   304                          showPopup(event.target);
   305                      }
   306                  };
   307                  inp.addEventListener("focus", flagOnEntry);
   308              }
   309              var lgMsg = "blur";
   310              if (onInputShowAndRemove || onInputRemove) {
   311                  lgMsg = "blur+input";
   312              }
   313  
   314              var logLen = 1
   315              if (i < logLen || i > (inputs.length - 1 - logLen)) {
   316                  console.log(`     ${lgMsg} handler added to ${inp.getAttribute('name')}`);
   317              }
   318              if (i == logLen) {
   319                  console.log(`          ...`);
   320              }
   321          }
   322      }
   323  
   324  
   325  
   326  }