├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── example_project ├── example_project │ ├── __init__.py │ ├── forms.py │ ├── settings.py │ ├── templates │ │ └── form.html │ ├── urls.py │ ├── views.py │ └── wsgi.py └── manage.py ├── input_mask ├── __init__.py ├── contrib │ ├── __init__.py │ └── localflavor │ │ ├── __init__.py │ │ ├── br │ │ ├── __init__.py │ │ ├── fields.py │ │ └── widgets.py │ │ └── us │ │ ├── __init__.py │ │ ├── fields.py │ │ └── widgets.py ├── fields.py ├── static │ └── input_mask │ │ └── js │ │ ├── jquery.maskMoney.min.js │ │ ├── jquery.maskedinput.min.js │ │ └── text_input_mask.js ├── tests.py ├── utils.py └── widgets.py ├── runtests.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── forms.py ├── test.py └── test_settings.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | build 3 | dist 4 | __pycache__ 5 | *~ 6 | *.pyc 7 | 8 | .venv 9 | .eggs 10 | .tox 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | sudo: false 4 | 5 | python: 6 | - "2.7" 7 | - "3.4" 8 | - "pypy" 9 | 10 | env: 11 | - TOX_ENV=lnx-Django17 12 | - TOX_ENV=lnx-Django18 13 | 14 | install: 15 | - pip install 'tox>=2' 16 | 17 | script: 18 | - tox 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Caio Ariede. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | 4 | recursive-include input_mask/static * 5 | recursive-include input_mask/contrib *.py 6 | recursive-include input_mask/ *.py 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django Input Mask 2 | ================= 3 | 4 | A collection of easy-to-extend-widgets for applying masks to input elements. 5 | 6 | **License:** MIT 7 | 8 | Status 9 | ------ 10 | 11 | [![Build Status](https://travis-ci.org/caioariede/django-input-mask.svg?branch=master)](https://travis-ci.org/caioariede/django-input-mask) 12 | 13 | Compatibility 14 | ------------- 15 | 16 | * Django 1.7 and 1.8 17 | * Python 2.7, 3.x and PyPy 18 | 19 | Requirements 20 | ------------ 21 | 22 | * jQuery 1.8 or higher 23 | 24 | We do not include jQuery in this package, you must add it by your hands. 25 | 26 | **Note:** 27 | *jQuery 1.9 support will be granted through the* `jQuery Migrate Plugin `_. 28 | 29 | *Just take a look on* `example_project/templates/form.html `_ *file for an example.* 30 | 31 | Installation 32 | --- 33 | 34 | ```bash 35 | pip install django-input-mask 36 | ``` 37 | 38 | Configuration 39 | --- 40 | 41 | Add ``input_mask`` to the ``INSTALLED_APPS`` setting. 42 | 43 | *This is needed so that Django can handle the app's static files* 44 | 45 | Usage 46 | --- 47 | 48 | ```python 49 | from django import forms 50 | from django.contrib.localflavor.br.forms import BRPhoneNumberField 51 | 52 | from input_mask.contrib.localflavor.br.widgets import BRPhoneNumberInput 53 | 54 | class YourForm(forms.ModelForm): 55 | phone = BRPhoneNumberField(widget=BRPhoneNumberInput) 56 | ``` 57 | 58 | **Decimal masks** 59 | 60 | ```python 61 | from input_mask.fields import DecimalField 62 | 63 | class MyForm(forms.ModelForm): 64 | my_decimal_field = DecimalField(max_digits=10, decimal_places=2) 65 | ``` 66 | * `input_mask.fields.DecimalField` will automatically handle separators. 67 | * `input_mask.contrib.localflavor.*.fields.*DecimalField` will use local-based separators. 68 | 69 | **Creating your own masks** 70 | 71 | ```python 72 | from input_mask.widgets import InputMask 73 | 74 | class MyCustomInput(InputMask): 75 | mask = {'mask': '999-111'} 76 | ``` 77 | 78 | For more rules, take a look at `meioMask documentation `_. 79 | -------------------------------------------------------------------------------- /example_project/example_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caioariede/django-input-mask/fc6c0faf52315e10f01acd2403e6f9e3a4129c23/example_project/example_project/__init__.py -------------------------------------------------------------------------------- /example_project/example_project/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from input_mask.fields import DecimalField 4 | 5 | 6 | class BasicForm(forms.Form): 7 | decimal_field = DecimalField(max_digits=10, decimal_places=2) 8 | -------------------------------------------------------------------------------- /example_project/example_project/settings.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os.path 3 | 4 | # Makes input_mask available 5 | sys.path.append(os.path.dirname(__file__) + '/../../') 6 | 7 | # Django settings for example_project project. 8 | 9 | DEBUG = True 10 | TEMPLATE_DEBUG = DEBUG 11 | 12 | ADMINS = ( 13 | # ('Your Name', 'your_email@example.com'), 14 | ) 15 | 16 | MANAGERS = ADMINS 17 | 18 | DATABASES = { 19 | 'default': { 20 | 'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 21 | 'NAME': '', # Or path to database file if using sqlite3. 22 | # The following settings are not used with sqlite3: 23 | 'USER': '', 24 | 'PASSWORD': '', 25 | 'HOST': '', # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP. 26 | 'PORT': '', # Set to empty string for default. 27 | } 28 | } 29 | 30 | # Hosts/domain names that are valid for this site; required if DEBUG is False 31 | # See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts 32 | ALLOWED_HOSTS = [] 33 | 34 | # Local time zone for this installation. Choices can be found here: 35 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 36 | # although not all choices may be available on all operating systems. 37 | # In a Windows environment this must be set to your system time zone. 38 | TIME_ZONE = 'America/Chicago' 39 | 40 | # Language code for this installation. All choices can be found here: 41 | # http://www.i18nguy.com/unicode/language-identifiers.html 42 | LANGUAGE_CODE = 'en-us' 43 | 44 | SITE_ID = 1 45 | 46 | # If you set this to False, Django will make some optimizations so as not 47 | # to load the internationalization machinery. 48 | USE_I18N = True 49 | 50 | # If you set this to False, Django will not format dates, numbers and 51 | # calendars according to the current locale. 52 | USE_L10N = True 53 | 54 | # If you set this to False, Django will not use timezone-aware datetimes. 55 | USE_TZ = True 56 | 57 | # Absolute filesystem path to the directory that will hold user-uploaded files. 58 | # Example: "/var/www/example.com/media/" 59 | MEDIA_ROOT = '' 60 | 61 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 62 | # trailing slash. 63 | # Examples: "http://example.com/media/", "http://media.example.com/" 64 | MEDIA_URL = '' 65 | 66 | # Absolute path to the directory static files should be collected to. 67 | # Don't put anything in this directory yourself; store your static files 68 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 69 | # Example: "/var/www/example.com/static/" 70 | STATIC_ROOT = '' 71 | 72 | # URL prefix for static files. 73 | # Example: "http://example.com/static/", "http://static.example.com/" 74 | STATIC_URL = '/static/' 75 | 76 | # Additional locations of static files 77 | STATICFILES_DIRS = ( 78 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 79 | # Always use forward slashes, even on Windows. 80 | # Don't forget to use absolute paths, not relative paths. 81 | ) 82 | 83 | # List of finder classes that know how to find static files in 84 | # various locations. 85 | STATICFILES_FINDERS = ( 86 | 'django.contrib.staticfiles.finders.FileSystemFinder', 87 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 88 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 89 | ) 90 | 91 | # Make this unique, and don't share it with anybody. 92 | SECRET_KEY = '-r(ysbp$9(!%9-7z6y3oh8&4i7e6)p5xbrks#$4-d=^m^v)^!-' 93 | 94 | # List of callables that know how to import templates from various sources. 95 | TEMPLATE_LOADERS = ( 96 | 'django.template.loaders.filesystem.Loader', 97 | 'django.template.loaders.app_directories.Loader', 98 | # 'django.template.loaders.eggs.Loader', 99 | ) 100 | 101 | MIDDLEWARE_CLASSES = ( 102 | 'django.middleware.common.CommonMiddleware', 103 | 'django.contrib.sessions.middleware.SessionMiddleware', 104 | 'django.middleware.csrf.CsrfViewMiddleware', 105 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 106 | 'django.contrib.messages.middleware.MessageMiddleware', 107 | # Uncomment the next line for simple clickjacking protection: 108 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 109 | ) 110 | 111 | ROOT_URLCONF = 'example_project.urls' 112 | 113 | # Python dotted path to the WSGI application used by Django's runserver. 114 | WSGI_APPLICATION = 'example_project.wsgi.application' 115 | 116 | TEMPLATE_DIRS = ( 117 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 118 | # Always use forward slashes, even on Windows. 119 | # Don't forget to use absolute paths, not relative paths. 120 | ) 121 | 122 | INSTALLED_APPS = ( 123 | 'django.contrib.auth', 124 | 'django.contrib.contenttypes', 125 | 'django.contrib.sessions', 126 | 'django.contrib.sites', 127 | 'django.contrib.messages', 128 | 'django.contrib.staticfiles', 129 | # Uncomment the next line to enable the admin: 130 | # 'django.contrib.admin', 131 | # Uncomment the next line to enable admin documentation: 132 | # 'django.contrib.admindocs', 133 | 134 | 'example_project', 135 | 'input_mask', 136 | ) 137 | 138 | # A sample logging configuration. The only tangible logging 139 | # performed by this configuration is to send an email to 140 | # the site admins on every HTTP 500 error when DEBUG=False. 141 | # See http://docs.djangoproject.com/en/dev/topics/logging for 142 | # more details on how to customize your logging configuration. 143 | LOGGING = { 144 | 'version': 1, 145 | 'disable_existing_loggers': False, 146 | 'filters': { 147 | 'require_debug_false': { 148 | '()': 'django.utils.log.RequireDebugFalse' 149 | } 150 | }, 151 | 'handlers': { 152 | 'mail_admins': { 153 | 'level': 'ERROR', 154 | 'filters': ['require_debug_false'], 155 | 'class': 'django.utils.log.AdminEmailHandler' 156 | } 157 | }, 158 | 'loggers': { 159 | 'django.request': { 160 | 'handlers': ['mail_admins'], 161 | 'level': 'ERROR', 162 | 'propagate': True, 163 | }, 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /example_project/example_project/templates/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ form.media }} 7 | 8 | 9 |
10 | {% csrf_token %} 11 | 12 | {{ form }} 13 | 14 | 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /example_project/example_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | 3 | from .views import BasicFormView 4 | 5 | # Uncomment the next two lines to enable the admin: 6 | # from django.contrib import admin 7 | # admin.autodiscover() 8 | 9 | urlpatterns = patterns( 10 | '', 11 | # Examples: 12 | # url(r'^$', 'example_project.views.home', name='home'), 13 | # url(r'^example_project/', include('example_project.foo.urls')), 14 | 15 | # Uncomment the admin/doc line below to enable admin documentation: 16 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 17 | 18 | # Uncomment the next line to enable the admin: 19 | # url(r'^admin/', include(admin.site.urls)), 20 | 21 | url(r'^$', BasicFormView.as_view(), name='form'), 22 | ) 23 | -------------------------------------------------------------------------------- /example_project/example_project/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import FormView 2 | from django.core.urlresolvers import reverse 3 | 4 | from .forms import BasicForm 5 | 6 | 7 | class BasicFormView(FormView): 8 | template_name = 'form.html' 9 | form_class = BasicForm 10 | 11 | def get_success_url(self): 12 | return reverse('form') + '?success=1' 13 | -------------------------------------------------------------------------------- /example_project/example_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example_project project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 19 | # if running multiple sites in the same mod_wsgi process. To fix this, use 20 | # mod_wsgi daemon mode with each site in its own daemon process, or use 21 | # os.environ["DJANGO_SETTINGS_MODULE"] = "example_project.settings" 22 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings") 23 | 24 | # This application object is used by any WSGI server configured to use this 25 | # file. This includes Django's development server, if the WSGI_APPLICATION 26 | # setting points here. 27 | from django.core.wsgi import get_wsgi_application 28 | application = get_wsgi_application() 29 | 30 | # Apply WSGI middleware here. 31 | # from helloworld.wsgi import HelloWorldApplication 32 | # application = HelloWorldApplication(application) 33 | -------------------------------------------------------------------------------- /example_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /input_mask/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.0.2' 2 | -------------------------------------------------------------------------------- /input_mask/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caioariede/django-input-mask/fc6c0faf52315e10f01acd2403e6f9e3a4129c23/input_mask/contrib/__init__.py -------------------------------------------------------------------------------- /input_mask/contrib/localflavor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caioariede/django-input-mask/fc6c0faf52315e10f01acd2403e6f9e3a4129c23/input_mask/contrib/localflavor/__init__.py -------------------------------------------------------------------------------- /input_mask/contrib/localflavor/br/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caioariede/django-input-mask/fc6c0faf52315e10f01acd2403e6f9e3a4129c23/input_mask/contrib/localflavor/br/__init__.py -------------------------------------------------------------------------------- /input_mask/contrib/localflavor/br/fields.py: -------------------------------------------------------------------------------- 1 | from django.forms import ValidationError 2 | 3 | from ....fields import DecimalField 4 | from .widgets import BRDecimalInput 5 | 6 | from decimal import Decimal, DecimalException 7 | 8 | 9 | class BRDecimalField(DecimalField): 10 | widget = BRDecimalInput 11 | 12 | def to_python(self, value): 13 | value = value.replace(',', '.') 14 | value = value.replace('.', '', value.count('.') - 1) 15 | 16 | try: 17 | value = Decimal(value) 18 | except DecimalException: 19 | raise ValidationError(self.error_messages['invalid']) 20 | -------------------------------------------------------------------------------- /input_mask/contrib/localflavor/br/widgets.py: -------------------------------------------------------------------------------- 1 | from ....widgets import * 2 | 3 | 4 | class BRPhoneNumberInput(InputMask): 5 | mask = { 6 | 'mask': '(99) 999999999', 7 | } 8 | 9 | 10 | class BRZipCodeInput(InputMask): 11 | mask = { 12 | 'mask': '99999-999', 13 | } 14 | 15 | 16 | class BRCPFInput(InputMask): 17 | mask = { 18 | 'mask': '999.999.999-99', 19 | } 20 | 21 | 22 | class BRCNPJInput(InputMask): 23 | mask = { 24 | 'mask': '99.999.999/9999-99', 25 | } 26 | 27 | 28 | class BRDecimalInput(DecimalInputMask): 29 | thousands_sep = '.' 30 | decimal_sep = ',' 31 | -------------------------------------------------------------------------------- /input_mask/contrib/localflavor/us/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caioariede/django-input-mask/fc6c0faf52315e10f01acd2403e6f9e3a4129c23/input_mask/contrib/localflavor/us/__init__.py -------------------------------------------------------------------------------- /input_mask/contrib/localflavor/us/fields.py: -------------------------------------------------------------------------------- 1 | from ....fields import DecimalField 2 | from .widgets import USDecimalInput 3 | 4 | from decimal import Decimal 5 | 6 | 7 | class USDecimalField(DecimalField): 8 | widget = USDecimalInput 9 | 10 | def to_python(self, value): 11 | return Decimal(value) 12 | -------------------------------------------------------------------------------- /input_mask/contrib/localflavor/us/widgets.py: -------------------------------------------------------------------------------- 1 | from ....widgets import * 2 | 3 | 4 | class USPhoneNumberInput(InputMask): 5 | mask = { 6 | 'mask': '999-999-9999', 7 | } 8 | 9 | 10 | class USSocialSecurityNumberInput(InputMask): 11 | mask = { 12 | 'mask': '999-99-9999', 13 | } 14 | 15 | 16 | class USZipCodeInput(InputMask): 17 | mask = { 18 | 'mask': '99999-9999', 19 | } 20 | 21 | 22 | class USDecimalInput(DecimalInputMask): 23 | thousands_sep = ',' 24 | decimal_sep = '.' 25 | -------------------------------------------------------------------------------- /input_mask/fields.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django import forms 3 | 4 | from .utils import money_mask 5 | 6 | 7 | class DecimalField(forms.DecimalField): 8 | def __init__(self, max_digits=10, decimal_places=2, *args, **kwargs): 9 | mask = kwargs.pop('mask', {}) 10 | self.widget = money_mask(forms.TextInput, mask=mask) 11 | 12 | super(DecimalField, self).__init__(*args, **kwargs) 13 | 14 | self.widget.max_digits = max_digits 15 | self.widget.decimal_places = decimal_places 16 | 17 | self.localize = True 18 | 19 | def to_python(self, value): 20 | old_settings = settings.USE_L10N, settings.USE_THOUSAND_SEPARATOR 21 | 22 | settings.USE_L10N = True 23 | settings.USE_THOUSAND_SEPARATOR = True 24 | 25 | result = super(DecimalField, self).to_python(value) 26 | 27 | # restore original values 28 | settings.USE_L10N, settings.USE_THOUSAND_SEPARATOR = old_settings 29 | 30 | return result 31 | 32 | 33 | class MoneyField(DecimalField): 34 | pass 35 | -------------------------------------------------------------------------------- /input_mask/static/input_mask/js/jquery.maskMoney.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jquery-maskMoney - v3.0.0 3 | * jQuery plugin to mask data entry in the input text in the form of money (currency) 4 | * https://github.com/plentz/jquery-maskmoney 5 | * 6 | * Made by Diego Plentz 7 | * Under MIT License (https://raw.github.com/plentz/jquery-maskmoney/master/LICENSE) 8 | */ 9 | !function($){"use strict";$.browser||($.browser={},$.browser.mozilla=/mozilla/.test(navigator.userAgent.toLowerCase())&&!/webkit/.test(navigator.userAgent.toLowerCase()),$.browser.webkit=/webkit/.test(navigator.userAgent.toLowerCase()),$.browser.opera=/opera/.test(navigator.userAgent.toLowerCase()),$.browser.msie=/msie/.test(navigator.userAgent.toLowerCase()));var a={destroy:function(){return $(this).unbind(".maskMoney"),$.browser.msie&&(this.onpaste=null),this},mask:function(a){return this.each(function(){var b,c=$(this);return"number"==typeof a&&(c.trigger("mask"),b=$(c.val().split(/\D/)).last()[0].length,a=a.toFixed(b),c.val(a)),c.trigger("mask")})},unmasked:function(){return this.map(function(){var a,b=$(this).val()||"0",c=-1!==b.indexOf("-");return $(b.split(/\D/).reverse()).each(function(b,c){return c?(a=c,!1):void 0}),b=b.replace(/\D/g,""),b=b.replace(new RegExp(a+"$"),"."+a),c&&(b="-"+b),parseFloat(b)})},init:function(a){return a=$.extend({prefix:"",suffix:"",affixesStay:!0,thousands:",",decimal:".",precision:2,allowZero:!1,allowNegative:!1},a),this.each(function(){function b(){var a,b,c,d,e,f=r.get(0),g=0,h=0;return"number"==typeof f.selectionStart&&"number"==typeof f.selectionEnd?(g=f.selectionStart,h=f.selectionEnd):(b=document.selection.createRange(),b&&b.parentElement()===f&&(d=f.value.length,a=f.value.replace(/\r\n/g,"\n"),c=f.createTextRange(),c.moveToBookmark(b.getBookmark()),e=f.createTextRange(),e.collapse(!1),c.compareEndPoints("StartToEnd",e)>-1?g=h=d:(g=-c.moveStart("character",-d),g+=a.slice(0,g).split("\n").length-1,c.compareEndPoints("EndToEnd",e)>-1?h=d:(h=-c.moveEnd("character",-d),h+=a.slice(0,h).split("\n").length-1)))),{start:g,end:h}}function c(){var a=!(r.val().length>=r.attr("maxlength")&&r.attr("maxlength")>=0),c=b(),d=c.start,e=c.end,f=c.start!==c.end&&r.val().substring(d,e).match(/\d/)?!0:!1,g="0"===r.val().substring(0,1);return a||f||g}function d(a){r.each(function(b,c){if(c.setSelectionRange)c.focus(),c.setSelectionRange(a,a);else if(c.createTextRange){var d=c.createTextRange();d.collapse(!0),d.moveEnd("character",a),d.moveStart("character",a),d.select()}})}function e(b){var c="";return b.indexOf("-")>-1&&(b=b.replace("-",""),c="-"),c+a.prefix+b+a.suffix}function f(b){var c,d,f,g=b.indexOf("-")>-1?"-":"",h=b.replace(/[^0-9]/g,""),i=h.slice(0,h.length-a.precision);return i=i.replace(/^0/g,""),i=i.replace(/\B(?=(\d{3})+(?!\d))/g,a.thousands),""===i&&(i="0"),c=g+i,a.precision>0&&(d=h.slice(h.length-a.precision),f=new Array(a.precision+1-d.length).join(0),c+=a.decimal+f+d),e(c)}function g(a){var b,c=r.val().length;r.val(f(r.val())),b=r.val().length,a-=c-b,d(a)}function h(){var a=r.val();r.val(f(a))}function i(){var b=r.val();return a.allowNegative?""!==b&&"-"===b.charAt(0)?b.replace("-",""):"-"+b:b}function j(a){a.preventDefault?a.preventDefault():a.returnValue=!1}function k(a){a=a||window.event;var d,e,f,h,k,l=a.which||a.charCode||a.keyCode;return void 0===l?!1:48>l||l>57?45===l?(r.val(i()),!1):43===l?(r.val(r.val().replace("-","")),!1):13===l||9===l?!0:!$.browser.mozilla||37!==l&&39!==l||0!==a.charCode?(j(a),!0):!0:c()?(j(a),d=String.fromCharCode(l),e=b(),f=e.start,h=e.end,k=r.val(),r.val(k.substring(0,f)+d+k.substring(h,k.length)),g(f+1),!1):!1}function l(c){c=c||window.event;var d,e,f,h,i,k=c.which||c.charCode||c.keyCode;return void 0===k?!1:(d=b(),e=d.start,f=d.end,8===k||46===k||63272===k?(j(c),h=r.val(),e===f&&(8===k?""===a.suffix?e-=1:(i=h.split("").reverse().join("").search(/\d/),e=h.length-i-1,f=e+1):f+=1),r.val(h.substring(0,e)+h.substring(f,h.length)),g(e),!1):9===k?!0:!0)}function m(){q=r.val(),h();var a,b=r.get(0);b.createTextRange&&(a=b.createTextRange(),a.collapse(!1),a.select())}function n(){var b=parseFloat("0")/Math.pow(10,a.precision);return b.toFixed(a.precision).replace(new RegExp("\\.","g"),a.decimal)}function o(b){if($.browser.msie&&k(b),""===r.val()||r.val()===e(n()))a.allowZero?a.affixesStay?r.val(e(n())):r.val(n()):r.val("");else if(!a.affixesStay){var c=r.val().replace(a.prefix,"").replace(a.suffix,"");r.val(c)}r.val()!==q&&r.change()}function p(){var a,b=r.get(0);b.setSelectionRange?(a=r.val().length,b.setSelectionRange(a,a)):r.val(r.val())}var q,r=$(this);a=$.extend(a,r.data()),r.unbind(".maskMoney"),r.bind("keypress.maskMoney",k),r.bind("keydown.maskMoney",l),r.bind("blur.maskMoney",o),r.bind("focus.maskMoney",m),r.bind("click.maskMoney",p),r.bind("mask.maskMoney",h)})}};$.fn.maskMoney=function(b){return a[b]?a[b].apply(this,Array.prototype.slice.call(arguments,1)):"object"!=typeof b&&b?($.error("Method "+b+" does not exist on jQuery.maskMoney"),void 0):a.init.apply(this,arguments)}}(window.jQuery||window.Zepto); -------------------------------------------------------------------------------- /input_mask/static/input_mask/js/jquery.maskedinput.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | jQuery Masked Input Plugin 3 | Copyright (c) 2007 - 2015 Josh Bush (digitalbush.com) 4 | Licensed under the MIT license (http://digitalbush.com/projects/masked-input-plugin/#license) 5 | Version: 1.4.1 6 | */ 7 | !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a){var b,c=navigator.userAgent,d=/iphone/i.test(c),e=/chrome/i.test(c),f=/android/i.test(c);a.mask={definitions:{9:"[0-9]",a:"[A-Za-z]","*":"[A-Za-z0-9]"},autoclear:!0,dataName:"rawMaskFn",placeholder:"_"},a.fn.extend({caret:function(a,b){var c;if(0!==this.length&&!this.is(":hidden"))return"number"==typeof a?(b="number"==typeof b?b:a,this.each(function(){this.setSelectionRange?this.setSelectionRange(a,b):this.createTextRange&&(c=this.createTextRange(),c.collapse(!0),c.moveEnd("character",b),c.moveStart("character",a),c.select())})):(this[0].setSelectionRange?(a=this[0].selectionStart,b=this[0].selectionEnd):document.selection&&document.selection.createRange&&(c=document.selection.createRange(),a=0-c.duplicate().moveStart("character",-1e5),b=a+c.text.length),{begin:a,end:b})},unmask:function(){return this.trigger("unmask")},mask:function(c,g){var h,i,j,k,l,m,n,o;if(!c&&this.length>0){h=a(this[0]);var p=h.data(a.mask.dataName);return p?p():void 0}return g=a.extend({autoclear:a.mask.autoclear,placeholder:a.mask.placeholder,completed:null},g),i=a.mask.definitions,j=[],k=n=c.length,l=null,a.each(c.split(""),function(a,b){"?"==b?(n--,k=a):i[b]?(j.push(new RegExp(i[b])),null===l&&(l=j.length-1),k>a&&(m=j.length-1)):j.push(null)}),this.trigger("unmask").each(function(){function h(){if(g.completed){for(var a=l;m>=a;a++)if(j[a]&&C[a]===p(a))return;g.completed.call(B)}}function p(a){return g.placeholder.charAt(a=0&&!j[a];);return a}function s(a,b){var c,d;if(!(0>a)){for(c=a,d=q(b);n>c;c++)if(j[c]){if(!(n>d&&j[c].test(C[d])))break;C[c]=C[d],C[d]=p(d),d=q(d)}z(),B.caret(Math.max(l,a))}}function t(a){var b,c,d,e;for(b=a,c=p(a);n>b;b++)if(j[b]){if(d=q(b),e=C[b],C[b]=c,!(n>d&&j[d].test(e)))break;c=e}}function u(){var a=B.val(),b=B.caret();if(o&&o.length&&o.length>a.length){for(A(!0);b.begin>0&&!j[b.begin-1];)b.begin--;if(0===b.begin)for(;b.beging)&&g&&13!==g){if(i.end-i.begin!==0&&(y(i.begin,i.end),s(i.begin,i.end-1)),c=q(i.begin-1),n>c&&(d=String.fromCharCode(g),j[c].test(d))){if(t(c),C[c]=d,z(),e=q(c),f){var k=function(){a.proxy(a.fn.caret,B,e)()};setTimeout(k,0)}else B.caret(e);i.begin<=m&&h()}b.preventDefault()}}}function y(a,b){var c;for(c=a;b>c&&n>c;c++)j[c]&&(C[c]=p(c))}function z(){B.val(C.join(""))}function A(a){var b,c,d,e=B.val(),f=-1;for(b=0,d=0;n>b;b++)if(j[b]){for(C[b]=p(b);d++e.length){y(b+1,n);break}}else C[b]===e.charAt(d)&&d++,k>b&&(f=b);return a?z():k>f+1?g.autoclear||C.join("")===D?(B.val()&&B.val(""),y(0,n)):z():(z(),B.val(B.val().substring(0,f+1))),k?b:l}var B=a(this),C=a.map(c.split(""),function(a,b){return"?"!=a?i[a]?p(b):a:void 0}),D=C.join(""),E=B.val();B.data(a.mask.dataName,function(){return a.map(C,function(a,b){return j[b]&&a!=p(b)?a:null}).join("")}),B.one("unmask",function(){B.off(".mask").removeData(a.mask.dataName)}).on("focus.mask",function(){if(!B.prop("readonly")){clearTimeout(b);var a;E=B.val(),a=A(),b=setTimeout(function(){B.get(0)===document.activeElement&&(z(),a==c.replace("?","").length?B.caret(0,a):B.caret(a))},10)}}).on("blur.mask",v).on("keydown.mask",w).on("keypress.mask",x).on("input.mask paste.mask",function(){B.prop("readonly")||setTimeout(function(){var a=A(!0);B.caret(a),h()},0)}),e&&f&&B.off("input.mask").on("input.mask",u),A()})}})}); -------------------------------------------------------------------------------- /input_mask/static/input_mask/js/text_input_mask.js: -------------------------------------------------------------------------------- 1 | // the semi-colon before function invocation is a safety net against concatenated 2 | // scripts and/or other plugins which may not be closed properly. 3 | ;( function( $, window, document, undefined ) { 4 | 5 | "use strict"; 6 | 7 | var pluginName = 'djangoInputMask'; 8 | 9 | function Plugin(container, options) { 10 | this.container = container; 11 | this.init(); 12 | } 13 | 14 | $.extend(Plugin.prototype, { 15 | init: function() { 16 | $(this.container).find('[data-input-mask]').each(this.initInputMask); 17 | $(this.container).find('[data-money-mask]').each(this.initMoneyMask); 18 | }, 19 | 20 | initInputMask: function() { 21 | var input = $(this), 22 | opts = input.attr('data-input-mask').replace(/"/g, '"'), 23 | opts = JSON.parse(opts), 24 | mask = opts.mask; 25 | 26 | opts.placeholder = input.attr('placeholder'); 27 | 28 | // maxlength causes a bug in jquery-maskedinput in android 29 | input.removeAttr('maxlength'); 30 | input.mask(mask, opts); 31 | }, 32 | 33 | initMoneyMask: function() { 34 | var input = $(this), 35 | opts = input.attr('data-money-mask').replace(/"/g, '"'), 36 | opts = JSON.parse(opts); 37 | 38 | input.maskMoney(opts); 39 | 40 | if (opts.allowZero || input.val() != '') { 41 | input.maskMoney('mask'); 42 | } 43 | 44 | if (input.is('[readonly]')) { 45 | input.maskMoney('destroy'); 46 | } 47 | } 48 | }); 49 | 50 | $.fn[pluginName] = function(options) { 51 | return this.each(function(){ 52 | new Plugin(this, options); 53 | }); 54 | }; 55 | 56 | } )( jQuery, window, document ); 57 | 58 | 59 | $(function(){ 60 | $('body').djangoInputMask(); 61 | }); 62 | -------------------------------------------------------------------------------- /input_mask/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | TODO: Write tests 3 | """ 4 | -------------------------------------------------------------------------------- /input_mask/utils.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | import json 4 | 5 | 6 | def encode_options(opts): 7 | return json.dumps(opts).replace('"', '"') 8 | 9 | 10 | def mask_cls(widget_cls): 11 | class InputMask(widget_cls): 12 | mask = {} 13 | 14 | def __init__(self, **kwargs): 15 | mask = kwargs.pop('mask', {}) 16 | 17 | if type(mask) != object: 18 | mask = {'mask': mask} 19 | 20 | super(InputMask, self).__init__(**kwargs) 21 | 22 | self.mask.update(mask) 23 | 24 | def render(self, name, value, attrs=None): 25 | attrs = attrs or {} 26 | attrs['data-input-mask'] = encode_options(self.mask) 27 | return super(InputMask, self).render(name, value, attrs=attrs) 28 | 29 | class Media: 30 | js = ( 31 | settings.STATIC_URL + 'input_mask/js/jquery.maskedinput.min.js', # noqa 32 | settings.STATIC_URL + 'input_mask/js/text_input_mask.js', 33 | ) 34 | 35 | return InputMask 36 | 37 | 38 | def mask(widget_cls, **kwargs): 39 | return mask_cls(widget_cls)(**kwargs) 40 | 41 | 42 | def money_mask_cls(widget_cls): 43 | class MoneyMask(widget_cls): 44 | mask = { 45 | 'allowZero': True, 46 | } 47 | 48 | def __init__(self, **kwargs): 49 | mask = kwargs.pop('mask', {}) 50 | 51 | super(MoneyMask, self).__init__(**kwargs) 52 | 53 | self.mask.update(mask) 54 | 55 | def render(self, name, value, attrs=None): 56 | attrs = attrs or {} 57 | attrs['data-money-mask'] = encode_options(self.mask) 58 | return super(MoneyMask, self).render(name, value, attrs=attrs) 59 | 60 | class Media: 61 | js = ( 62 | settings.STATIC_URL + 'input_mask/js/jquery.maskMoney.min.js', 63 | settings.STATIC_URL + 'input_mask/js/text_input_mask.js', 64 | ) 65 | 66 | return MoneyMask 67 | 68 | 69 | def money_mask(widget_cls, **kwargs): 70 | return money_mask_cls(widget_cls)(**kwargs) 71 | 72 | 73 | def decimal_mask(widget_cls, **kwargs): 74 | mask = { 75 | 'thousands': '', 76 | 'allowNegative': True, 77 | } 78 | 79 | override_mask = kwargs.pop('mask', {}) 80 | mask.update(override_mask) 81 | 82 | return money_mask_cls(widget_cls)(mask=mask, **kwargs) 83 | -------------------------------------------------------------------------------- /input_mask/widgets.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .utils import mask_cls, money_mask_cls 4 | 5 | 6 | __all__ = ( 7 | 'InputMask', 8 | 'DecimalInputMask', 9 | ) 10 | 11 | 12 | InputMask = mask_cls(forms.TextInput) 13 | 14 | 15 | class DecimalInputMask(money_mask_cls(forms.TextInput)): 16 | mask = { 17 | 'thousands': '', 18 | 'allowNegative': True, 19 | } 20 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.test_settings' 6 | 7 | test_dir = os.path.dirname(__file__) 8 | sys.path.insert(0, test_dir) 9 | 10 | 11 | import django 12 | 13 | if django.VERSION[1] > 6: 14 | django.setup() 15 | 16 | 17 | def runtests(): 18 | from django.test.utils import get_runner 19 | from django.conf import settings 20 | 21 | TestRunner = get_runner(settings) 22 | test_runner = TestRunner(verbosity=1, interactive=True) 23 | 24 | failures = test_runner.run_tests(['tests']) 25 | 26 | sys.exit(bool(failures)) 27 | 28 | 29 | if __name__ == '__main__': 30 | runtests() 31 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license-file = LICENSE 3 | 4 | [wheel] 5 | universal = 1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | VERSION = __import__('input_mask').__version__ 5 | 6 | 7 | setup( 8 | name='django-input-mask', 9 | version=VERSION, 10 | url='http://github.com/caioariede/django-input-mask', 11 | author='Caio Ariede', 12 | author_email='caio.ariede@gmail.com', 13 | description='JavaScript input masks for Django', 14 | license='MIT', 15 | zip_safe=False, 16 | platforms=['any'], 17 | packages=find_packages(), 18 | package_data={'input_mask': [ 19 | 'static/input_mask/js/*', 20 | ]}, 21 | classifiers=[ 22 | "Development Status :: 5 - Production/Stable", 23 | "Environment :: Web Environment", 24 | "Intended Audience :: Developers", 25 | "License :: OSI Approved :: MIT License", 26 | "Natural Language :: English", 27 | "Operating System :: OS Independent", 28 | "Framework :: Django", 29 | "Programming Language :: Python", 30 | "Programming Language :: Python :: 3", 31 | "Programming Language :: Python :: 3.4", 32 | "Programming Language :: Python :: Implementation :: PyPy", 33 | ], 34 | include_package_data=True, 35 | install_requires=[ 36 | 'Django>=1.7,<1.9', 37 | 'six', 38 | ], 39 | test_suite="runtests.runtests", 40 | ) 41 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caioariede/django-input-mask/fc6c0faf52315e10f01acd2403e6f9e3a4129c23/tests/__init__.py -------------------------------------------------------------------------------- /tests/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from input_mask.fields import DecimalField 4 | 5 | 6 | class Form1(forms.Form): 7 | dec_field = DecimalField(max_digits=10, decimal_places=2) 8 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from decimal import Decimal 4 | from tests.forms import Form1 5 | 6 | 7 | class InputMaskTest(TestCase): 8 | def test_decimal_field(self): 9 | data = { 10 | 'dec_field': '1.98', 11 | } 12 | 13 | form = Form1(data=data) 14 | 15 | self.assertTrue(form.is_valid()) 16 | self.assertEqual(form.cleaned_data.get('dec_field'), Decimal('1.98')) 17 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | SECRET_KEY = 'fake-key' 5 | 6 | INSTALLED_APPS = [ 7 | "tests", 8 | ] 9 | 10 | DATABASES = { 11 | 'default': { 12 | 'ENGINE': 'django.db.backends.sqlite3', 13 | 'NAME': 'db.sqlite3', 14 | } 15 | } 16 | 17 | STATIC_URL = '/static/' 18 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = {py2,py3}-Django17,{py2,py3}-Django18 3 | platform = linux|darwin 4 | 5 | [testenv] 6 | commands = 7 | {envpython} setup.py test 8 | 9 | deps = 10 | {py2,py3}-Django17: Django==1.7 11 | {py2,py3}-Django18: Django==1.8 12 | --------------------------------------------------------------------------------