HTML
<input type="text"> <br> <div data-char-input data-char-input-chars="15" data-char-input-gap="1" ></div> <br> <input type="text"> <br><br> <small>(Du kannst mit der Tab-Taste die Felder wechseln, du kannst auch Texte einfügen)</small>
SCSS
.fpcharinput { display: inline-block; white-space: nowrap; position: relative; background: orange; font-size: 16px; width: 100%; box-sizing: border-box; &.mono { font-family: monospace; background: #fff; } .fpchar { font-family: inherit; background: #fff; text-align: center; outline: 0; border: 0; font-size: inherit; padding: 5px 0; margin: 0; &[data-pasted], &[data-pasted]:focus { color: transparent; text-shadow: none; } &::selection { background: transparent; } &::-moz-selection { background: transparent; } &::-webkit-contacts-auto-fill-button { visibility: hidden; display: none !important; pointer-events: none; position: absolute; right: 0; } &:focus { background: rgba(#fff, .5); outline: none; color : transparent; text-shadow : 0 0 0 #000; } } .fpchartabcatch { opacity: 0; width: 1px; position: absolute; border: 0; padding: 0; margin: 0; } .fpcharmobile { width: 100%; height: 100%; font-family: inherit; background: transparent; outline: 0; border: 0; font-size: inherit; padding: 5px; margin: 0; } .fpchargap { display: inline-block; position: absolute; height: 100%; pointer-events: none; z-index: 1; background: orange; } }
Script
!function($) { var FpCharInput = function($element, options) { this.mobile = screen.orientation || screen.mozOrientation || screen.msOrientation || window.orientation; this.$e = $element.addClass('fpcharinput'); this.options = options || {}; var dataChars = this.$e.data('charInputChars'), dataCharWidth = this.$e.data('charInputCharWidth'), dataGap = this.$e.data('charInputGap'), dataFillGaps = this.$e.data('charInputFillGaps') !== undefined, dataName = this.$e.data('charInputName'); // optionen vom Element ermitteln if (dataChars) this.options.chars = dataChars; if (dataCharWidth) this.options.charWidth = dataCharWidth; if (dataGap) this.options.gap = dataGap; if (dataFillGaps) this.options.fillGaps = dataFillGaps; if (dataName) this.options.name = dataName; this.chars = this.options.chars || 10; this.charWidth = this.options.charWidth || false; this.gap = this.options.gap || 0; this.fillGaps = this.options.gap || 0; this.$chars = []; this.$afterChars; this.value = ''; if (this.options.name && !this.mobile) { this.$input = $('<input type="hidden" />').prop('name', this.options.name); this.$e.after(this.$input); } else { this.$input = false; } if (this.mobile) { this.$e.addClass('mono'); var $monoChar = $('<span style="display: inline-block; letter-spacing: 0; font-size: inherit; font-family: inherit; padding: 0;" />').text('i').appendTo(this.$e); this.monoCharWidth = $monoChar.width(); $monoChar.remove(); } this['build'+(this.mobile ? 'Mobile' : '')](); }; $.extend(FpCharInput.prototype, { constructor: FpCharInput, build: function() { var self = this, $char, inputEventName = 'oninput' in window ? 'input' : 'keyup', width, comboKeyChars = '^~´`¨'; if (this.charWidth) width = this.charWidth + 'px'; else width = 'calc(' + (100 / this.chars) + '% - ' + (this.gap + this.gap / this.chars) + 'px)'; // add invisible input field before the other inputs to catch tab-focus this.$tabCatch = $('<input type="text" class="fpchartabcatch" />').on('focus', function() { self.$chars[self.chars - 1].trigger('mousedown'); }); if (this.options.name) this.$tabCatch.prop('id', this.options.name); this.$e.append(this.$tabCatch); for (var i = 0; i <= this.chars; i++) { $char = $('<input class="fpchar" type="text" />') .attr('autocomplete', 'off') .attr('autocorrect', 'off') .attr('autocapitalize', 'off') .attr('spellcheck', 'false') .prop('tabindex', '-1'); // fill gaps with an element if (this.fillGaps) this.$e.append($('<div class="fpchargap" />').css({ width: this.gap + 'px' })); // hidden character after last visible character if (i == this.chars) { $char.css({ width: this.gap ? this.gap + 'px' : '1px', margin: 0, color: 'transparent', position: 'absolute', background: 'transparent' }).on(inputEventName, function() { $(this).val(''); }); // visible character input } else { if (this.options.name) $char.prop('id', '' + this.options.name + (i + 1)); $char.css({ width: width, marginLeft: this.gap + 'px' }).on('mousedown', function(e) { var select = false; if ($(this).val().length || !$(this).data('prevChar')) select = true; else if (!$(this).data('prevChar').val().length) $(this).data('prevChar').trigger('mousedown'); else select = true; if (select) $(this).select(); else e.preventDefault(); }).on(inputEventName, function(e, opt) { if ($(this).attr('data-pasted')) return; if (!opt) opt = {}; var lastVal = $(this).data('val'), val = $(this).val(), $prev = $(this).data('prevChar'), $next = $(this).data('nextChar'); // change focus if (val.length && !opt.skipFocus) { if (comboKeyChars.indexOf(val) == -1) { if ($next) $next.select(); else $(this).blur(); } } // spread out chars if (val.length > 1) { $(this).val(val[0]); if ($next) { var focusSet = false; for (var i = 1, l = val.length, $e = $next; i < l; i++) { $e.val(val[i]).removeAttr('data-pasted').trigger(inputEventName, { skipFocus: true }); if ($e.data('nextChar')) $e = $e.data('nextChar'); else { focusSet = true; $e.data('prevChar').focus(); break; } } if (!focusSet) { $e.focus(); } } } $(this).data('val', $(this).val()); self.trigger('input'); self.updateValue(); }); } $char.data('charIndex', i).data('prevChar', i ? this.$chars[i - 1] : false) .on('moveLeft', function(arg, text) { var $prev = $(this).data('prevChar'); if ($prev && $prev.val() == '') { $prev.trigger('moveLeft', text); } else { $(this).removeAttr('data-pasted').val(text).trigger(inputEventName); } }) .on('paste', function(e) { if (e.originalEvent.clipboardData) { var val = e.originalEvent.clipboardData.getData('Text'); $(this).trigger('moveLeft', val); e.preventDefault(); } else { // fallback, if clipboardData is not supported $(this).attr('data-pasted', 'true'); setTimeout(function($self) { var val = $self.val(); $self.val(''); setTimeout(function(val) { // clear async, smoother $self.trigger('moveLeft', val); }, 10, val); }, 10, $(this)); setTimeout(function() { $(this).removeAttr('data-pasted'); }, 100); } }) .on('keydown', function(e) { var key = e.originalEvent.keyCode, $prev = $(this).data('prevChar'), $next = $(this).data('nextChar'), prevent = false; if (e.originalEvent.shiftKey && key == 9) { self.$tabCatch.prop('tabindex', '-1'); setTimeout(function() { self.$tabCatch.prop('tabindex', ''); }, 10); } else self.$tabCatch.prop('tabindex', ''); // left if (key == 37) { if ($prev) $prev.select(); prevent = true; // right } else if (key == 39) { if ($next) $next.select(); prevent = true; // 229: keyCode after e.g. "tilde" + "space" //} else if (key == 229) { // space // } else if (key == 32) { // if (~comboKeyChars.indexOf($(this).val())) { // if ($next) $next.select(); // } // backspace } else if (key == 8) { if ($(this).val().length) { $(this).val(''); prevent = true; } if ($prev) $prev.select(); } if (prevent) e.preventDefault(); }); if (i == this.chars) this.$afterChars = $char; else this.$chars[i] = $char; if (i) this.$chars[i - 1].data('nextChar', $char); this.$e.append($char); } }, buildMobile: function() { var self = this, gapWidth = this.fillGaps ? this.gap : 0; this.$mobileInput = $('<input type="text" class="fpcharmobile" />') .attr('autocomplete', 'off') .attr('autocorrect', 'off') .attr('autocapitalize', 'off') .attr('spellcheck', 'false') .prop('maxlength', this.chars - 1); // -1, to prevent scrolling, #todo if (this.charWidth) { var space = this.charWidth - this.monoCharWidth, left = space / 2, letterSpacing = space + 'px'; this.$mobileInput.css({ letterSpacing: letterSpacing, paddingLeft: left }); } else { var setLetterSpacing = function() { var width = self.$e.width(), letterSpacing = (width / self.chars - self.monoCharWidth) + 'px'; self.$mobileInput.css('letter-spacing', letterSpacing); }, left = 'calc(' + (50 / this.chars) + '% - ' + (this.monoCharWidth / 2) + 'px)'; this.$mobileInput.css({ paddingLeft: left }); $(window).on('resize', setLetterSpacing); setTimeout(setLetterSpacing, 10); } if (this.options.name) this.$mobileInput.prop('id', this.options.name).prop('name', this.options.name); // fill gaps with an element if (this.fillGaps) { this.$mobileInput.css({ marginLeft: (gapWidth / 2) + 'px' }); this.$e.css({ marginLeft: (-gapWidth / 2) + 'px', marginRight: (-gapWidth / 2) + 'px' }); var $gap; for (var i = 0; i <= this.chars; i++) { $gap = $('<div class="fpchargap" />').css('width', this.gap + 'px'); if (i == this.chars) $gap.attr('data-mobile-last', 'true').css('right', '0px'); else $gap.css('left', (100 / this.chars * i)+'%'); this.$e.append($gap); } } this.$e.append(this.$mobileInput); }, updateValue: function() { if (this.mobile) { this.$mobileInput.val(self.value); } else { self.value = ''; if (this.$input) { for (var i = 0; i < this.chars; i++) { self.value += this.$chars[i].val(); } this.$input.val(self.value); } } }, trigger: function(event) { this.$e.trigger(event, self.value); } }); $.fn.fpcharinput = function(a, b, c) { return this.each(function() { var $this = $(this), $obj = $this.data('fpcharinput'), options = typeof a === 'object' && a; if (!$obj) { $this.data('fpcharinput', ($obj = new FpCharInput($this, a))); } }); }; }(window.jQuery); $(function() { $('[data-char-input]').fpcharinput(); });