web-dev-qa-db-fra.com

Implémenter une entrée avec un masque

Je souhaite implémenter un masque pour un champ texte input qui accepte une date. La valeur masquée doit s'afficher directement dans la variable input.

Quelque chose comme ça:

<input type='text' value='____/__/__'>

J'ai écrit le masque comme une valeur dans cet exemple, mais mon intention est de permettre aux personnes d'écrire une date sans taper / ou - pour séparer les mois, les années et les jours. L'utilisateur doit pouvoir entrer des chiffres dans le champ affiché, tandis que le masque applique automatiquement le format lors de la saisie.

J'ai observé ce comportement sur d'autres sites, mais je n'ai aucune idée de son fonctionnement ni de la façon de le mettre en œuvre moi-même.

41

Les masques de saisie peuvent être implémentés en combinant les propriétés keyup et les propriétés HTMLInputElementvalue, selectionStart et selectionEnd. Voici une implémentation très simple qui fait ce que vous voulez. Ce n'est certainement pas parfait, mais fonctionne assez bien pour démontrer le principe:

Array.prototype.forEach.call(document.body.querySelectorAll("*[data-mask]"), applyDataMask);

function applyDataMask(field) {
    var mask = field.dataset.mask.split('');
    
    // For now, this just strips everything that's not a number
    function stripMask(maskedData) {
        function isDigit(char) {
            return /\d/.test(char);
        }
        return maskedData.split('').filter(isDigit);
    }
    
    // Replace `_` characters with characters from `data`
    function applyMask(data) {
        return mask.map(function(char) {
            if (char != '_') return char;
            if (data.length == 0) return char;
            return data.shift();
        }).join('')
    }
    
    function reapplyMask(data) {
        return applyMask(stripMask(data));
    }
    
    function changed() {   
        var oldStart = field.selectionStart;
        var oldEnd = field.selectionEnd;
        
        field.value = reapplyMask(field.value);
        
        field.selectionStart = oldStart;
        field.selectionEnd = oldEnd;
    }
    
    field.addEventListener('click', changed)
    field.addEventListener('keyup', changed)
}
ISO Date: <input type="text" value="____-__-__" data-mask="____-__-__"/><br/>
Telephone: <input type="text" value="(___) ___-____" data-mask="(___) ___-____"/><br/>

( Voir dans JSFiddle )

Il existe également un certain nombre de bibliothèques qui remplissent cette fonction. Quelques exemples incluent:

34
Ajedi32

Vous pouvez également réaliser cela en utilisant la méthode native de JavaScripts. C'est assez simple et ne nécessite aucune bibliothèque supplémentaire à importer.

<input type="text" name="date" placeholder="yyyy-mm-dd" onkeyup="
  var date = this.value;
  if (date.match(/^\d{4}$/) !== null) {
     this.value = date + '-';
  } else if (date.match(/^\d{4}\-\d{2}$/) !== null) {
     this.value = date + '-';
  }" maxlength="10">
10
shubhamkes

Vous pouvez également essayer mon implémentation, qui ne présente pas de retard après chaque pression sur une touche lorsque vous saisissez le contenu, et prend totalement en charge les opérations de retour arrière et de suppression.

Vous pouvez l'essayer en ligne: https://jsfiddle.net/qmyo6a1h/1/

    <html>
    <style>
    input{
      font-family:'monospace';
    }
    </style>
    <body>
      <input type="text" id="phone" placeholder="123-5678-1234" title="123-5678-1234" input-mask="___-____-____">
      <input type="button" onClick="showValue_phone()" value="Show Value" />
      <input type="text" id="console_phone" />
      <script>
        function InputMask(element) {
          var self = this;

          self.element = element;

          self.mask = element.attributes["input-mask"].nodeValue;

          self.inputBuffer = "";

          self.cursorPosition = 0;

          self.bufferCursorPosition = 0;

          self.dataLength = getDataLength();

          function getDataLength() {
            var ret = 0;

            for (var i = 0; i < self.mask.length; i++) {
              if (self.mask.charAt(i) == "_") {
                ret++;
              }
            }

            return ret;
          }

          self.keyEventHandler = function (obj) {
            obj.preventDefault();

            self.updateBuffer(obj);
            self.manageCursor(obj);
            self.render();
            self.moveCursor();
          }

          self.updateBufferPosition = function () {
            var selectionStart = self.element.selectionStart;
            self.bufferCursorPosition = self.displayPosToBufferPos(selectionStart);
            console.log("self.bufferCursorPosition==" + self.bufferCursorPosition);
          }

          self.onClick = function () {
            self.updateBufferPosition();
          }

          self.updateBuffer = function (obj) {
            if (obj.keyCode == 8) {
              self.inputBuffer = self.inputBuffer.substring(0, self.bufferCursorPosition - 1) + self.inputBuffer.substring(self.bufferCursorPosition);
            }
            else if (obj.keyCode == 46) {
              self.inputBuffer = self.inputBuffer.substring(0, self.bufferCursorPosition) + self.inputBuffer.substring(self.bufferCursorPosition + 1);
            }
            else if (obj.keyCode >= 37 && obj.keyCode <= 40) {
              //do nothing on cursor keys.
            }
            else {
              var selectionStart = self.element.selectionStart;
              var bufferCursorPosition = self.displayPosToBufferPos(selectionStart);
              self.inputBuffer = self.inputBuffer.substring(0, bufferCursorPosition) + String.fromCharCode(obj.which) + self.inputBuffer.substring(bufferCursorPosition);
              if (self.inputBuffer.length > self.dataLength) {
                self.inputBuffer = self.inputBuffer.substring(0, self.dataLength);
              }
            }
          }

          self.manageCursor = function (obj) {
            console.log(obj.keyCode);
            if (obj.keyCode == 8) {
              self.bufferCursorPosition--;
            }
            else if (obj.keyCode == 46) {
              //do nothing on delete key.
            }
            else if (obj.keyCode >= 37 && obj.keyCode <= 40) {
              if (obj.keyCode == 37) {
                self.bufferCursorPosition--;
              }
              else if (obj.keyCode == 39) {
                self.bufferCursorPosition++;
              }
            }
            else {
              var bufferCursorPosition = self.displayPosToBufferPos(self.element.selectionStart);
              self.bufferCursorPosition = bufferCursorPosition + 1;
            }
          }

          self.setCursorByBuffer = function (bufferCursorPosition) {
            var displayCursorPos = self.bufferPosToDisplayPos(bufferCursorPosition);
            self.element.setSelectionRange(displayCursorPos, displayCursorPos);
          }

          self.moveCursor = function () {
            self.setCursorByBuffer(self.bufferCursorPosition);
          }

          self.render = function () {
            var bufferCopy = self.inputBuffer;
            var ret = {
              muskifiedValue: ""
            };

            var lastChar = 0;

            for (var i = 0; i < self.mask.length; i++) {
              if (self.mask.charAt(i) == "_" &&
                bufferCopy) {
                ret.muskifiedValue += bufferCopy.charAt(0);
                bufferCopy = bufferCopy.substr(1);
                lastChar = i;
              }
              else {
                ret.muskifiedValue += self.mask.charAt(i);
              }
            }

            self.element.value = ret.muskifiedValue;

          }

          self.preceedingMaskCharCount = function (displayCursorPos) {
            var lastCharIndex = 0;
            var ret = 0;

            for (var i = 0; i < self.element.value.length; i++) {
              if (self.element.value.charAt(i) == "_"
                || i > displayCursorPos - 1) {
                lastCharIndex = i;
                break;
              }
            }

            if (self.mask.charAt(lastCharIndex - 1) != "_") {
              var i = lastCharIndex - 1;
              while (self.mask.charAt(i) != "_") {
                i--;
                if (i < 0) break;
                ret++;
              }
            }

            return ret;
          }

          self.leadingMaskCharCount = function (displayIndex) {
            var ret = 0;

            for (var i = displayIndex; i >= 0; i--) {
              if (i >= self.mask.length) {
                continue;
              }
              if (self.mask.charAt(i) != "_") {
                ret++;
              }
            }

            return ret;
          }

          self.bufferPosToDisplayPos = function (bufferIndex) {
            var offset = 0;
            var indexInBuffer = 0;

            for (var i = 0; i < self.mask.length; i++) {
              if (indexInBuffer > bufferIndex) {
                break;
              }

              if (self.mask.charAt(i) != "_") {
                offset++;
                continue;
              }

              indexInBuffer++;
            }
            var ret = bufferIndex + offset;

            return ret;
          }

          self.displayPosToBufferPos = function (displayIndex) {
            var offset = 0;
            var indexInBuffer = 0;

            for (var i = 0; i < self.mask.length && i <= displayIndex; i++) {
              if (indexInBuffer >= self.inputBuffer.length) {
                break;
              }

              if (self.mask.charAt(i) != "_") {
                offset++;
                continue;
              }

              indexInBuffer++;
            }

            return displayIndex - offset;
          }

          self.getValue = function () {
            return this.inputBuffer;
          }
          self.element.onkeypress = self.keyEventHandler;
          self.element.onclick = self.onClick;
        }

        function InputMaskManager() {
          var self = this;

          self.instances = {};

          self.add = function (id) {
            var elem = document.getElementById(id);
            var maskInstance = new InputMask(elem);
            self.instances[id] = maskInstance;
          }

          self.getValue = function (id) {
            return self.instances[id].getValue();
          }

          document.onkeydown = function (obj) {
            if (obj.target.attributes["input-mask"]) {
              if (obj.keyCode == 8 ||
                obj.keyCode == 46 ||
                (obj.keyCode >= 37 && obj.keyCode <= 40)) {

                if (obj.keyCode == 8 || obj.keyCode == 46) {
                  obj.preventDefault();
                }

                //needs to broadcast to all instances here:
                var keys = Object.keys(self.instances);
                for (var i = 0; i < keys.length; i++) {
                  if (self.instances[keys[i]].element.id == obj.target.id) {
                    self.instances[keys[i]].keyEventHandler(obj);
                  }
                }
              }
            }
          }
        }

        //Initialize an instance of InputMaskManager and
        //add masker instances by passing in the DOM ids
        //of each HTML counterpart.
        var maskMgr = new InputMaskManager();
        maskMgr.add("phone");

        function showValue_phone() {
          //-------------------------------------------------------__Value_Here_____
          document.getElementById("console_phone").value = maskMgr.getValue("phone");
        }
      </script>
    </body>

    </html>
4
Cecilk Cao

Une solution qui répond à l'événement input au lieu d'événements clés (comme keyup) donnera une expérience fluide (sans aucun tremblement) et fonctionne également lorsque des modifications sont apportées sans le clavier (menu contextuel, glisser de souris). , autre appareil ...).

Le code ci-dessous cherchera les éléments d'entrée qui ont à la fois un attribut placeholder et un attribut data-slots. Ce dernier doit définir le (s) caractère (s) dans l’espace réservé qui est conçu pour être un emplacement d’entrée, par exemple, "_". Un attribut data-accept facultatif peut être fourni avec une expression régulière qui définit les caractères autorisés dans un tel emplacement. La valeur par défaut est \d, c'est-à-dire des chiffres.

// This code empowers all input tags having a placeholder and data-slots attribute
document.addEventListener('DOMContentLoaded', () => {
    for (const el of document.querySelectorAll("[placeholder][data-slots]")) {
        const pattern = el.getAttribute("placeholder"),
            slots = new Set(el.dataset.slots || "_"),
            prev = (j => Array.from(pattern, (c,i) => slots.has(c)? j=i+1: j))(0),
            first = [...pattern].findIndex(c => slots.has(c)),
            accept = new RegExp(el.dataset.accept || "\\d", "g"),
            clean = input => {
                input = input.match(accept) || [];
                return Array.from(pattern, c =>
                    input[0] === c || slots.has(c) ? input.shift() || c : c
                );
            },
            format = () => {
                const [i, j] = [el.selectionStart, el.selectionEnd].map(i => {
                    i = clean(el.value.slice(0, i)).findIndex(c => slots.has(c));
                    return i<0? prev[prev.length-1]: back? prev[i-1] || first: i;
                });
                el.value = clean(el.value).join``;
                el.setSelectionRange(i, j);
                back = false;
            };
        let back = false;
        el.addEventListener("keydown", (e) => back = e.key === "Backspace");
        el.addEventListener("input", format);
        el.addEventListener("focus", format);
        el.addEventListener("blur", () => el.value === pattern && (el.value=""));
    }
});
[data-slots] { font-family: monospace }
<label>Date time: 
    <input placeholder="dd/mm/yyyy hh:mm" data-slots="dmyh">
</label><br>
<label>Telephone:
    <input placeholder="+1 (___) ___-____" data-slots="_">
</label><br>
<label>MAC Address:
    <input placeholder="XX:XX:XX:XX:XX:XX" data-slots="X" data-accept="[\dA-H]">
</label><br>
3
trincot

Ci-dessous, je décris ma méthode. J'ai défini event sur input dans input pour appeler la méthode Masking (), qui renverra une chaîne formatée de celle que nous avons insérée dans input.

Html:

<input name="phone" pattern="+373 __ ___ ___" class="masked" required>

JQ: Ici nous mettons l'événement sur l'entrée:

$('.masked').on('input', function () {
    var input = $(this);
    input.val(Masking(input.val(), input.attr('pattern')));
});

JS: Function, qui formatera chaîne par motif;

function Masking (value, pattern) {
var out = '';
var space = ' ';
var any = '_';

for (var i = 0, j = 0; j < value.length; i++, j++) {
    if (value[j] === pattern[i]) {
        out += value[j];
    }
    else if(pattern[i] === any && value[j] !== space) {
        out += value[j];
    }
    else if(pattern[i] === space && value[j] !== space) {
        out += space;
        j--;
    }
    else if(pattern[i] !== any && pattern[i] !== space) {
        out += pattern[i];
        j--;
    }
}

return out;
}
0
Dumitru Boaghi

J'ai écrit une solution similaire il y a quelque temps.
Bien sûr, il ne s’agit que d’une PoC et peut encore être amélioré.

Cette solution couvre les fonctionnalités suivantes:

  • Saisie de caractères transparente
  • Personnalisation du motif
  • Validation en direct lors de la frappe
  • Validation de la date complète (comprenant les jours corrects chaque mois et une considération d'année bissextile)
  • Erreurs descriptives, afin que l'utilisateur comprenne ce qui se passe lorsqu'il ne parvient pas à saisir un caractère
  • Corrige la position du curseur et empêche les sélections
  • Afficher un espace réservé si la valeur est vide
const pattern = "__/__/____";
const patternFreeChar = "_";
const validDate = [
  /^[0-3]$/,
  /^(0[1-9]|[12]\d|3[01])$/,
  /^(0[1-9]|[12]\d|3[01])[01]$/,
  /^((0[1-9]|[12]\d|3[01])(0[13578]|1[02])|(0[1-9]|[12]\d|30)(0[469]|11)|(0[1-9]|[12]\d)02)$/,
  /^((0[1-9]|[12]\d|3[01])(0[13578]|1[02])|(0[1-9]|[12]\d|30)(0[469]|11)|(0[1-9]|[12]\d)02)[12]$/,
  /^((0[1-9]|[12]\d|3[01])(0[13578]|1[02])|(0[1-9]|[12]\d|30)(0[469]|11)|(0[1-9]|[12]\d)02)(19|20)/
]

/**
 * Validate a date as your type.
 * @param {string} date The date in format DDMMYYYY as a string representation.
 * @throws {Error} When the date is invalid.
 */
function validateStartTypingDate(date) {
  if ( !date ) return "";
  
  date = date.substr(0, 8);
  
  if ( !/^\d+$/.test(date) )
        throw new Error("Please type numbers only");
  
        if ( !validDate[Math.min(date.length-1,validDate.length-1)].test(date) ) {
    let errMsg = "";
    switch ( date.length ) {
        case 1:
        throw new Error("Day in month can start only with 0, 1, 2 or 3");
        
        case 2:
        throw new Error("Day in month must be in a range between 01 and 31");
        
        case 3:
        throw new Error("Month can start only with 0 or 1");
        
        case 4: {
        const day = parseInt(date.substr(0,2));
        const month = parseInt(date.substr(2,2));
        const monthName = new Date(0,month-1).toLocaleString('en-us',{month:'long'});
        
        if ( month < 1 || month > 12 )
                throw new Error("Month number must be in a range between 01 and 12");
          
        if ( day > 30 && [4,6,9,11].includes(month) )
                throw new Error(`${monthName} have maximum 30 days`);
          
        if ( day > 29 && month === 2 )
                throw new Error(`${monthName} have maximum 29 days`);
        break; 
      }
         
      case 5:
      case 6:
        throw new Error("We support only years between 1900 and 2099, so the full year can start only with 19 or 20");
    }
  }
  
  if ( date.length === 8 ) {
        const day = parseInt(date.substr(0,2));
    const month = parseInt(date.substr(2,2));
    const year = parseInt(date.substr(4,4));
    const monthName = new Date(0,month-1).toLocaleString('en-us',{month:'long'});
    if ( !isLeap(year) && month === 2 && day === 29 )
      throw new Error(`The year you are trying to enter (${year}) is not a leap year. Thus, in this year, ${monthName} can have maximum 28 days`);
  }
  
  return date;
}

/**
 * Check whether the given year is a leap year.
 */
function isLeap(year) {
  return new Date(year, 1, 29).getDate() === 29;
}

/**
 * Move cursor to the end of the provided input element.
 */
function moveCursorToEnd(el) {
        if (typeof el.selectionStart == "number") {
                el.selectionStart = el.selectionEnd = el.value.length;
        } else if (typeof el.createTextRange != "undefined") {
                el.focus();
                var range = el.createTextRange();
                range.collapse(false);
                range.select();
        }
}

/**
 * Move cursor to the end of the self input element.
 */
function selfMoveCursorToEnd() {
        return moveCursorToEnd(this);
}

const input = document.querySelector("input")

input.addEventListener("keydown", function(event){
        event.preventDefault();
  document.getElementById("date-error-msg").innerText = "";
  
  // On digit pressed
  let inputMemory = this.dataset.inputMemory || "";
  
  if ( event.key.length === 1 ) {
    try {
      inputMemory = validateStartTypingDate(inputMemory + event.key);
    } catch (err) {
      document.getElementById("date-error-msg").innerText = err.message;
    }
  }
  
  // On backspace pressed
  if ( event.code === "Backspace" ) {
        inputMemory = inputMemory.slice(0, -1);
  }
  
  // Build an output using a pattern
  if ( this.dataset.inputMemory !== inputMemory ) {
        let output = pattern;
        for ( let i=0, digit; i<inputMemory.length, digit=inputMemory[i]; i++ ) {
        output = output.replace(patternFreeChar, digit);
    }
    this.dataset.inputMemory = inputMemory;
    this.value = output;
  }
  
  // Clean the value if the memory is empty
  if ( inputMemory === "" ) {
        this.value = "";
  }
}, false);

input.addEventListener('select', selfMoveCursorToEnd, false);
input.addEventListener('mousedown', selfMoveCursorToEnd, false);
input.addEventListener('mouseup', selfMoveCursorToEnd, false);
input.addEventListener('click', selfMoveCursorToEnd, false);
<input type="text" placeholder="DD/MM/YYYY" />
<div id="date-error-msg"></div>

Un lien vers jsfiddle: https://jsfiddle.net/d1xbpw8f/56/

Bonne chance!

0
Slavik Meltser
Array.prototype.forEach.call(document.body.querySelectorAll("*[data-mask]"), applyDataMask);

function applyDataMask(field) {
    var mask = field.dataset.mask.split('');

    // For now, this just strips everything that's not a number
    function stripMask(maskedData) {
        function isDigit(char) {
            return /\d/.test(char);
        }
        return maskedData.split('').filter(isDigit);
    }

    // Replace `_` characters with characters from `data`
    function applyMask(data) {
        return mask.map(function(char) {
            if (char != '_') return char;
            if (data.length == 0) return char;
            return data.shift();
        }).join('')
    }

    function reapplyMask(data) {
        return applyMask(stripMask(data));
    }

    function changed() {   
        var oldStart = field.selectionStart;
        var oldEnd = field.selectionEnd;

        field.value = reapplyMask(field.value);

        field.selectionStart = oldStart;
        field.selectionEnd = oldEnd;
    }

    field.addEventListener('click', changed)
    field.addEventListener('keyup', changed)
}
Date: <input type="text" value="__-__-____" data-mask="__-__-____"/><br/>
Telephone: <input type="text" value="(___) ___-____" data-mask="(___) ___-____"/><br/>
0
Vishnu Kant