update for compatibility ie11

This commit is contained in:
Emmanuel ROY 2021-06-16 11:35:03 +02:00
parent 037e2b0812
commit 58f6adb7ba
132 changed files with 26636 additions and 35 deletions

View File

@ -15,6 +15,104 @@ class Browser
//Logger::addLog('http.browser',$this->user);
}
public static function get()
{
// Make case insensitive.
$t = strtolower($_SERVER['HTTP_USER_AGENT']);
// If the string *starts* with the string, strpos returns 0 (i.e., FALSE). Do a ghetto hack and start with a space.
// "[strpos()] may return Boolean FALSE, but may also return a non-Boolean value which evaluates to FALSE."
// http://php.net/manual/en/function.strpos.php
$t = " " . $t;
// Humans / Regular Users
if (strpos($t, 'opera') || strpos($t, 'opr/')) {
return 'Opera';
} elseif (strpos($t, 'edge')) {
return 'Edge';
} elseif (strpos($t, 'chrome')) {
return 'Chrome';
} elseif (strpos($t, 'safari')) {
return 'Safari';
} elseif (strpos($t, 'firefox')) {
return 'Firefox';
} elseif (strpos($t, 'msie') || strpos($t, 'trident/7')) {
return 'Internet Explorer';
}
}
public static function get_firefox_version() {
// Make case insensitive.
$t = strtolower($_SERVER['HTTP_USER_AGENT']);
// If the string *starts* with the string, strpos returns 0 (i.e., FALSE). Do a ghetto hack and start with a space.
// "[strpos()] may return Boolean FALSE, but may also return a non-Boolean value which evaluates to FALSE."
// http://php.net/manual/en/function.strpos.php
$t = " " . $t;
// Firefox Users
if (strpos($t, 'firefox')) {
preg_match('/rv:(.*)\)/', $_SERVER['HTTP_USER_AGENT'], $matches, PREG_OFFSET_CAPTURE);
if(isset($matches[1])) {
return intval($matches[1][0]);
}else{
return 'no-version';
}
}
return 'not-firefox';
}
public static function get_ip() {
// IP si internet partagé
if (isset($_SERVER['HTTP_CLIENT_IP'])) {
return $_SERVER['HTTP_CLIENT_IP'];
}
// IP derrière un proxy
elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return $_SERVER['HTTP_X_FORWARDED_FOR'];
}
// Sinon : IP normale
else {
return (isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '');
}
}
public static function get_os() {
$user_agent = $_SERVER['HTTP_USER_AGENT'];
$os_platform = "Inconnu";
$os_array = array(
'/windows nt 10/i' => 'Windows 10',
'/windows nt 6.3/i' => 'Windows 8.1',
'/windows nt 6.2/i' => 'Windows 8',
'/windows nt 6.1/i' => 'Windows 7',
'/windows nt 6.0/i' => 'Windows Vista',
'/windows nt 5.2/i' => 'Windows Server 2003/XP x64',
'/windows nt 5.1/i' => 'Windows XP',
'/windows xp/i' => 'Windows XP',
'/windows nt 5.0/i' => 'Windows 2000',
'/windows me/i' => 'Windows ME',
'/win98/i' => 'Windows 98',
'/win95/i' => 'Windows 95',
'/win16/i' => 'Windows 3.11',
'/macintosh|mac os x/i' => 'Mac OS X',
'/mac_powerpc/i' => 'Mac OS 9',
'/linux/i' => 'Linux',
'/ubuntu/i' => 'Ubuntu',
'/iphone/i' => 'iPhone',
'/ipod/i' => 'iPod',
'/ipad/i' => 'iPad',
'/android/i' => 'Android',
'/blackberry/i' => 'BlackBerry',
'/webos/i' => 'Mobile'
);
foreach ($os_array as $regex => $value) {
if (preg_match($regex, $user_agent)) {
$os_platform = $value;
}
}
return $os_platform;
}
protected function get_browser_name()
{

View File

@ -277,6 +277,43 @@ class Url
return $url . "/" . BASE_SERVER_DIRECTORY;
}
}
public static function getPageName(){
$url = parse_url($_SERVER['REQUEST_URI']);
$urlTrim = trim($url['path'], '/');
$urlParts = explode('/', $urlTrim);
//suppression des sous repertoires du BASE_SERVER_DIRECTORY
$basePath = explode( '/', BASE_SERVER_DIRECTORY);
foreach($basePath as $subDir) {
if ($urlParts[0] == $subDir) {
array_shift($urlParts);
}
}
//Récupération du nom de la page
if (isset($urlParts[0])) {
//il se peut que l'on ait des variable avec ? dans l'url
$urlQuery = explode('?', $urlParts[0]);
$urlQueryPageName = $urlQuery[0];
($urlQueryPageName == 'index' || $urlQueryPageName == '') ? $page['name'] = 'index' : $page['name'] = $urlQueryPageName;
unset($urlParts[0]);
} else {
$page['name'] = 'index';
}
$page['name'] = strtolower($page['name']);
//si c'est une page de controle de formulaire : on renomme la page
if ($page['name'] == 'control') {
$page['control'] = true;
($urlParts[1] == 'index' || $urlParts[1] == '') ? $page['name']='index' : $page['name']=$urlParts[1];
unset($urlParts[1]);
}
return $page;
}
/**
* Obtiens le fragment depuis une variable serveur,
* ce qui est selon moi possible avec une bonne configuration serveur

View File

@ -30,6 +30,10 @@
@section('top-css')
<link rel="stylesheet" href="{{ \MVC\Classe\Url::asset_rewrite('assets/bootstrap-5.0.0-beta1-dist/css/bootstrap.min.css')}}">
<link rel="stylesheet" href="{{ \MVC\Classe\Url::asset_rewrite('assets/css/custom.css')}}">
@if(\MVC\Classe\Browser::get() == 'Internet Explorer')
<link rel="stylesheet" href="{{\MVC\Classe\Url::asset_rewrite('assets/html5-simple-date-input-polyfill-master/html5-simple-date-input-polyfill.css')}}">
<!--<link rel="stylesheet" href="{{\MVC\Classe\Url::asset_rewrite('assets/hyperform-0.12.0/css/hyperform.css')}}">-->
@endif
@show
</head>
@ -37,6 +41,15 @@
<body>
@section('top-javascript')
@if(\MVC\Classe\Browser::get() == 'Internet Explorer')
<!-- Polyfill.io will load polyfills your browser needs -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=default%2CNumber.parseInt%2CNumber.parseFloat%2CArray.prototype.find%2CArray.prototype.includes"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.12.1/polyfill.min.js" integrity="sha512-uzOpZ74myvXTYZ+mXUsPhDF+/iL/n32GDxdryI2SJronkEyKC8FBFRLiBQ7l7U/PTYebDbgTtbqTa6/vGtU23A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script>
<script src="{{\MVC\Classe\Url::asset_rewrite('assets/html5-simple-date-input-polyfill-master/html5-simple-date-input-polyfill.js')}}"></script>
<script src="{{\MVC\Classe\Url::asset_rewrite('assets/hyperform-0.12.0/dist/hyperform.js')}}"></script>
<script>hyperform(window);</script>
@endif
@show
@yield('body')
@ -45,6 +58,7 @@
<script src="{{ \MVC\Classe\Url::asset_rewrite('assets/bootstrap-5.0.0-beta1-dist/js/bootstrap.min.js')}}"></script>
<script src="{{ \MVC\Classe\Url::asset_rewrite('assets/js/custom.js')}}"></script>
@if(\MVC\Classe\Browser::get() !== 'Internet Explorer')
<script>
/*
@ -71,6 +85,7 @@
}
}
</script>
@endif
@show
</body>

View File

@ -7,43 +7,44 @@
@endsection
@section('content')
<h1>%%PAGE%% - VUE.js Controlleur</h1>
<br/><br/><br/>
<div id="app">
<div>
<input v-model="searchText" placeholder="Search...">
</div>
<div v-if="is_loading">
<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
</div>
<div v-if="items" >
<a href="#" v-for="item in itemsSearched" :key="item.id">
<div>
<div>
<h2>
@{{ item.title }}
</h2>
</div>
</div>
<div>
<p>
@{{ item.description.slice(0, 300) + "..." }}
</p>
</div>
<div>
<span>Year : @{{ item.release_date }}</span>
<span>Director : @{{ item.director }}</span>
<span>Producer : @{{ item.producer }}</span>
</div>
</a>
</div>
<h1>ghibli - VUE.js Controlleur</h1>
<br/><br/><br/>
<div id="app">
<div>
<input v-model="searchText" placeholder="Search...">
</div>
<div v-if="is_loading" id="is-loading">
<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
</div>
<div v-if="items" >
<a href="#" v-for="item in itemsSearched" :key="item.id">
<div>
<div>
<h2>
@{{ item.title }}
</h2>
</div>
</div>
<div>
<p>
@{{ item.description.slice(0, 300) + "..." }}
</p>
</div>
<div>
<span>Year : @{{ item.release_date }}</span>
<span>Director : @{{ item.director }}</span>
<span>Producer : @{{ item.producer }}</span>
</div>
</a>
</div>
</div>
@endsection
@section('bottom-javascript')
@parent
<script>
@if(\MVC\Classe\Browser::get() !== 'Internet Explorer')
const vue = new Vue({
el: '#app',
data: {
@ -53,11 +54,11 @@
},
mounted() {
axios
.get('https://ghibliapi.herokuapp.com/films')
.then(response => {
this.items = response.data;
.get('https://ghibliapi.herokuapp.com/films')
.then(response => {
this.items = response.data;
this.is_loading = false
})
})
.catch(error => console.log(error))
},
computed : {
@ -78,5 +79,42 @@
}
}
});
@else
const vue = new Vue({
el: '#app',
data: {
items: [],
searchText: '',
is_loading: true,
},
mounted: function() {
axios
.get('https://ghibliapi.herokuapp.com/films')
.then(function(response) {
this.items = response.data;
this.is_loading = false;
document.getElementById('is-loading').style.display = 'none';
})
.catch(function(error) {console.log(error)})
},
computed: {
itemsSearched : function(){
var self = this;
if( this.searchText == ''){
return this.items;
}
return this.items.filter(function(item){
// https://www.reddit.com/r/vuejs/comments/62kfae/how_do_i_create_very_simple_instant_search_filter/
// Must be of string type
return item.title.toLowerCase().indexOf(self.searchText) >= 0 ||
item.producer.toLowerCase().indexOf(self.searchText) >= 0 ||
item.director.toLowerCase().indexOf(self.searchText) >= 0 ||
item.release_date.toString().indexOf(self.searchText) >= 0;
});
}
}
});
@endif
</script>
@endsection

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 liorwohl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,23 @@
# html5-simple-date-input-polyfill
Just include this simple script and IE (>=10) and Firefox will support `<input type="date">` without any dependencies, not even jQuery! 🎉
Support dynamically created inputs, so can be used in single page applications.
Support [AngularJS](https://github.com/angular/angular.js) (and possibly other libraries) bindings.
# Usage
#### browserify
`npm install html5-simple-date-input-polyfill --save`
`require('html5-simple-date-input-polyfill');`
#### Browser
`<link rel="stylesheet" href="html5-simple-date-input-polyfill.css" />`
`<script src="html5-simple-date-input-polyfill.min.js"></script>`
#### SCSS (optional)
`@import "../node_modules/html5-simple-date-input-polyfill/html5-simple-date-input-polyfill.scss";`

View File

@ -0,0 +1,49 @@
.calendar,
.calendar select,
.calendar table,
.calendar td,
.calendar th {
background: #fff;
color: #000;
text-shadow: none;
height: auto;
width: auto;
padding: 0;
line-height: normal;
border: 0;
box-shadow: none;
border-radius: 0;
font-family: sans-serif;
font-size: 14px;
z-index:1000;
}
.calendarContainer{
display:block !important;
}
.calendar {
position: absolute;
border: 1px solid #c0c0c0;
text-align: center;
}
.calendar select {
margin: 3px 5px;
border: 1px solid #c0c0c0;
}
.calendar td,
.calendar th {
width: 14%;
padding: 4px;
text-align: center;
}
.calendar td {
cursor: pointer;
}
.calendar .selected {
font-weight: bold;
}

View File

@ -0,0 +1,241 @@
function calendarExtender (theInput) {
var self = this;
this.theInput = theInput;
this.container = null;
this.theCalDiv = null;
this.selectedDate = new Date();
this.init = function () {
this.getDateFromInput();
this.createCal();
};
//update selectedDate with the date from the input, return true if changed
this.getDateFromInput = function () {
if (this.theInput.value) {
var possibleNewDate = new Date(this.theInput.value);
if (Date.parse(this.theInput.value) && possibleNewDate.toDateString() !== this.selectedDate.toDateString()) {
this.selectedDate = possibleNewDate;
return true;
}
}
return false;
};
//create the calendar html and events
this.createCal = function () {
//creating a container div around the input, the calendar will also be there
this.container = document.createElement('div');
this.container.className = 'calendarContainer';
this.container.style.display = 'inline-block';
this.theInput.parentNode.replaceChild(this.container, this.theInput);
this.container.appendChild(this.theInput);
//the calendar div
this.theCalDiv = document.createElement('div');
this.theCalDiv.className = 'calendar';
this.theCalDiv.style.display = 'none';
this.container.appendChild(this.theCalDiv);
//the year and month selects inside the calendar
this.creathYearAndMonthSelects();
//the days table inside the calendar
this.createMonthTable();
//open the calendar when the input get focus, also on various click events to capture it in all corner cases
this.theInput.addEventListener('focus', function () { self.theCalDiv.style.display = ''; });
this.theInput.addEventListener('mouseup', function () { self.theCalDiv.style.display = ''; });
this.theInput.addEventListener('mousedown', function () { self.theCalDiv.style.display = ''; });
//update the calendar if the date changed manually in the input
this.theInput.addEventListener('keyup', function () {
if (self.getDateFromInput()) {
self.updateSelecteds();
}
});
//close the calendar when clicking outside of the input or calendar
document.addEventListener('click', function (e) {
if (e.target.parentNode !== self.container &&
e.target.parentNode.parentNode !== self.container &&
e.target.parentNode.parentNode !== self.theCalDiv
) {
self.theCalDiv.style.display = 'none';
}
});
};
//create the year and month selects html
this.creathYearAndMonthSelects = function () {
//the year selector inside the calendar
var yearSelect = this.createRangeSelect(new Date().getFullYear() - 80, new Date().getFullYear() + 20, this.selectedDate.getFullYear());
yearSelect.className = 'yearSelect';
this.theCalDiv.appendChild(yearSelect);
yearSelect.onchange = function () {
self.selectedDate.setYear(this.value);
self.selectDate();
self.createMonthTable();
self.theInput.focus();
};
//the month selector inside the calendar
var monthsNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
var monthSelect = this.createRangeSelect(0, 11, this.selectedDate.getMonth(), monthsNames);
monthSelect.className = 'monthSelect';
this.theCalDiv.appendChild(monthSelect);
monthSelect.onchange = function () {
self.selectedDate.setMonth(this.value);
self.selectDate();
self.createMonthTable();
self.theInput.focus();
};
};
//update the year and month selects with the right selected value (if date changed externally)
this.updateSelecteds = function () {
this.theCalDiv.querySelector('.yearSelect').value = this.selectedDate.getFullYear();
this.theCalDiv.querySelector('.monthSelect').value = this.selectedDate.getMonth();
this.createMonthTable();
};
//create the days table
this.createMonthTable = function () {
var year = this.selectedDate.getFullYear(); //get the year (2015)
var month = this.selectedDate.getMonth(); //get the month number (0-11)
var startDay = new Date(year, month, 1).getDay(); //first weekday of month (0-6)
var maxDays = new Date(this.selectedDate.getFullYear(), month + 1, 0).getDate(); //get days in month (1-31)
//if there was a table before, remove it
var oldTables = this.theCalDiv.getElementsByTagName('table');
if (oldTables.length > 0) {
this.theCalDiv.removeChild(oldTables[0]);
}
//the table and header for the month days
var theTable = document.createElement('table');
theTable.innerHTML = '<tr><th>Sun</th><th>Mon</th><th>Tue</th><th>Wed</th><th>Thu</th><th>Fri</th><th>Sat</th></tr>';
this.theCalDiv.appendChild(theTable);
//create the days cols according to the selected month days
var aRow;
var aCell;
for (var cellNum = 0; cellNum < maxDays + startDay; cellNum++) {
//crate a table row in the begining and after each 7 cells
if (cellNum % 7 === 0) {
aRow = theTable.insertRow(-1);
}
aCell = aRow.insertCell(-1);
if (cellNum + 1 > startDay) {
var dayNum = cellNum + 1 - startDay;
aCell.innerHTML = dayNum;
if (dayNum === this.selectedDate.getDate()) {
aCell.className = 'selected';
}
//when clicking on a day in the days table
aCell.addEventListener('click', function () {
//mark the dey with 'selected' css class
self.theCalDiv.querySelector('.selected').className = '';
this.className = 'selected';
self.selectedDate.setDate(parseInt(this.innerHTML));
self.selectDate();
self.theInput.focus();
});
}
}
};
//copy the selected date to the input field
this.selectDate = function () {
var monthText = this.selectedDate.getMonth() + 1;
if (monthText < 10) {
monthText = '0' + monthText;
}
var dayText = this.selectedDate.getDate();
if (dayText < 10) {
dayText = '0' + dayText;
}
this.theInput.value = '' + this.selectedDate.getFullYear() + '-' + monthText + '-' + dayText + '';
//make angular see the change
var fakeEvent = document.createEvent('KeyboardEvent');
fakeEvent.initEvent("change", true, false);
this.theInput.dispatchEvent(fakeEvent);
};
//helper function to create html select tags
this.createRangeSelect = function (min, max, selected, namesArray) {
var aOption;
var curNum;
var theText;
var theSelect = document.createElement('select');
for (curNum = min; curNum <= max; curNum++) {
aOption = document.createElement('option');
theSelect.appendChild(aOption);
if (namesArray) {
theText = namesArray[curNum - min];
} else {
theText = curNum;
}
aOption.text = theText;
aOption.value = curNum;
if (curNum === selected) {
aOption.selected = true;
}
};
return theSelect;
}
this.init();
}
//return false if the browser dont support input[type=date]
function checkDateInputSupport () {
var input = document.createElement('input');
input.setAttribute('type','date');
var notADateValue = 'not-a-date';
input.setAttribute('value', notADateValue);
return !(input.value === notADateValue);
}
//will add the calendarExtender to all inputs in the page
function addcalendarExtenderToDateInputs () {
//get and loop all the input[type=date]s in the page that dont have "haveCal" class yet
var dateInputs = document.querySelectorAll('input[type=date]:not(.haveCal)');
[].forEach.call(dateInputs, function (dateInput) {
//call calendarExtender function on the input
new calendarExtender(dateInput);
//mark that it have calendar
dateInput.classList.add('haveCal');
});
}
//run the above code on any <input type='date'> in the document, also on dynamically created ones
//check if type=date is supported or if not mobile, they have built-in support for type='date'
if (!checkDateInputSupport() && typeof window.orientation === 'undefined') {
addcalendarExtenderToDateInputs();
//this is also on mousedown event so it will capture new inputs that might joined to the dom dynamically
document.querySelector('body').addEventListener('mousedown', function (event) {
addcalendarExtenderToDateInputs();
});
}

View File

@ -0,0 +1,9 @@
function calendarExtender(c){var a=this;this.theInput=c;this.theCalDiv=this.container=null;this.selectedDate=new Date;this.init=function(){this.getDateFromInput();this.createCal()};this.getDateFromInput=function(){if(this.theInput.value){var b=new Date(this.theInput.value);if(Date.parse(this.theInput.value)&&b.toDateString()!==this.selectedDate.toDateString())return this.selectedDate=b,!0}return!1};this.createCal=function(){this.container=document.createElement("div");this.container.className="calendarContainer";
this.container.style.display="inline-block";this.theInput.parentNode.replaceChild(this.container,this.theInput);this.container.appendChild(this.theInput);this.theCalDiv=document.createElement("div");this.theCalDiv.className="calendar";this.theCalDiv.style.display="none";this.container.appendChild(this.theCalDiv);this.creathYearAndMonthSelects();this.createMonthTable();this.theInput.addEventListener("focus",function(){a.theCalDiv.style.display=""});this.theInput.addEventListener("mouseup",function(){a.theCalDiv.style.display=
""});this.theInput.addEventListener("mousedown",function(){a.theCalDiv.style.display=""});this.theInput.addEventListener("keyup",function(){a.getDateFromInput()&&a.updateSelecteds()});document.addEventListener("click",function(b){b.target.parentNode!==a.container&&b.target.parentNode.parentNode!==a.container&&b.target.parentNode.parentNode!==a.theCalDiv&&(a.theCalDiv.style.display="none")})};this.creathYearAndMonthSelects=function(){var b=this.createRangeSelect((new Date).getFullYear()-80,(new Date).getFullYear()+
20,this.selectedDate.getFullYear());b.className="yearSelect";this.theCalDiv.appendChild(b);b.onchange=function(){a.selectedDate.setYear(this.value);a.selectDate();a.createMonthTable();a.theInput.focus()};b=this.createRangeSelect(0,11,this.selectedDate.getMonth(),"January February March April May June July August September October November December".split(" "));b.className="monthSelect";this.theCalDiv.appendChild(b);b.onchange=function(){a.selectedDate.setMonth(this.value);a.selectDate();a.createMonthTable();
a.theInput.focus()}};this.updateSelecteds=function(){this.theCalDiv.querySelector(".yearSelect").value=this.selectedDate.getFullYear();this.theCalDiv.querySelector(".monthSelect").value=this.selectedDate.getMonth();this.createMonthTable()};this.createMonthTable=function(){var b=this.selectedDate.getFullYear(),f=this.selectedDate.getMonth(),b=(new Date(b,f,1)).getDay(),f=(new Date(this.selectedDate.getFullYear(),f+1,0)).getDate(),c=this.theCalDiv.getElementsByTagName("table");0<c.length&&this.theCalDiv.removeChild(c[0]);
c=document.createElement("table");c.innerHTML="<tr><th>Sun</th><th>Mon</th><th>Tue</th><th>Wed</th><th>Thu</th><th>Fri</th><th>Sat</th></tr>";this.theCalDiv.appendChild(c);for(var g,e,d=0;d<f+b;d++)if(0===d%7&&(g=c.insertRow(-1)),e=g.insertCell(-1),d+1>b){var h=d+1-b;e.innerHTML=h;h===this.selectedDate.getDate()&&(e.className="selected");e.addEventListener("click",function(){a.theCalDiv.querySelector(".selected").className="";this.className="selected";a.selectedDate.setDate(parseInt(this.innerHTML));
a.selectDate();a.theInput.focus()})}};this.selectDate=function(){var b=this.selectedDate.getMonth()+1;10>b&&(b="0"+b);var a=this.selectedDate.getDate();10>a&&(a="0"+a);this.theInput.value=""+this.selectedDate.getFullYear()+"-"+b+"-"+a+"";b=document.createEvent("KeyboardEvent");b.initEvent("change",!0,!1);this.theInput.dispatchEvent(b)};this.createRangeSelect=function(b,a,c,g){var e,d,h,f=document.createElement("select");for(d=b;d<=a;d++)e=document.createElement("option"),f.appendChild(e),h=g?g[d-
b]:d,e.text=h,e.value=d,d===c&&(e.selected=!0);return f};this.init()}function checkDateInputSupport(){var c=document.createElement("input");c.setAttribute("type","date");c.setAttribute("value","not-a-date");return"not-a-date"!==c.value}function addcalendarExtenderToDateInputs(){var c=document.querySelectorAll("input[type=date]:not(.haveCal)");[].forEach.call(c,function(a){new calendarExtender(a);a.classList.add("haveCal")})}
checkDateInputSupport()||"undefined"!==typeof window.orientation||(addcalendarExtenderToDateInputs(),document.querySelector("body").addEventListener("mousedown",function(c){addcalendarExtenderToDateInputs()}));

View File

@ -0,0 +1,43 @@
@mixin reset() {
background: #fff;
color: #000;
text-shadow: none;
height: auto;
width: auto;
padding: 0;
line-height: normal;
border: 0;
box-shadow: none;
border-radius: 0;
font-family: sans-serif;
font-size: 14px;
}
.calendar {
&, select, table, th, td {
@include reset();
}
position: absolute;
border: 1px solid #c0c0c0;
text-align: center;
select {
margin: 3px 5px;
border: 1px solid #c0c0c0;
}
th, td {
width: 14%;
padding: 4px;
text-align: center;
}
td {
cursor: pointer;
}
.selected {
font-weight: bold;
}
}

View File

@ -0,0 +1,20 @@
{
"name": "html5-simple-date-input-polyfill",
"version": "1.1.0",
"description": "input type date polyfill",
"main": "html5-simple-date-input-polyfill.js",
"author": {
"name": "Lior Wohl",
"email": "liorwohl@gmail.com"
},
"repository": {
"type": "git",
"url": "git://github.com/liorwohl/html5-simple-date-input-polyfill.git"
},
"bugs": "https://github.com/liorwohl/html5-simple-date-input-polyfill/issues",
"keywords": ["html5","date","datepicker","type"],
"analyze": true,
"license": "MIT",
"readme": "ERROR: No README data found!"
}

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="html5-simple-date-input-polyfill.css" />
</head>
<body>
normal:
<input type="date" />
with value:
<input type="date" value="2015-03-28" />
dynamically created:
<script>
setTimeout(function(){
var input = document.createElement("input");
input.type = "date";
document.body.appendChild(input);
}, 2000);
</script>
<script src="html5-simple-date-input-polyfill.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,2 @@
/node_modules
npm-debug.log

View File

@ -0,0 +1,8 @@
**/.*
bower.json
bower_components
examples
.gitignore
Makefile
node_modules
.travis.yml

View File

@ -0,0 +1,7 @@
language: node_js
node_js:
- node
env:
global:
- secure: JsPlO0dP7mK7GEqUovqWsXzXY2cVRPvFM6ZJn3QIRRvXTrP4u/qcDpvKvgwR+DvVObLa9/exxEwcIqeYZJz5XKjET2JIgozJERACkJkZkiAjHWyDCuVd271OzNJ968TsOA9Rzzdq3fuv5wBiqjNuW3DK0nQwJiW4iL5ygsyOw0KNEcNVyvipWUY7n0GkIlnCPfi2SqaD+nQxocbsRuDAyCPEl5b01E3IgJPOuHUhBz4r2Dfs9eO5hf1eQSaPCDflvFyKvHIsfwUuN2NzgBXmT0quuoSTxR+V6k69eo2+FuShfOd8+0Z7FlyFnTA6bniBKaVbJ45GXYDA8ecGtYbmUqaT0iA7aUIuHvCBonW2m+x8tWvDR/mDFavdE6loR/RgR1mBtuY+dCKDNBBKFwss3iQar2aMepiU8AvrXq5ViV3rZk6ZFgIsKZ7QHW4kCghhOm0oXLPzaid4kezF9wIw4navEJh9x3+2CPwSh2++IhA8RiyJ5/XDBN5EoDQqxwP6fS3CJUImgM4qal0jRr20YJNeiovkH+yzVDW8frbecLQbpN+GEWyt1Wm9BG8nVccIej7Tc/ndUAnZLsYTv5qhwYp72XnHq/6fLVc4mTpCbA5hNAB8sWiep2NKvujbU7HYaPvoy5tb690ExAuBb4vHS4pND/9w3P5XmdCocVMamOo=
- secure: I9qTcPli1f0IqgzGwQURRp9qi5vWVt0aKKQzUJNfNnhp7zMJEIm3+kTwuORxaFWi8CKq9qkqvL+MmQX3stfkJYCe3NrzHxCb15VgbysGOQtkH2L4WmXZbSWoQVMM9m8RxPZ8WSGdOsLZrdDJWDw2qKHg9RGEhdr9E5PMeOXnIWZx3Le/kdEKBct2QQquWUrKAYcGYfDAejQhj8hzjYBm/fmEchBq3voxUKZ0NwgIOlkgg3pjs9Y/Er/OCzagFkBWuR8HadbhLhPU2CMbnwO97ntUC3taTZHae+xI6n5/4WK5mUZx1M5q5AmriOZ6M/mbxkxrcRx4Ohyzzh/fyoei4Xp4K5MEoibl7L0wD8J58FOEmPn3kUWgHXl17SuQvPpGj98G9Xz5rwUYza5SBLUvtjPkPfQj2j1NXOpD/Vdj2ibidN2EVB2XmkTc99M9TNFVPWmxxfzMjpCQ/4s7sSJKXrjuIFaPUpgqdcP8F6UsAjXXfxnV1AgEGADCeJ7cNnOqq11lqQdfaD2Gk4Zh7OYgeikbQUWgRe4DAdhnfrbFZ22DgLj8hw1NyKCAYl7MxtjA3abZZ2y5v9k+PP4j+T7ijJEClNZ/0MAwbqarOtQnmPAkPjB62nPC4IOxjhnu/JSmBONTAoANvtvnRbHEgsW+bFPS+enjecmy8SRYsFk1NLg=

View File

@ -0,0 +1,508 @@
# Changelog
## UNRELEASED
## v0.12.0
* fix return values for ValidityState properties on non-input elements
* add ES module build
* fix dependency cycle. Rollup runs now w/o warnings
* add support for calling Hyperform on arbitrary DOM nodes
* add index.d.ts to npm package
## v0.11.0
* switch build tool to rollup
* remove deprecated underscore names
* fix revalidation for forms with novalidate attribute
## v0.10.2
* fix form validation error for detached elements
## v0.10.1
* fix event properties being set too late
## v0.10.0
* add new event "implicit_submit", that allows to prevent implicit submits
## v0.9.23
* fix application of class hf-user-invalid to checkboxes/radio buttons
## v0.9.22
* fix select boxes not respectin placeholder options
* validate dates stricter
* add TypeScript declarations
## v0.9.21
* fix problem, where badInput was not detected with hyperform(form) calls (issue #49)
## v0.9.20
* hide empty warnings with CSS
* fix select boxes with disabled options selected validating as required
## v0.9.19
* fix custom validation messages gone missing (regression from v0.9.17)
## v0.9.18
* prevent infinite loops from custom validators
## v0.9.17
* add tests to npm package
* live-update warnings, when `setCustomValidity()` is called
* properly delete custom messages
## v0.9.16
* fix radio button warnings still being multiplied on submit
## v0.9.15
* update some dependencies
* add `CHANGELOG.md`
* prevent `is_validation_candidate()` from running twice for each validation
* re-allow validation of elements without `name` attribute, if asked directly
* fix validation for partly required radio groups
* on form validation show only one warning per radio group
## v0.9.14
* prevent non-candidates from being handled
## v0.9.13
* fix tabbing into fields triggering revalidation
## v0.9.12
* fix `element.noValidate` being broken
## v0.9.11
* do not validate elements without name
* add guard against trying to remove a detached warning
* start removing default imports and add comments
* switch to `babel-preset-env`
* add a command to quickly generate a `.po` file for l10n
## v0.9.10
* Connect error messages via `aria-describedby`
## v0.9.9
* update README
## v0.9.8
* Fix for IE 10 & 11 not supporting multiple parameters for classList add() and remove() methods
## v0.9.7
* fix "novalidateOnElements" not added to settings
## v0.9.6
* use translation for base language, if available
## v0.9.5
* trigger a "forminvalid" event
## v0.9.4
* add option to autoload Hyperform
## v0.9.3
* fix bower.json's main field
## v0.9.2
* fix wrong value in `tooShort` message
## v0.9.1
* convert renderer methods to camelCase, too
## v0.9.0
* switch from snake_case to camelCase for public API
* upgrade ava (please run "npm install" after pull)
* remove mobile clients from test matrix, since they throw strange SauceLabs errors
* split "make test" in sub-targets
* extend saucelabs browsers to mobile
* fix Safari `<=` 9 throwing error on uninstalling properties
* enable tests for IE 9/10
* crank up the test matrix
* implement better "clicked on child of `<button>`" detection
## v0.8.15
* fix IE `<=` 10 not knowing HTMLDocument
## v0.8.14
* reintroduce accidentally deleted Firefox safe-guard
## v0.8.13
* change the way we evaluate prevented submit event
* for whatever reason `insertRule()` now needs an explicit index
## v0.8.12
* add support for `\n` in error messages
* fix child nodes mixing up `event.target` of button clicks
## v0.8.11
* allow filtering is_validation_candidate result
## v0.8.10
* make logging optional with debug=bool setting
* add `.hf-user-valid`
* add more classes to mirror pseudo-classes
* adapt the date rendering in date and time input error messages
## v0.8.9
* fix IE not setting `defaultPrevented`
## v0.8.8
* catch case, where native properties cannot be overloaded
## v0.8.7
* ignore non-essential files on (bower|npm) install
## v0.8.6
* fix the `prevent_implicit_submit` switch
## v0.8.5
* add setting `prevent_implicit_submit`
* apparently `originalTarget` is a protected getter on `Event`
* streamline form submission better
## v0.8.4
* fix some problems with non-AJAX form submission
## v0.8.3
* filter attributes before being set/get
## v0.8.2
* fix evaluation of "formnovalidate" on submit buttons
## v0.8.1
* fix polyfills not being applied in global context
* create hook registry w/o prototype
* add add_filter as alias to add_hook
* add current value as param to filters
* add call_filter to filter a value through hooks
* add hook infrastructure
## v0.8.0
* rename hyperform.register to hyperform.add_validator
## v0.7.7
* unify form submission a bit
* add name=value of submit button to form submit
## v0.7.6
* disallow multiple wrappers per form
## v0.7.5
* complete uninstall process
* define attribute helpers
* fix calling issue and tests
* add a "polyunfill" method mirroring polyfill()
* create dedicated "polyfill" method
* add a hybrid re-evaluation strategy
* fix leaking implementation
* allow renderer to be reset to default
## v0.7.4
* add /css to "files" setting in package.json
* remove necessity for some ES6 shims
* fix detection of revalidate=never
* `const`-antize all the things!
* fall back to attribute data-validator for custom validation messages
* fix "blur" event delegation
* allow "onblur" for settings.revalidate
* support "never" for settings.revalidate
* add support for per-element custom messages
* make naming of component clearer
* uninstall more properties
* refactor validity checkers
* factor out the huge validity state checkers
* fix calls to polyfill w/ changed signature
* change rest of polyfills to use explicit element arg
* change some polyfills from `this` to explicit arg
* set aria-live=polite on warnings
* enhance setting propagation
* switch case for a class
* add a destroy method to Wrapper
* polyfill some properties like element.maxLength
* trim email/url before validation
* fix possible loop in bad_input validator
* postpone creation of DOM elements
* add support for children of `<datalist>` not being validated
* change overwrite behavior of property_installer
* add missing methods to `<form>`
## v0.7.3
* branch out attach/detach renderer
* fix naming error
## v0.7.2
* make classes for validation / warnings configurable
* fix get_wrapper import :(
* make Wrapper.get_wrapped a standalone function
## v0.7.1
* fix wrong values in error messages
* fix bogged export
## v0.7.0
* add proper AMD and CJS versions
## v0.6.3
* fix evaluation of original badInput
## v0.6.2
* try to evaluate the original badInput state, too
## v0.6.1
* allow non-boolean returns in custom validators
## v0.6.0
* s/hyperform.add_renderer/hyperform.set_renderer/
## v0.5.12
* focus first invalid field on submit validation
## v0.5.11
* trigger a submit event manually when catching the original form submit
* fix wrong validity calculation
* call warning renderer for all radio buttons w/ same name
* change the way custom validators are called in customError
## v0.5.10
* fix setCustomValidity setter
## v0.5.9
* fix try to set property on possible primitive
## v0.5.8
* know your own wrapper functions...
## v0.5.7
* trigger "validate" on form before submit
## v0.5.6
* do report errors on input validation
## v0.5.5
* fix reportValidity not removing warning
## v0.5.4
* fix the way the wrapped container is fetched
* implement shortcut to find wrapper for element
## v0.5.3
* fix problems with maxlength (D'oh!) and Unicode string length
## v0.5.2
* apparently getElementsByName is not available on Element
## v0.5.1
* support dates in get_next_value
* put step validation consts in own file
* confirm step working as specced
* fix ms calculation
## v0.5.0
* add support for @accept
* fix minor errors
## v0.4.8
* add proper classes hf-(in)valid and aria-invalid on validation
## v0.4.7
* run validation for _all_ inputs of a form
## v0.4.6
* fix annoying errors
## v0.4.5
* add Wrapper.install to allow adding fields dynamically
## v0.4.4
* add support for a non-custom "valid" event
## v0.4.3
* enhance `<fieldset>` support
## v0.4.2
* add support for non-standard "novalidate" on `<input>` elements
## v0.4.1
* make sure the registry always returns an array
* allow more than one custom validator per element
## v0.4.0
* add a registry for user-defined validators
## v0.3.1
* update README
## v0.3.0
* correct wrong typeof test
* change public API to simple callable
* support novalidate in submit catcher
* add first versions of step(Up|Down)
* fix type detection for valueAs*
* make step validation for months more robust
## v0.2.4
* add checkmarks to feature table
## v0.2.3
* update README
## v0.2.2
* reset the validity again, when an element becomes valid
* set the validation msg via original setCustomValidity
* add styles
* determine the type of an input more reliably
## v0.2.1
* fix issues and mask the WeakMap in message_store
* catch form submission and call our own reportValidity
* fix bugs
* react appropriately in reportValidity, when event is canceled
* publish Renderer.set as hyperform.add_renderer
## v0.2.0
* fix step validator for type=month
* allow overflowing months / dates
## v0.1.9
* restrict npm package to src and dist folders
* change l10n infrastructure
## v0.1.8
* make code more robust thanks to tests
* update bower keyword list
* fix npm version script
## v0.1.7
* add bower.json (for the good old times)
* publish original method as `_original_method`
## v0.1.6
* support "jsnext:main" in package.json
* implement most of ValidityState.badInput
* disallow mark() on primitives
## v0.1.5
* fix problem with string_to_date parser
## v0.1.4
* fix determining current version during npm version bump
## v0.1.3
* switch to npm version for bumping versions
## v0.1.2
* allow step validator to consume most date types
* add license
## v0.1.1
* support date types in max/min validators
* prepare date support for validators
* put type information in single place
* fix minor issues
* mark all polyfills with a "hyperform" property
* change the way element.validity is installed
* allow capturing all inputs via prototype
* fix array search, rebuild
* change sprintf implementation
* fix getting Date from a week number
* fix valueAs* and add jsdom to tests
* fix padding in date_to_string
* fix valueAs*.install
* add valueAsNumber and fix issues with valueAsDate
* add polyfill for valueAsDate
* add version to interface
* fix some errors
* add build infrastructure
* start implementing the HTML5 form validation API

View File

@ -0,0 +1,81 @@
# Contributing to Hyperform
Cool, thanks for joining in and welcome aboard! If you have Node.js and `make`
installed, you are ready to start.
**Before you start editing:** If you dont directly fix an already reported
issue, please do open a new one before! Otherwise there might be the chance,
that your work is not fully aligned with Hyperforms goals, and your time
wasted.
## Set-Up
Log in to [GitHub](https://github.com) and fork
[Hyperform](https://github.com/hyperform/hyperform) (button in the upper-right
corner). Then switch to your terminal:
```sh
$ git clone git@github.com:YourUserName/hyperform.git
$ cd hyperform
$ npm install
# now you're ready to go. Try your first build to see if everything works:
$ make -B && git status
```
Git should show no file changes. Start editing the files in `src` and build
again with `make`.
## Testing Your Edit
For this you need a [SauceLabs](https://saucelabs.com/) account. Its free to
register and allows testing in a bunch of browsers concurrently. Export your
SauceLabs API token from your profile page to your shell:
```sh
$ export SAUCE_USERNAME=your_saucelabs_user
$ export SAUCE_ACCESS_KEY=your_api_key
```
Then you can run all tests with a single command:
```sh
$ make test
```
If you do not want to create a SauceLabs account, you can also do the tests
manually:
```sh
$ make test-syntax
$ make test-unit
```
and then open `test/functional/index.html` in your browser and verify, that
all tests return green.
**Attention:** The functional tests are performed on `dist/hyperform.js`. Dont
forget to `make` that file prior to testing!
## Keeping a Look at the File Size
If you have [`gnuplot`](http://gnuplot.sourceforge.net/) installed, try
```sh
$ make cmpsize
```
This produces a nice little chart of how the size of `dist/hyperform.min.js`
changed over time. If you notice a huge peak at the very end, maybe there could
be one or the other byte shoved off before you commit :wink:.
## Submitting a Pull Request
See [Githubs help page](https://help.github.com/articles/using-pull-requests/)
on how that works exactly (with screenshots!). Please try to make title and
description of the change request meaningful.
## If Something Goes Wrong
If you encounter any problem, grab Manuel on
[Twitter](https://twitter.com/m_strehl) or via
[e-mail](http://www.manuel-strehl.de/about/contact).

View File

@ -0,0 +1,18 @@
Copyright © 2016 Manuel Strehl <boldewyn@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IM-
PLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC-
TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,74 @@
ROLLUP := ./node_modules/.bin/rollup
ROLLUP_ARGS := -c
JSHINT := node_modules/.bin/jshint
JSHINT_ARGS :=
all: js
.PHONY: all
js: dist/hyperform.js dist/hyperform.min.js \
dist/hyperform.amd.js dist/hyperform.amd.min.js \
dist/hyperform.cjs.js dist/hyperform.cjs.min.js \
dist/hyperform.esm.js dist/hyperform.esm.min.js
.PHONY: js
# see
# https://stackoverflow.com/a/10609434/113195
# for this trick to invoke rollup just once for all files
dist/hyperform.amd.min.js \
dist/hyperform.cjs.min.js \
dist/hyperform.esm.min.js \
dist/hyperform.min.js \
dist/hyperform.js \
dist/hyperform.amd.js \
dist/hyperform.cjs.js \
dist/hyperform.esm.js: intermediate-build-step
.INTERMEDIATE: intermediate-build-step
intermediate-build-step: src/hyperform.js src/*.js src/*/*.js
@echo "* build $@"
@mkdir -p dist
@$(ROLLUP) $(ROLLUP_ARGS)
test: test-syntax test-unit test-functional
.PHONY: test
test-syntax:
@echo "* run syntax tests"
@$(JSHINT) $(JSHINT_ARGS) src
.PHONY: test-syntax
test-unit:
@echo "* run unit tests"
@node_modules/.bin/ava
.PHONY: test-unit
test-functional:
@echo "* run functional tests"
@node_modules/.bin/karma start karma.conf.js
.PHONY: test-functional
version:
@# needs a VERSION= variable on the command line!
@# assumes line 3 in bower.json is the version!
@if [ ! -z '$(VERSION)' ]; then \
sed -i '/^export default '"'"'[0-9.]\+'"'"';$$/c\export default '"'"'$(VERSION)'"'"';' src/version.js; \
sed -i '3c\ "version": "$(VERSION)",' bower.json; \
sed -i 's/## UNRELEASED$$/## UNRELEASED\n\n## v$(VERSION)/' CHANGELOG.md; \
fi
.PHONY: version
GNUPLOT_STYLE := impulses
cmpsize:
git log --reverse --pretty=format:%H dist/hyperform.min.js | \
( \
while read x; do git show "$$x:dist/hyperform.min.js" | wc -c ; done; \
cat dist/hyperform.min.js | wc -c \
) | \
gnuplot -p -e "set ylabel 'bytes'; set key outside; set key above; plot '< cat' using 1 title 'size of dist/hyperform.min.js' with $(GNUPLOT_STYLE)"
.PHONY: cmpsize
translate.po: src/*.js src/*/*.js
xgettext -LJavascript -k_ -o $@ --from-code utf-8 $^

View File

@ -0,0 +1,191 @@
# ![Text “Hyperform - Insert Form” in 80s arcade game style](https://hyperform.js.org/statics/header.png)
[![CDNJS](https://img.shields.io/cdnjs/v/hyperform.svg?colorB=green)](https://cdnjs.com/libraries/hyperform)
## Capture form validation back from the browser
Hyperform is your one-stop solution for client-side form handling.
It features a complete implementation of the HTML5 form validation API in
JavaScript, replaces the browsers native methods (if they are even
implemented…), and enriches your toolbox with custom events and hooks.
Not pumped yet? Then [take a look](https://hyperform.js.org/examples.html) at
our awesome [examples](https://hyperform.js.org/examples.html).
## Installation
### Embed from a CDN
Get up and running with Hyperform by embedding it from a CDN:
[CDNJS](https://cdnjs.com/libraries/hyperform)
```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/hyperform/0.9.5/hyperform.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/hyperform/0.9.5/hyperform.min.css">
```
or unpkg.com:
```html
<script src="https://unpkg.com/hyperform"></script>
<link rel="stylesheet" href="https://unpkg.com/hyperform@latest/css/hyperform.css">
```
### Install locally
The easiest way is installing via `npm`:
```sh
npm install hyperform
```
or you can use Bower:
```sh
bower install hyperform
```
or download the [current version as ZIP
archive](https://github.com/hyperform/hyperform/archive/master.zip).
Then embed `dist/hyperform.min.js` in your file:
```html
<script src="path/to/hyperform/dist/hyperform.min.js"></script>
```
or require it in your code:
```js
const hyperform = require('hyperform');
```
In old browsers you will need polyfills for the following features:
[`WeakMap`](https://github.com/Benvie/WeakMap) (IE 10 and lower),
[`element.classList`](https://github.com/remy/polyfills) (IE 9 and lower),
`array.filter`, `array.every`, `Object.keys` and
`Object.defineProperty` (IE 8 and lower). For IE 9+ support simply add this
line to use [Polyfill.ios service](https://polyfill.io):
```html
<script src="https://polyfill.io/v2/polyfill.min.js?features=Element.prototype.classList,WeakMap"></script>
```
## Usage
You can let Hyperform take over a single form:
```js
hyperform(document.forms[0]);
```
or all forms, current and future ones:
```js
hyperform(window);
```
Configure settings as second argument:
```js
hyperform(window, { revalidate: 'never' });
```
If you only need a certain feature once, you can access it directly by name:
```js
if (hyperform.willValidate(some_input_element)) {
var is_valid = hyperform.validityState(some_input_element).valid;
}
```
[The full documentation](https://hyperform.js.org/docs/) provides you with all
the nitty-gritty details and tricks.
### What About the UI?
You might be wondering, how to get nifty datepickers and range sliders and
stuff. Unfortunately, this is out of topic for Hyperform, but despair not!
[Hyperform UI](https://github.com/hyperform/hyperform-ui) (beta) is here to
fill in the gaps with the help of jQuery UI.
> “jQuery UI? Isnt that that thing that they had before React?” — “No, thats
> Backbone.” — “But before that?” — “No, that was Kendo.” — “...?”
If you had these thoughts right now, rest assured. For the purpose of input
widgets there is still close to no other library, that is complete, themable,
accessible and has wide browser support. Just try it yourself!
## Examples
[Yes, please! The more the better.](https://hyperform.js.org/examples.html)
## Status
The target is 100% support for the [HTML5 validation
API](https://html.spec.whatwg.org/multipage/forms.html#constraints). Currently
supported:
| feature | status |
| ---------------------------- | ----------- |
| `willValidate` | :full_moon: |
| `setCustomValidity(message)` | :full_moon: |
| `validity.valueMissing` | :full_moon: |
| `validity.typeMismatch` | :full_moon: |
| `validity.patternMismatch` | :full_moon: |
| `validity.tooLong` | :full_moon: |
| `validity.tooShort` | :full_moon: |
| `validity.rangeUnderflow` | :full_moon: |
| `validity.rangeOverflow` | :full_moon: |
| `validity.stepMismatch` | :full_moon: |
| `validity.badInput` | :full_moon: |
| `validity.customError` | :full_moon: |
| `validity.valid` | :full_moon: |
| `checkValidity()` | :full_moon: |
| `reportValidity()` | :full_moon: |
| `validationMessage` | :full_moon: |
| `valueAsDate` | :full_moon: |
| `valueAsNumber` | :full_moon: |
| `valueLow` / `valueHigh` | :new_moon: |
| `stepUp(n)` / `stepDown(n)` | :full_moon: |
| `accept` attribute | :full_moon: |
| support for `novalidate` | :full_moon: |
Current test status: [![View on Travis CI](https://api.travis-ci.org/hyperform/hyperform.svg?branch=master)](https://travis-ci.org/hyperform/hyperform)
### Browser Support
Hyperform is fully tested and supported in
* Chrome (latest 3)
* Firefox (latest 3 and ESR)
* MS Edge (latest)
* IE down to version 9 (yes, you've read that correctly) when `WeakMap` for IE
≤ 10 and `classList` for IE 9 are polyfilled
* Safari. _Caveat:_ In versions ≤ 9 [polyfills do not
work](https://github.com/hyperform/hyperform/issues/16). However, form
validation and direct method calling works as expected.)
## Contributing
Cool, yes! Head over to the [contributing guide](CONTRIBUTING.md) for details.
## Changelog
We maintain an up-to date [changelog named `CHANGELOG.md`](CHANGELOG.md)
alongside this file.
## License
This library is released under the terms of the [MIT license](LICENSE.md).
## Contributors
Hyperform is developed by [Manuel Strehl](https://twitter.com/m_strehl) with
contributions by
[Andrey Volynkin](https://github.com/Avol-V),
[Daniel Wang](https://github.com/pvnr0082t),
[Darlan Mendonça](https://github.com/darlanmendonca),
[Christoph Dörfel](https://github.com/Garbanas),
[Josh Farneman](https://github.com/farneman),
[Casey Corcoran](https://github.com/snaptopixel),
and many people reporting issues.

View File

@ -0,0 +1,29 @@
{
"name": "hyperform",
"version": "0.12.0",
"homepage": "https://github.com/hyperform/hyperform",
"authors": [
"Manuel Strehl"
],
"description": "Capture form validation back from the browser",
"main": "src/hyperform.js",
"moduleType": "es6",
"keywords": [
"html5",
"form",
"forms",
"input",
"validation"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"examples",
"Makefile",
"src",
"test",
"tests"
]
}

View File

@ -0,0 +1,37 @@
.hf-warning {
background: linear-gradient(to bottom, rgba(255,255,255,.9), rgba(255,255,255,.75));
border: 1px solid rgba(0,0,0,.2);
border-radius: 1px;
box-shadow: 0 12px 10px -10px rgba(0,0,0,.5);
box-sizing: border-box;
max-width: 100%;
color: #621;
font-size: 14px;
line-height: 18px;
padding: .25em .5em;
pointer-events: none;
/* make sure, \n is preserved in messages. */
white-space: pre-line;
}
.hf-warning:empty {
display: none;
}
/* :invalid is not yet supported in IE 9, so split selectors */
.hf-invalid + .hf-warning {
display: none;
position: absolute;
}
:invalid + .hf-warning {
display: none;
position: absolute;
}
.hf-invalid:focus + .hf-warning:not(:empty) {
display: block;
}
:invalid:focus + .hf-warning:not(:empty) {
display: block;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,12 @@
main {
max-width: 1024px;
margin: 1.5em auto;
}
:not(button).hf-invalid {
border-color: red !important;
}
:not(button).hf-valid {
border-color: green !important;
}

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Simple Form</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<link rel="stylesheet" href="../css/hyperform.css">
<link rel="stylesheet" href="examples.css">
</head>
<body>
<main>
<form id="simple">
<p>
<label>
A required field. Try submitting without filling it in.<br>
<input type="text" name="foo" required class="form-control">
</label>
</p>
<p>
<button class="pure-button pure-button-primary">go!</button>
</p>
</form>
</main>
<script src="../dist/hyperform.js"></script>
<script>hyperform(window);</script>
</body>
</html>

View File

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Different Element Types</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<link rel="stylesheet" href="../css/hyperform.css">
<link rel="stylesheet" href="examples.css">
</head>
<body>
<main>
<form id="types">
<div class="row">
<p class="col-md-4 form-group">
<label for="number">Number:</label>
<input type="number" name="number" id="number" class="form-control">
</p>
<p class="col-md-4 form-group">
<label for="url">URL:</label>
<input type="url" name="url" id="url" class="form-control">
</p>
<p class="col-md-4 form-group">
<label for="email">E-Mail address:</label>
<input type="email" name="email" id="email" class="form-control">
</p>
</div>
<div class="row">
<p class="col-md-4 form-group">
<label for="tel">Phone number:</label>
<input type="tel" name="tel" id="tel" class="form-control">
<span class="help-block">there is no default validation associated with those.</span>
</p>
<p class="col-md-4 form-group">
<label for="color">Color:</label>
<input type="color" name="color" id="color" class="form-control">
<span class="help-block">must be in format <code>#[a-f0-9]{6}</code>.</span>
</p>
<p class="col-md-4 form-group">
<label for="range">Range:</label>
<input type="range" name="range" id="range" class="form-control">
</p>
</div>
<div class="row">
<p class="col-md-4 form-group">
<label for="date">Date:</label>
<input type="date" name="date" id="date" class="form-control">
</p>
<p class="col-md-4 form-group">
<label for="time">Time:</label>
<input type="time" name="time" id="time" class="form-control">
<span class="help-block">in 1-minute steps</span>
</p>
<p class="col-md-4 form-group">
<label for="month">Month:</label>
<input type="month" name="month" id="month" class="form-control">
</p>
</div>
<p>
<button class="btn btn-primary">go!</button>
</p>
</form>
</main>
<script src="../dist/hyperform.js"></script>
<script>hyperform(window);</script>
</body>
</html>

View File

@ -0,0 +1,110 @@
interface HyperformOptions {
/** When true, disable the high-level API. Default: false. */
strict?: boolean;
/** Prevent Enter in input fields to submit the form. Default: false. */
preventImplicitSubmit?: boolean;
/** Whether and when fields should be re-validated automatically. */
revalidate?: 'oninput' | 'onblur' | 'hybrid' | 'onsubmit' | 'never';
/** Whether the non-standard valid event should be triggered. Default: true. */
validEvent?: boolean;
/** Whether the <fieldset> element should be treated like a normal input field, e.g. allowing it to receive an error message. Default: true. */
extendFieldset?: boolean;
/** Whether input fields with the non-standard attribute novalidate should be exempted from validation. Default: true. */
novalidateOnElements?: boolean;
/** CSS class names to use instead of the default ones. */
classes?: {
warning?: string,
valid?: string,
invalid?: string,
validated?: string
}
/** Whether to include input elements without name attribute as validation candidates. Default: false. */
validateNameless?: boolean;
}
export interface HyperformRenderer {
/** called when a warning should become visible */
attachWarning: (warning: HTMLElement, element: HTMLElement) => void;
/** called when a warning should vanish */
detachWarning: (warning: HTMLElement, element: HTMLElement) => void;
/** called when feedback to an element's state should be handled ie: showing and hiding warnings */
showWarning: (element: HTMLElement, wholeFormValidated: boolean) => void;
/** set the warning's content */
setMessage: (warning: HTMLElement, message: string, element: HTMLElement) => void;
}
export interface HyperformValidator {
(element: HTMLElement): boolean;
}
export interface HyperformStatic {
version: string;
/** initializes hyperform on a specific form or globally */
(target: Window | HTMLFormElement, options?: HyperformOptions): void;
/** TODO: add documentation */
ValidityState(element: HTMLElement): ValidityState;
/** check, if an element will be subject to HTML5 validation at all */
willValidate(element: HTMLElement): boolean;
/** represents the value of the element, interpreted as a date */
valueAsDate(element: HTMLElement, value: any): Date | null;
/** represents the value of the element, interpreted as a number */
valueAsNumber(element: HTMLElement, value: any): Number;
/** add custom validation logic for specific elements */
addValidator(element: HTMLElement, validate: HyperformValidator): void;
/** override default renderer methods */
setRenderer<T extends keyof HyperformRenderer>(renderer: T, action: HyperformRenderer[T]): void;
/** check an element's validity with respect to it's form */
checkValidity(element: HTMLElement): boolean;
/** check element's validity and report an error back to the user */
reportValidity(element: HTMLElement): boolean;
/** set a custom validity message or delete it with an empty string */
setCustomValidity(element: HTMLElement, message: string): void;
/** TODO: add documentation */
stepDown(element: HTMLElement, amount: number): void;
/** TODO: add documentation */
stepUp(element: HTMLElement, amount: number): void;
/** get the validation message for an element, empty string, if the element satisfies all constraints. */
validationMessage(element: HTMLElement): string;
/** set the language for Hyperforms messages */
setLanguage(lang: string): HyperformStatic;
/** add a lookup catalog "string: translation" for a language */
addTranslation(lang: string, catalog: any): HyperformStatic;
/** register custom error messages per element */
setMessage(element: HTMLElement, validator: HyperformValidator, message: string): HyperformStatic;
/** TODO: add documentation and types */
addHook(hook: any, action: any, position: any): HyperformStatic;
/** TODO: add documentation and types */
removeHook(hook: any, action: any): HyperformStatic;
}
declare const Hyperform: HyperformStatic;
export default Hyperform;

View File

@ -0,0 +1,130 @@
var customLaunchers = {};
[
//['chrome', 'dev', 'Windows 10'],
['chrome', 'beta', 'Windows 10'],
['chrome', '76.0', 'Windows 10'],
['chrome', '75.0', 'Windows 10'],
['chrome', '74.0', 'Windows 10'],
['chrome', '48.0', 'Linux'],
['chrome', '69.0', 'macOS 10.14'],
['firefox', 'dev', 'Windows 10'],
['firefox', 'beta', 'Windows 10'],
['firefox', '68', 'Windows 10'], /* ESR */
['firefox', '67', 'Windows 10'],
['firefox', '66', 'Windows 10'],
['firefox', '45.0', 'Linux'],
['firefox', '68', 'macOS 10.14'],
['MicrosoftEdge', '18', 'Windows 10'],
['MicrosoftEdge', '17', 'Windows 10'],
['internet explorer', '11.0', 'Windows 10'],
['internet explorer', '11.0', 'Windows 8.1'],
['internet explorer', '10.0', 'Windows 7'],
['internet explorer', '9.0', 'Windows 7'],
['safari', '12.0', 'macOS 10.14'],
['safari', '11.0', 'OS X 10.13'],
// TODO: ['iphone', '8.1', 'OS X 10.10'],
// TODO: ['iphone', '10.0', 'Mac 10.11'],
// TODO: ['android', '4.4', 'Linux'],
].forEach(set => {
customLaunchers['sl_'+set.join('_').replace(/[^a-zA-Z0-9_]+/g, '_')] = {
base: 'SauceLabs',
browserName: set[0],
version: set[1],
platform: set[2],
};
});
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha'],
// list of files / patterns to load in the browser
files: [
// for IE 10 support
'test/functional/weakmap.min.js',
// for IE 9 support
'test/functional/classList.min.js',
'dist/hyperform.js',
'test/functional/test.*.js',
{ pattern: 'test/functional/blank.html', watched: false, included: true, served: true, nocache: true, }
],
proxies: {
// blank.html loading files from another base dir
'/blank.html': '/base/test/functional/blank.html',
'/weakmap.min.js': '/base/test/functional/weakmap.min.js',
'/classList.min.js': '/base/test/functional/classList.min.js',
'/dist/hyperform.js': '/base/dist/hyperform.js',
},
preprocessors: [],
// list of files to exclude
exclude: [
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
//reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
//browsers: ['Chrome', 'Firefox'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous
concurrency: 5,
browserDisconnectTimeout: 5000,
sauceLabs: {
testName: 'Hyperform functional tests',
recordScreenshots: true,
},
customLaunchers: customLaunchers,
browsers: Object.keys(customLaunchers),
reporters: ['dots', 'saucelabs'],
})
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,86 @@
{
"name": "hyperform",
"version": "0.12.0",
"description": "Capture form validation back from the browser",
"main": "dist/hyperform.cjs.js",
"types": "index.d.ts",
"jsnext:main": "src/hyperform.js",
"module": "dist/hyperform.esm.js",
"style": "css/hyperform.css",
"scripts": {
"test": "jshint src && ava",
"preversion": "npm test",
"version": "make version VERSION=$npm_package_version && make all && git add src/version.js dist/ bower.json CHANGELOG.md",
"postversion": "git push && git push --tags"
},
"repository": {
"type": "git",
"url": "git+https://github.com/hyperform/hyperform.git"
},
"keywords": [
"html5",
"form",
"forms",
"input",
"validation"
],
"author": "Manuel Strehl",
"license": "MIT",
"bugs": {
"url": "https://github.com/hyperform/hyperform/issues"
},
"homepage": "https://hyperform.js.org/",
"devDependencies": {
"@babel/core": "^7.8.4",
"@babel/preset-env": "^7.8.4",
"@babel/register": "^7.8.3",
"ava": "^3.5.1",
"jsdom": "^9.10.0",
"jshint": "^2.11.0",
"karma": "^4.4.1",
"karma-chrome-launcher": "^3.1.0",
"karma-firefox-launcher": "^1.3.0",
"karma-mocha": "^1.3.0",
"karma-sauce-launcher": "^4.0.0",
"mocha": "^7.1.1",
"rollup": "^1.31.1",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-banner": "^0.2.1",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^5.2.0",
"rollup-plugin-uglify": "^6.0.4",
"selenium-webdriver": "^3.0.1"
},
"files": [
"css",
"dist",
"src",
"test",
"index.d.ts",
"rollup.config.js"
],
"jshintConfig": {
"esversion": 6,
"strict": "global",
"laxbreak": true,
"globals": {
"window": true,
"document": true
}
},
"ava": {
"require": [
"@babel/register",
"./test/helpers/setup-browser-env.js"
],
"files": [
"test/unit/**/*.js"
],
"concurrency": 5
},
"babel": {
"presets": [
"@babel/env"
]
}
}

View File

@ -0,0 +1,63 @@
import resolve from 'rollup-plugin-node-resolve';
import babel from 'rollup-plugin-babel';
import { terser } from 'rollup-plugin-terser';
import { uglify } from 'rollup-plugin-uglify';
import banner from 'rollup-plugin-banner';
const hybanner = banner('hyperform.js.org');
export default [{
input: 'src/hyperform.js',
output: [
{
file: 'dist/hyperform.js',
format: 'iife',
name: 'hyperform',
plugins: [hybanner],
},
{
file: 'dist/hyperform.min.js',
format: 'iife',
name: 'hyperform',
plugins: [uglify(), hybanner],
},
{
file: 'dist/hyperform.cjs.js',
format: 'cjs',
plugins: [hybanner],
},
{
file: 'dist/hyperform.cjs.min.js',
format: 'cjs',
plugins: [uglify(), hybanner],
},
{
file: 'dist/hyperform.amd.js',
format: 'amd',
plugins: [hybanner],
},
{
file: 'dist/hyperform.amd.min.js',
format: 'amd',
plugins: [uglify(), hybanner],
},
{
file: 'dist/hyperform.esm.js',
format: 'esm',
plugins: [hybanner],
},
{
file: 'dist/hyperform.esm.min.js',
format: 'esm',
plugins: [terser({
module: true,
}), hybanner],
},
],
plugins: [
resolve(),
babel({
exclude: 'node_modules/**' // only transpile our source code
}),
],
}];

View File

@ -0,0 +1,54 @@
'use strict';
/**
* internal storage for custom error messages
*/
const store = new WeakMap();
/**
* register custom error messages per element
*/
const custom_messages = {
set(element, validator, message) {
const messages = store.get(element) || {};
messages[validator] = message;
store.set(element, messages);
return custom_messages;
},
get(element, validator, _default=undefined) {
const messages = store.get(element);
if (messages === undefined || ! (validator in messages)) {
const data_id = 'data-' + validator.replace(/[A-Z]/g, '-$&').toLowerCase();
if (element.hasAttribute(data_id)) {
/* if the element has a data-validator attribute, use this as fallback.
* E.g., if validator == 'valueMissing', the element can specify a
* custom validation message like this:
* <input data-value-missing="Oh noes!">
*/
return element.getAttribute(data_id);
}
return _default;
}
return messages[validator];
},
delete(element, validator=null) {
if (! validator) {
return store.delete(element);
}
const messages = store.get(element) || {};
if (validator in messages) {
delete(messages[validator]);
store.set(element, messages);
return true;
}
return false;
},
};
export default custom_messages;

View File

@ -0,0 +1,87 @@
'use strict';
const registry = Object.create(null);
/**
* run all actions registered for a hook
*
* Every action gets called with a state object as `this` argument and with the
* hook's call arguments as call arguments.
*
* @return mixed the returned value of the action calls or undefined
*/
export function call_hook(hook) {
var result;
const call_args = Array.prototype.slice.call(arguments, 1);
if (hook in registry) {
result = registry[hook].reduce((function(args) {
return function(previousResult, currentAction) {
const interimResult = currentAction.apply({
state: previousResult,
hook: hook,
}, args);
return (interimResult !== undefined)? interimResult : previousResult;
};
})(call_args), result);
}
return result;
}
/**
* Filter a value through hooked functions
*
* Allows for additional parameters:
* js> do_filter('foo', null, current_element)
*/
export function do_filter(hook, initial_value) {
var result = initial_value;
var call_args = Array.prototype.slice.call(arguments, 1);
if (hook in registry) {
result = registry[hook].reduce(function(previousResult, currentAction) {
call_args[0] = previousResult;
const interimResult = currentAction.apply({
state: previousResult,
hook: hook,
}, call_args);
return (interimResult !== undefined)? interimResult : previousResult;
}, result);
}
return result;
}
/**
* remove an action again
*/
export function remove_hook(hook, action) {
if (hook in registry) {
for (let i = 0; i < registry[hook].length; i++) {
if (registry[hook][i] === action) {
registry[hook].splice(i, 1);
break;
}
}
}
}
export { remove_hook as remove_filter };
/**
* add an action to a hook
*/
export function add_hook(hook, action, position) {
if (! (hook in registry)) {
registry[hook] = [];
}
if (position === undefined) {
position = registry[hook].length;
}
registry[hook].splice(position, 0, action);
}
export { add_hook as add_filter };

View File

@ -0,0 +1,85 @@
'use strict';
/**
* the following validation messages are from Firefox source,
* http://mxr.mozilla.org/mozilla-central/source/dom/locales/en-US/chrome/dom/dom.properties
* released under MPL license, http://mozilla.org/MPL/2.0/.
*/
const catalog = {
en: {
TextTooLong: 'Please shorten this text to %l characters or less (you are currently using %l characters).',
ValueMissing: 'Please fill out this field.',
CheckboxMissing: 'Please check this box if you want to proceed.',
RadioMissing: 'Please select one of these options.',
FileMissing: 'Please select a file.',
SelectMissing: 'Please select an item in the list.',
InvalidEmail: 'Please enter an email address.',
InvalidURL: 'Please enter a URL.',
PatternMismatch: 'Please match the requested format.',
PatternMismatchWithTitle: 'Please match the requested format: %l.',
NumberRangeOverflow: 'Please select a value that is no more than %l.',
DateRangeOverflow: 'Please select a value that is no later than %l.',
TimeRangeOverflow: 'Please select a value that is no later than %l.',
NumberRangeUnderflow: 'Please select a value that is no less than %l.',
DateRangeUnderflow: 'Please select a value that is no earlier than %l.',
TimeRangeUnderflow: 'Please select a value that is no earlier than %l.',
StepMismatch: 'Please select a valid value. The two nearest valid values are %l and %l.',
StepMismatchOneValue: 'Please select a valid value. The nearest valid value is %l.',
BadInputNumber: 'Please enter a number.',
},
};
/**
* the global language Hyperform will use
*/
var language = 'en';
/**
* the base language according to BCP47, i.e., only the piece before the first hyphen
*/
var base_lang = 'en';
/**
* set the language for Hyperforms messages
*/
export function set_language(newlang) {
language = newlang;
base_lang = newlang.replace(/[-_].*/, '');
}
/**
* add a lookup catalog "string: translation" for a language
*/
export function add_translation(lang, new_catalog) {
if (! (lang in catalog)) {
catalog[lang] = {};
}
for (let key in new_catalog) {
if (new_catalog.hasOwnProperty(key)) {
catalog[lang][key] = new_catalog[key];
}
}
}
/**
* return `s` translated into the current language
*
* Defaults to the base language and then English if the former has no
* translation for `s`.
*/
export default function(s) {
if ((language in catalog) && (s in catalog[language])) {
return catalog[language][s];
} else if ((base_lang in catalog) && (s in catalog[base_lang])) {
return catalog[base_lang][s];
} else if (s in catalog.en) {
return catalog.en[s];
}
return s;
}

View File

@ -0,0 +1,74 @@
'use strict';
import { get_wrapper } from './wrapper';
import mark from '../tools/mark';
/**
* the internal storage for messages
*/
const store = new WeakMap();
/* jshint -W053 *//* allow new String() */
/**
* handle validation messages
*
* Falls back to browser-native errors, if any are available. The messages
* are String objects so that we can mark() them.
*/
const message_store = {
set(element, message, is_custom=false) {
if (element instanceof window.HTMLFieldSetElement) {
const wrapped_form = get_wrapper(element);
if (wrapped_form && ! wrapped_form.settings.extendFieldset) {
/* make this a no-op for <fieldset> in strict mode */
return message_store;
}
}
if (typeof message === 'string') {
message = new String(message);
}
if (is_custom) {
message.is_custom = true;
}
mark(message);
store.set(element, message);
/* allow the :invalid selector to match */
if ('_original_setCustomValidity' in element) {
element._original_setCustomValidity(message.toString());
}
return message_store;
},
get(element) {
var message = store.get(element);
if (message === undefined && ('_original_validationMessage' in element)) {
/* get the browser's validation message, if we have none. Maybe it
* knows more than we. */
message = new String(element._original_validationMessage);
}
return message? message : new String('');
},
delete(element, is_custom=false) {
if ('_original_setCustomValidity' in element) {
element._original_setCustomValidity('');
}
var message = store.get(element);
if (message && is_custom && ! message.is_custom) {
/* do not delete "native" messages, if asked */
return false;
}
return store.delete(element);
},
};
/* jshint +W053 */
export { message_store };

View File

@ -0,0 +1,32 @@
'use strict';
const internal_registry = new WeakMap();
/**
* A registry for custom validators
*
* slim wrapper around a WeakMap to ensure the values are arrays
* (hence allowing > 1 validators per element)
*/
const custom_validator_registry = {
set(element, validator) {
const current = internal_registry.get(element) || [];
current.push(validator);
internal_registry.set(element, current);
return custom_validator_registry;
},
get(element) {
return internal_registry.get(element) || [];
},
delete(element) {
return internal_registry.delete(element);
},
};
export default custom_validator_registry;

View File

@ -0,0 +1,111 @@
'use strict';
import { message_store } from './message_store';
import { get_wrapper } from './wrapper';
import generate_id from '../tools/generate_id';
import { get_radiogroup } from '../tools/get_radiogroup';
const warningsCache = new WeakMap();
const DefaultRenderer = {
/**
* called when a warning should become visible
*/
attachWarning: function(warning, element) {
/* should also work, if element is last,
* http://stackoverflow.com/a/4793630/113195 */
element.parentNode.insertBefore(warning, element.nextSibling);
},
/**
* called when a warning should vanish
*/
detachWarning: function(warning, element) {
/* be conservative here, since an overwritten attachWarning() might not
* actually have attached the warning. */
if (warning.parentNode) {
warning.parentNode.removeChild(warning);
}
},
/**
* called when feedback to an element's state should be handled
*
* i.e., showing and hiding warnings
*/
showWarning: function(element, whole_form_validated=false) {
/* don't render error messages on subsequent radio buttons of the
* same group. This assumes, that element.validity.valueMissing is the only
* possible validation failure for radio buttons. */
if (whole_form_validated && element.type === 'radio' &&
get_radiogroup(element)[0] !== element) {
return;
}
const msg = message_store.get(element).toString();
var warning = warningsCache.get(element);
if (msg) {
if (! warning) {
const wrapper = get_wrapper(element);
warning = document.createElement('div');
warning.className = wrapper && wrapper.settings.classes.warning || 'hf-warning';
warning.id = generate_id();
warning.setAttribute('aria-live', 'polite');
warningsCache.set(element, warning);
}
element.setAttribute('aria-errormessage', warning.id);
if (!element.hasAttribute('aria-describedby')) {
element.setAttribute('aria-describedby', warning.id);
}
Renderer.setMessage(warning, msg, element);
Renderer.attachWarning(warning, element);
} else if (warning && warning.parentNode) {
if (element.getAttribute('aria-describedby') === warning.id) {
element.removeAttribute('aria-describedby');
}
element.removeAttribute('aria-errormessage');
Renderer.detachWarning(warning, element);
}
},
/**
* set the warning's content
*
* Overwrite this method, if you want, e.g., to allow HTML in warnings
* or preprocess the content.
*/
setMessage: function(warning, message, element) {
warning.textContent = message;
}
};
const Renderer = {
attachWarning: DefaultRenderer.attachWarning,
detachWarning: DefaultRenderer.detachWarning,
showWarning: DefaultRenderer.showWarning,
setMessage: DefaultRenderer.setMessage,
set: function(renderer, action) {
if (! action) {
action = DefaultRenderer[renderer];
}
Renderer[renderer] = action;
},
getWarning: element => warningsCache.get(element),
};
export default Renderer;

View File

@ -0,0 +1,181 @@
'use strict';
import { catch_submit, uncatch_submit } from '../tools/catch_submit';
import ValidityState from '../polyfills/validityState';
import reportValidity from '../polyfills/reportValidity';
import polyfill from '../tools/polyfill';
import polyunfill from '../tools/polyunfill';
const element_prototypes = [
window.HTMLButtonElement.prototype,
window.HTMLInputElement.prototype,
window.HTMLSelectElement.prototype,
window.HTMLTextAreaElement.prototype,
window.HTMLFieldSetElement.prototype,
];
/**
* get the appropriate function to revalidate form elements
*/
function get_revalidator(method='hybrid') {
return function(event) {
if (event.target instanceof window.HTMLButtonElement ||
event.target instanceof window.HTMLTextAreaElement ||
event.target instanceof window.HTMLSelectElement ||
event.target instanceof window.HTMLInputElement) {
if (event.target.form && event.target.form.hasAttribute('novalidate')) {
/* do nothing, if the form forbids it. This still allows manual
* validation via, e.g., input.reportValidity(), but mirrors browser
* behavior, that are also completely silent in this case. */
return;
}
if (method === 'hybrid') {
/* "hybrid" somewhat simulates what browsers do. See for example
* Firefox's :-moz-ui-invalid pseudo-class:
* https://developer.mozilla.org/en-US/docs/Web/CSS/:-moz-ui-invalid */
if (event.type === 'blur' &&
event.target.value !== event.target.defaultValue ||
ValidityState(event.target).valid) {
/* on blur, update the report when the value has changed from the
* default or when the element is valid (possibly removing a still
* standing invalidity report). */
reportValidity(event.target);
} else if ((event.type === 'keyup' && event.keyCode !== 9) ||
event.type === 'change') {
if (ValidityState(event.target).valid) {
// report instantly, when an element becomes valid,
// postpone report to blur event, when an element is invalid
reportValidity(event.target);
}
}
} else if (event.type !== 'keyup' || event.keyCode !== 9) {
/* do _not_ validate, when the user "tabbed" into the field initially,
* i.e., a keyup event with keyCode 9 */
reportValidity(event.target);
}
}
};
}
/**
* run a function on all found elements
*/
function execute_on_elements(fn, elements) {
if (elements instanceof window.Element) {
elements = [ elements ];
}
const elements_length = elements.length;
for (let i = 0; i < elements_length; i++) {
fn(elements[i]);
}
}
/**
* get a function, that removes hyperform behavior again
*/
function get_destructor(hform) {
const form = hform.form;
return function() {
uncatch_submit(form);
form.removeEventListener('keyup', hform.revalidator);
form.removeEventListener('change', hform.revalidator);
form.removeEventListener('blur', hform.revalidator, true);
if (form === window || form.nodeType === 9) {
hform.uninstall(element_prototypes);
polyunfill(window.HTMLFormElement);
} else if (form instanceof window.HTMLFormElement ||
form instanceof window.HTMLFieldSetElement) {
hform.uninstall(form.elements);
if (form instanceof window.HTMLFormElement) {
polyunfill(form);
}
} else if (form instanceof window.HTMLElement) {
hform.observer.disconnect();
for (let subform of Array.prototype.slice.call(form.getElementsByTagName('form'))) {
hform.uninstall(subform.elements);
polyunfill(subform);
}
}
};
}
/**
* add hyperform behavior to a freshly initialized wrapper
*/
export function add_behavior(hform) {
const form = hform.form;
const settings = hform.settings;
hform.revalidator = get_revalidator(settings.revalidate);
hform.observer = { disconnect() {} };
hform.install = elements => execute_on_elements(polyfill, elements);
hform.uninstall = elements => execute_on_elements(polyunfill, elements);
hform._destruct = get_destructor(hform);
catch_submit(form, settings.revalidate === 'never');
if (form === window || form.nodeType === 9) {
/* install on the prototypes, when called for the whole document */
hform.install(element_prototypes);
polyfill(window.HTMLFormElement);
} else if (form instanceof window.HTMLFormElement ||
form instanceof window.HTMLFieldSetElement) {
hform.install(form.elements);
if (form instanceof window.HTMLFormElement) {
polyfill(form);
}
} else if (form instanceof window.HTMLElement) {
for (let subform of Array.prototype.slice.call(hform.form.getElementsByTagName('form'))) {
hform.install(subform.elements);
polyfill(subform);
}
hform.observer = new window.MutationObserver(mutationsList => {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
for (let subform of Array.prototype.slice.call(mutation.addedNodes)) {
if (subform instanceof window.HTMLFormElement) {
hform.install(subform.elements);
polyfill(subform);
}
}
for (let subform of Array.prototype.slice.call(mutation.removedNodes)) {
if (subform instanceof window.HTMLFormElement) {
hform.uninstall(subform.elements);
polyunfill(subform);
}
}
}
}
});
hform.observer.observe(form, {subtree: true, childList: true});
} else {
throw new Error('Hyperform must be used with a node or window.');
}
if (settings.revalidate === 'oninput' || settings.revalidate === 'hybrid') {
/* in a perfect world we'd just bind to "input", but support here is
* abysmal: http://caniuse.com/#feat=input-event */
form.addEventListener('keyup', hform.revalidator);
form.addEventListener('change', hform.revalidator);
}
if (settings.revalidate === 'onblur' || settings.revalidate === 'hybrid') {
/* useCapture=true, because `blur` doesn't bubble. See
* https://developer.mozilla.org/en-US/docs/Web/Events/blur#Event_delegation
* for a discussion */
form.addEventListener('blur', hform.revalidator, true);
}
}

View File

@ -0,0 +1,28 @@
'use strict';
export const default_step = {
'datetime-local': 60,
datetime: 60,
time: 60,
};
export const step_scale_factor = {
'datetime-local': 1000,
datetime: 1000,
date: 86400000,
week: 604800000,
time: 1000,
};
export const default_step_base = {
week: -259200000,
};
export const default_min = {
range: 0,
};
export const default_max = {
range: 100,
};

View File

@ -0,0 +1,32 @@
'use strict';
/* and datetime-local? Spec says “Nah!” */
export const dates = [ 'datetime', 'date', 'month', 'week', 'time', ];
export const plain_numbers = [ 'number', 'range', ];
/* everything that returns something meaningful for valueAsNumber and
* can have the step attribute */
export const numbers = dates.concat(plain_numbers, 'datetime-local');
/* the spec says to only check those for syntax in validity.typeMismatch.
* ¯\_()_/¯ */
export const type_checked = [ 'email', 'url', ];
/* check these for validity.badInput */
export const input_checked = [ 'email', 'date', 'month', 'week', 'time',
'datetime', 'datetime-local', 'number', 'range', 'color', ];
export const text = [ 'text', 'search', 'tel', 'password', ].concat(type_checked);
/* input element types, that are candidates for the validation API.
* Missing from this set are: button, hidden, menu (from <button>), reset and
* the types for non-<input> elements. */
export const validation_candidates = [ 'checkbox', 'color', 'file', 'image',
'radio', 'submit', ].concat(numbers, text);
/* all known types of <input> */
export const inputs = ['button', 'hidden', 'reset'].concat(validation_candidates);
/* apparently <select> and <textarea> have types of their own */
export const non_inputs = ['select-one', 'select-multiple', 'textarea'];

View File

@ -0,0 +1,65 @@
'use strict';
const instances = new WeakMap();
/**
* wrap <form>s, window or document, that get treated with the global
* hyperform()
*/
export default function Wrapper(form, settings) {
/* do not allow more than one instance per form. Otherwise we'd end
* up with double event handlers, polyfills re-applied, ... */
var existing = instances.get(form);
if (existing) {
existing.settings = settings;
return existing;
}
this.form = form;
this.settings = settings;
this.observer = null;
instances.set(form, this);
}
Wrapper.prototype = {
destroy() {
instances.delete(this.form);
if (this._destruct) {
this._destruct();
}
},
};
/**
* try to get the appropriate wrapper for a specific element by looking up
* its parent chain
*
* @return Wrapper | undefined
*/
export function get_wrapper(element) {
var wrapped;
if (element.form) {
/* try a shortcut with the element's <form> */
wrapped = instances.get(element.form);
}
/* walk up the parent nodes until document (including) */
while (! wrapped && element) {
wrapped = instances.get(element);
element = element.parentNode;
}
if (! wrapped) {
/* try the global instance, if exists. This may also be undefined. */
wrapped = instances.get(window);
}
return wrapped;
}

View File

@ -0,0 +1,101 @@
'use strict';
import checkValidity from './polyfills/checkValidity';
import reportValidity from './polyfills/reportValidity';
import setCustomValidity from './polyfills/setCustomValidity';
import stepDown from './polyfills/stepDown';
import stepUp from './polyfills/stepUp';
import validationMessage from './polyfills/validationMessage';
import ValidityState from './polyfills/validityState';
import valueAsDate from './polyfills/valueAsDate';
import valueAsNumber from './polyfills/valueAsNumber';
import willValidate from './polyfills/willValidate';
import custom_messages from './components/custom_messages';
import { add_hook, remove_hook } from './components/hooks';
import { set_language, add_translation } from './components/localization';
import CustomValidatorRegistry from './components/registry';
import Renderer from './components/renderer';
import Wrapper from './components/wrapper';
import { add_behavior } from './components/setup';
import version from './version';
/**
* public hyperform interface:
*/
function hyperform(form, {
classes,
debug=false,
extendFieldset,
novalidateOnElements,
preventImplicitSubmit=false,
revalidate,
strict=false,
validEvent,
validateNameless=false,
}={}) {
if (! classes) {
classes = {};
}
if (extendFieldset === undefined) {
extendFieldset = ! strict;
}
if (novalidateOnElements === undefined) {
novalidateOnElements = ! strict;
}
if (preventImplicitSubmit === undefined) {
preventImplicitSubmit = false;
}
if (revalidate === undefined) {
/* other recognized values: 'oninput', 'onblur', 'onsubmit' and 'never' */
revalidate = strict? 'onsubmit' : 'hybrid';
}
if (validEvent === undefined) {
validEvent = ! strict;
}
const settings = { debug, strict, preventImplicitSubmit, revalidate,
validEvent, extendFieldset, classes, novalidateOnElements,
validateNameless,
};
if (form instanceof window.NodeList ||
form instanceof window.HTMLCollection ||
form instanceof Array) {
return Array.prototype.map.call(form,
element => hyperform(element, settings));
}
const wrapper = new Wrapper(form, settings);
add_behavior(wrapper);
return wrapper;
}
hyperform.version = version;
hyperform.checkValidity = checkValidity;
hyperform.reportValidity = reportValidity;
hyperform.setCustomValidity = setCustomValidity;
hyperform.stepDown = stepDown;
hyperform.stepUp = stepUp;
hyperform.validationMessage = validationMessage;
hyperform.ValidityState = ValidityState;
hyperform.valueAsDate = valueAsDate;
hyperform.valueAsNumber = valueAsNumber;
hyperform.willValidate = willValidate;
hyperform.setLanguage = lang => { set_language(lang); return hyperform; };
hyperform.addTranslation = (lang, catalog) => { add_translation(lang, catalog); return hyperform; };
hyperform.setRenderer = (renderer, action) => { Renderer.set(renderer, action); return hyperform; };
hyperform.addValidator = (element, validator) => { CustomValidatorRegistry.set(element, validator); return hyperform; };
hyperform.setMessage = (element, validator, message) => { custom_messages.set(element, validator, message); return hyperform; };
hyperform.addHook = (hook, action, position) => { add_hook(hook, action, position); return hyperform; };
hyperform.removeHook = (hook, action) => { remove_hook(hook, action); return hyperform; };
if (document.currentScript && document.currentScript.hasAttribute('data-hf-autoload')) {
hyperform(window);
}
export default hyperform;

View File

@ -0,0 +1,35 @@
'use strict';
import { get_validated_elements } from '../tools/get_validated_elements';
import return_hook_or from '../tools/return_hook_or';
import trigger_event from '../tools/trigger_event';
import ValidityState from './validityState';
import { get_wrapper } from '../components/wrapper';
/**
* check an element's validity with respect to it's form
*/
const checkValidity = return_hook_or('checkValidity', function(element) {
/* if this is a <form>, check validity of all child inputs */
if (element instanceof window.HTMLFormElement) {
return get_validated_elements(element).map(checkValidity).every(b=>b);
}
/* default is true, also for elements that are no validation candidates */
const valid = ValidityState(element).valid;
if (valid) {
const wrapped_form = get_wrapper(element);
if (wrapped_form && wrapped_form.settings.validEvent) {
trigger_event(element, 'valid');
}
} else {
trigger_event(element, 'invalid', { cancelable: true });
}
return valid;
});
export default checkValidity;

View File

@ -0,0 +1,70 @@
'use strict';
import install_property from '../tools/property_installer';
import uninstall_property from '../tools/property_uninstaller';
import { do_filter } from '../components/hooks';
const gA = prop => function() {
return do_filter('attr_get_'+prop, this.getAttribute(prop), this);
};
const sA = prop => function(value) {
this.setAttribute(prop, do_filter('attr_set_'+prop, value, this));
};
const gAb = prop => function() {
return do_filter('attr_get_'+prop, this.hasAttribute(prop), this);
};
const sAb = prop => function(value) {
if (do_filter('attr_set_'+prop, value, this)) {
this.setAttribute(prop, prop);
} else {
this.removeAttribute(prop);
}
};
const gAn = prop => function() {
return do_filter('attr_get_'+prop, Math.max(0, Number(this.getAttribute(prop))), this);
};
const sAn = prop => function(value) {
value = do_filter('attr_set_'+prop, value, this);
if (/^[0-9]+$/.test(value)) {
this.setAttribute(prop, value);
}
};
function install_properties(element) {
for (let prop of [ 'accept', 'max', 'min', 'pattern', 'placeholder', 'step', ]) {
install_property(element, prop, {
get: gA(prop),
set: sA(prop),
});
}
for (let prop of [ 'multiple', 'required', 'readOnly', ]) {
install_property(element, prop, {
get: gAb(prop.toLowerCase()),
set: sAb(prop.toLowerCase()),
});
}
for (let prop of [ 'minLength', 'maxLength', ]) {
install_property(element, prop, {
get: gAn(prop.toLowerCase()),
set: sAn(prop.toLowerCase()),
});
}
}
function uninstall_properties(element) {
for (let prop of [ 'accept', 'max', 'min', 'pattern', 'placeholder', 'step',
'multiple', 'required', 'readOnly', 'minLength', 'maxLength', ]) {
uninstall_property(element, prop);
}
}
export { install_properties, uninstall_properties };

View File

@ -0,0 +1,41 @@
'use strict';
import { get_validated_elements } from '../tools/get_validated_elements';
import trigger_event from '../tools/trigger_event';
import Renderer from '../components/renderer';
import ValidityState from './validityState';
import { get_wrapper } from '../components/wrapper';
/**
* check element's validity and report an error back to the user
*/
export default function reportValidity(element) {
/* if this is a <form>, report validity of all child inputs */
if (element instanceof window.HTMLFormElement) {
element.__hf_form_validation = true;
const form_valid = get_validated_elements(element).map(reportValidity).every(b=>b);
delete(element.__hf_form_validation);
return form_valid;
}
/* we copy checkValidity() here, b/c we have to check if the "invalid"
* event was canceled. */
const valid = ValidityState(element).valid;
var event;
if (valid) {
const wrapped_form = get_wrapper(element);
if (wrapped_form && wrapped_form.settings.validEvent) {
event = trigger_event(element, 'valid', { cancelable: true });
}
} else {
event = trigger_event(element, 'invalid', { cancelable: true });
}
if (! event || ! event.defaultPrevented) {
Renderer.showWarning(element, (element.form && element.form.__hf_form_validation));
}
return valid;
}

View File

@ -0,0 +1,25 @@
'use strict';
import { message_store } from '../components/message_store';
import Renderer from '../components/renderer';
import validity_state_checkers from '../tools/validity_state_checkers';
/**
* set a custom validity message or delete it with an empty string
*/
export default function setCustomValidity(element, msg) {
if (! msg) {
message_store.delete(element, true);
} else {
message_store.set(element, msg, true);
}
/* live-update the warning */
const warning = Renderer.getWarning(element);
if (warning) {
Renderer.setMessage(warning, msg, element);
}
/* update any classes if the validity state changes */
validity_state_checkers.valid(element);
}

View File

@ -0,0 +1,28 @@
'use strict';
import get_next_valid from '../tools/get_next_valid';
import get_type from '../tools/get_type';
import { numbers } from '../components/types';
import valueAsNumber from './valueAsNumber';
/**
*
*/
export default function stepDown(element, n=1) {
if (numbers.indexOf(get_type(element)) === -1) {
throw new window.DOMException('stepDown encountered invalid type',
'InvalidStateError');
}
if ((element.getAttribute('step') || '').toLowerCase() === 'any') {
throw new window.DOMException('stepDown encountered step "any"',
'InvalidStateError');
}
const prev = get_next_valid(element, n)[0];
if (prev !== null) {
valueAsNumber(element, prev);
}
}

View File

@ -0,0 +1,28 @@
'use strict';
import get_next_valid from '../tools/get_next_valid';
import get_type from '../tools/get_type';
import { numbers } from '../components/types';
import valueAsNumber from './valueAsNumber';
/**
*
*/
export default function stepUp(element, n=1) {
if (numbers.indexOf(get_type(element)) === -1) {
throw new window.DOMException('stepUp encountered invalid type',
'InvalidStateError');
}
if ((element.getAttribute('step') || '').toLowerCase() === 'any') {
throw new window.DOMException('stepUp encountered step "any"',
'InvalidStateError');
}
const next = get_next_valid(element, n)[1];
if (next !== null) {
valueAsNumber(element, next);
}
}

View File

@ -0,0 +1,19 @@
'use strict';
import { message_store } from '../components/message_store';
/**
* get the validation message for an element, empty string, if the element
* satisfies all constraints.
*/
export default function validationMessage(element) {
const msg = message_store.get(element);
if (! msg) {
return '';
}
/* make it a primitive again, since message_store returns String(). */
return msg.toString();
}

View File

@ -0,0 +1,72 @@
'use strict';
import is_validation_candidate from '../tools/is_validation_candidate';
import mark from '../tools/mark';
import validity_state_checkers from '../tools/validity_state_checkers';
/**
* the validity state constructor
*/
const ValidityState = function(element) {
if (! (element instanceof window.HTMLElement)) {
throw new Error('cannot create a ValidityState for a non-element');
}
const cached = ValidityState.cache.get(element);
if (cached) {
return cached;
}
if (! (this instanceof ValidityState)) {
/* working around a forgotten `new` */
return new ValidityState(element);
}
this.element = element;
ValidityState.cache.set(element, this);
};
/**
* the prototype for new validityState instances
*/
const ValidityStatePrototype = {};
ValidityState.prototype = ValidityStatePrototype;
ValidityState.cache = new WeakMap();
/* small wrapper around the actual validator to check if the validator
* should actually be called. `this` refers to the ValidityState object. */
const checker_getter = (prop, func) => {
return function() {
if (! is_validation_candidate(this.element)) {
/* not being validated == valid by default
* return value == false for all props except "valid", because we test
* problems like badInput here */
return prop === 'valid';
}
return func(this.element);
};
};
/**
* copy functionality from the validity checkers to the ValidityState
* prototype
*/
for (let prop in validity_state_checkers) {
Object.defineProperty(ValidityStatePrototype, prop, {
configurable: true,
enumerable: true,
get: checker_getter(prop, validity_state_checkers[prop]),
set: undefined,
});
}
/**
* mark the validity prototype, because that is what the client-facing
* code deals with mostly, not the property descriptor thing */
mark(ValidityStatePrototype);
export default ValidityState;

View File

@ -0,0 +1,46 @@
'use strict';
import get_type from '../tools/get_type';
import string_to_date from '../tools/string_to_date';
import date_to_string from '../tools/date_to_string';
import { dates } from '../components/types';
/**
* implement the valueAsDate functionality
*
* @see https://html.spec.whatwg.org/multipage/forms.html#dom-input-valueasdate
*/
export default function valueAsDate(element, value=undefined) {
const type = get_type(element);
if (dates.indexOf(type) > -1) {
if (value !== undefined) {
/* setter: value must be null or a Date() */
if (value === null) {
element.value = '';
} else if (value instanceof Date) {
if (isNaN(value.getTime())) {
element.value = '';
} else {
element.value = date_to_string(value, type);
}
} else {
throw new window.DOMException(
'valueAsDate setter encountered invalid value', 'TypeError');
}
return;
}
const value_date = string_to_date(element.value, type);
return value_date instanceof Date? value_date : null;
} else if (value !== undefined) {
/* trying to set a date on a not-date input fails */
throw new window.DOMException(
'valueAsDate setter cannot set date on this element',
'InvalidStateError');
}
return null;
}

View File

@ -0,0 +1,56 @@
'use strict';
import get_type from '../tools/get_type';
import string_to_number from '../tools/string_to_number';
import { numbers } from '../components/types';
import valueAsDate from './valueAsDate';
/**
* implement the valueAsNumber functionality
*
* @see https://html.spec.whatwg.org/multipage/forms.html#dom-input-valueasnumber
*/
export default function valueAsNumber(element, value=undefined) {
const type = get_type(element);
if (numbers.indexOf(type) > -1) {
if (type === 'range' && element.hasAttribute('multiple')) {
/* @see https://html.spec.whatwg.org/multipage/forms.html#do-not-apply */
return NaN;
}
if (value !== undefined) {
/* setter: value must be NaN or a finite number */
if (isNaN(value)) {
element.value = '';
} else if (typeof value === 'number' && window.isFinite(value)) {
try {
/* try setting as a date, but... */
valueAsDate(element, new Date(value));
} catch (e) {
/* ... when valueAsDate is not responsible, ... */
if (! (e instanceof window.DOMException)) {
throw e;
}
/* ... set it via Number.toString(). */
element.value = value.toString();
}
} else {
throw new window.DOMException(
'valueAsNumber setter encountered invalid value', 'TypeError');
}
return;
}
return string_to_number(element.value, type);
} else if (value !== undefined) {
/* trying to set a number on a not-number input fails */
throw new window.DOMException(
'valueAsNumber setter cannot set number on this element',
'InvalidStateError');
}
return NaN;
}

View File

@ -0,0 +1,12 @@
'use strict';
import is_validation_candidate from '../tools/is_validation_candidate';
/**
* check, if an element will be subject to HTML5 validation at all
*/
export default function willValidate(element) {
return is_validation_candidate(element);
}

View File

@ -0,0 +1,323 @@
'use strict';
import trigger_event, { create_event } from './trigger_event';
import matches from './matches';
import { get_validated_elements } from './get_validated_elements';
import reportValidity from '../polyfills/reportValidity';
import { text as text_types } from '../components/types';
import { get_wrapper } from '../components/wrapper';
/**
* submit a form, because `element` triggered it
*
* This method also dispatches a submit event on the form prior to the
* submission. The event contains the trigger element as `submittedVia`.
*
* If the element is a button with a name, the name=value pair will be added
* to the submitted data.
*/
function submit_form_via(element) {
/* apparently, the submit event is not triggered in most browsers on
* the submit() method, so we do it manually here to model a natural
* submit as closely as possible.
* Now to the fun fact: If you trigger a submit event from a form, what
* do you think should happen?
* 1) the form will be automagically submitted by the browser, or
* 2) nothing.
* And as you already suspected, the correct answer is: both! Firefox
* opts for 1), Chrome for 2). Yay! */
var event_got_cancelled;
var submit_event = create_event('submit', { cancelable: true });
/* force Firefox to not submit the form, then fake preventDefault() */
submit_event.preventDefault();
Object.defineProperty(submit_event, 'defaultPrevented', {
value: false,
writable: true,
});
Object.defineProperty(submit_event, 'preventDefault', {
value: () => submit_event.defaultPrevented = event_got_cancelled = true,
writable: true,
});
trigger_event(element.form, submit_event, {}, { submittedVia: element });
if (! event_got_cancelled) {
add_submit_field(element);
window.HTMLFormElement.prototype.submit.call(element.form);
window.setTimeout(() => remove_submit_field(element));
}
}
/**
* if a submit button was clicked, add its name=value by means of a type=hidden
* input field
*/
function add_submit_field(button) {
if (['image', 'submit'].indexOf(button.type) > -1 && button.name) {
const wrapper = get_wrapper(button.form) || {};
var submit_helper = wrapper.submit_helper;
if (submit_helper) {
if (submit_helper.parentNode) {
submit_helper.parentNode.removeChild(submit_helper);
}
} else {
submit_helper = document.createElement('input');
submit_helper.type = 'hidden';
wrapper.submit_helper = submit_helper;
}
submit_helper.name = button.name;
submit_helper.value = button.value;
button.form.appendChild(submit_helper);
}
}
/**
* remove a possible helper input, that was added by `add_submit_field`
*/
function remove_submit_field(button) {
if (['image', 'submit'].indexOf(button.type) > -1 && button.name) {
const wrapper = get_wrapper(button.form) || {};
const submit_helper = wrapper.submit_helper;
if (submit_helper && submit_helper.parentNode) {
submit_helper.parentNode.removeChild(submit_helper);
}
}
}
/**
* check a form's validity and submit it
*
* The method triggers a cancellable `validate` event on the form. If the
* event is cancelled, form submission will be aborted, too.
*
* If the form is found to contain invalid fields, focus the first field.
*/
function check(button) {
/* trigger a "validate" event on the form to be submitted */
const val_event = trigger_event(button.form, 'validate',
{ cancelable: true });
if (val_event.defaultPrevented) {
/* skip the whole submit thing, if the validation is canceled. A user
* can still call form.submit() afterwards. */
return;
}
var valid = true;
var first_invalid;
button.form.__hf_form_validation = true;
get_validated_elements(button.form).map(element => {
if (! reportValidity(element)) {
valid = false;
if (! first_invalid && ('focus' in element)) {
first_invalid = element;
}
}
});
delete(button.form.__hf_form_validation);
if (valid) {
submit_form_via(button);
} else if (first_invalid) {
/* focus the first invalid element, if validation went south */
first_invalid.focus();
/* tell the tale, if anyone wants to react to it */
trigger_event(button.form, 'forminvalid');
}
}
/**
* test if node is a submit button
*/
function is_submit_button(node) {
return (
/* must be an input or button element... */
(node.nodeName === 'INPUT' ||
node.nodeName === 'BUTTON') &&
/* ...and have a submitting type */
(node.type === 'image' || node.type === 'submit')
);
}
/**
* test, if the click event would trigger a submit
*/
function is_submitting_click(event, button) {
return (
/* prevented default: won't trigger a submit */
! event.defaultPrevented &&
/* left button or middle button (submits in Chrome) */
(! ('button' in event) ||
event.button < 2) &&
/* must be a submit button... */
is_submit_button(button) &&
/* the button needs a form, that's going to be submitted */
button.form &&
/* again, if the form should not be validated, we're out of the game */
! button.form.hasAttribute('novalidate')
);
}
/**
* test, if the keypress event would trigger a submit
*/
function is_submitting_keypress(event) {
return (
/* prevented default: won't trigger a submit */
! event.defaultPrevented &&
(
(
/* ...and <Enter> was pressed... */
event.keyCode === 13 &&
/* ...on an <input> that is... */
event.target.nodeName === 'INPUT' &&
/* ...a standard text input field (not checkbox, ...) */
text_types.indexOf(event.target.type) > -1
) || (
/* or <Enter> or <Space> was pressed... */
(event.keyCode === 13 ||
event.keyCode === 32) &&
/* ...on a submit button */
is_submit_button(event.target)
)
) &&
/* there's a form... */
event.target.form &&
/* ...and the form allows validation */
! event.target.form.hasAttribute('novalidate')
);
}
/**
* catch clicks to children of <button>s
*/
function get_clicked_button(element) {
if (is_submit_button(element)) {
return element;
} else if (matches(element, 'button:not([type]) *, button[type="submit"] *')) {
return get_clicked_button(element.parentNode);
} else {
return null;
}
}
/**
* return event handler to catch explicit submission by click on a button
*/
function get_click_handler(ignore=false) {
return function(event) {
const button = get_clicked_button(event.target);
if (button && is_submitting_click(event, button)) {
event.preventDefault();
if (ignore || button.hasAttribute('formnovalidate')) {
/* if validation should be ignored, we're not interested in any checks */
submit_form_via(button);
} else {
check(button);
}
}
};
}
const click_handler = get_click_handler();
const ignored_click_handler = get_click_handler(true);
/**
* catch implicit submission by pressing <Enter> in some situations
*/
function get_keypress_handler(ignore) {
return function keypress_handler(event) {
if (is_submitting_keypress(event)) {
event.preventDefault();
const wrapper = get_wrapper(event.target.form) || { settings: {} };
if (wrapper.settings.preventImplicitSubmit) {
/* user doesn't want an implicit submit. Cancel here. */
return;
}
/* check, that there is no submit button in the form. Otherwise
* that should be clicked. */
const el = event.target.form.elements.length;
var submit;
for (let i = 0; i < el; i++) {
if (['image', 'submit'].indexOf(event.target.form.elements[i].type) > -1) {
submit = event.target.form.elements[i];
break;
}
}
/* trigger an "implicit_submit" event on the form to be submitted */
const implicit_event = trigger_event(event.target.form, 'implicit_submit',
{
cancelable: true
}, {
trigger: event.target,
submittedVia: submit || event.target,
});
if (implicit_event.defaultPrevented) {
/* skip the submit, if implicit submit is canceled */
return;
}
if (submit) {
submit.click();
} else if (ignore) {
submit_form_via(event.target);
} else {
check(event.target);
}
}
};
}
const keypress_handler = get_keypress_handler();
const ignored_keypress_handler = get_keypress_handler(true);
/**
* catch all relevant events _prior_ to a form being submitted
*
* @param bool ignore bypass validation, when an attempt to submit the
* form is detected. True, when the wrapper's revalidate
* setting is 'never'.
*/
export function catch_submit(listening_node, ignore=false) {
if (ignore) {
listening_node.addEventListener('click', ignored_click_handler);
listening_node.addEventListener('keypress', ignored_keypress_handler);
} else {
listening_node.addEventListener('click', click_handler);
listening_node.addEventListener('keypress', keypress_handler);
}
}
/**
* decommission the event listeners from catch_submit() again
*/
export function uncatch_submit(listening_node) {
listening_node.removeEventListener('click', ignored_click_handler);
listening_node.removeEventListener('keypress', ignored_keypress_handler);
listening_node.removeEventListener('click', click_handler);
listening_node.removeEventListener('keypress', keypress_handler);
}

View File

@ -0,0 +1,18 @@
'use strict';
import trim from './trim';
/**
* split a string on comma and trim the components
*
* As specified at
* https://html.spec.whatwg.org/multipage/infrastructure.html#split-a-string-on-commas
* plus removing empty entries.
*/
export default function(str) {
return str.split(',')
.map(item => trim(item))
.filter(b=>b);
}

View File

@ -0,0 +1,66 @@
'use strict';
import sprintf from './sprintf';
import get_week_of_year from './get_week_of_year';
function pad(num, size=2) {
var s = num + '';
while (s.length < size) {
s = '0' + s;
}
return s;
}
/**
* calculate a string from a date according to HTML5
*/
export default function date_to_string(date, element_type) {
if (! (date instanceof Date)) {
return null;
}
switch (element_type) {
case 'datetime':
return date_to_string(date, 'date') + 'T' +
date_to_string(date, 'time');
case 'datetime-local':
return sprintf('%s-%s-%sT%s:%s:%s.%s',
date.getFullYear(),
pad(date.getMonth() + 1),
pad(date.getDate()),
pad(date.getHours()),
pad(date.getMinutes()),
pad(date.getSeconds()),
pad(date.getMilliseconds(), 3)
).replace(/(:00)?\.000$/, '');
case 'date':
return sprintf('%s-%s-%s',
date.getUTCFullYear(),
pad(date.getUTCMonth() + 1),
pad(date.getUTCDate()));
case 'month':
return sprintf('%s-%s', date.getUTCFullYear(),
pad(date.getUTCMonth() + 1));
case 'week': {
const params = get_week_of_year(date);
return sprintf.call(null, '%s-W%s', params[0], pad(params[1]));
}
case 'time':
return sprintf('%s:%s:%s.%s',
pad(date.getUTCHours()),
pad(date.getUTCMinutes()),
pad(date.getUTCSeconds()),
pad(date.getUTCMilliseconds(), 3)
).replace(/(:00)?\.000$/, '');
}
return null;
}

View File

@ -0,0 +1,23 @@
'use strict';
export default function(date, part=undefined) {
switch (part) {
case 'date':
return (date.toLocaleDateString || date.toDateString).call(date);
case 'time':
return (date.toLocaleTimeString || date.toTimeString).call(date);
case 'month':
return ('toLocaleDateString' in date)?
date.toLocaleDateString(undefined, {
year: 'numeric',
month: '2-digit',
})
:
date.toDateString();
// case 'week':
// TODO
default:
return (date.toLocaleString || date.toString).call(date);
}
}

View File

@ -0,0 +1,20 @@
'use strict';
/**
* counter that will be incremented with every call
*
* Will enforce uniqueness, as long as no more than 1 hyperform scripts
* are loaded. (In that case we still have the "random" part below.)
*/
var uid = 0;
/**
* generate a random ID
*
* @see https://gist.github.com/gordonbrander/2230317
*/
export default function(prefix='hf_') {
return prefix + ( uid++ ) + Math.random().toString(36).substr(2);
}

View File

@ -0,0 +1,19 @@
'use strict';
/**
* return a new Date() representing the ISO date for a week number
*
* @see http://stackoverflow.com/a/16591175/113195
*/
export default function(week, year) {
const date = new Date(Date.UTC(year, 0, 1 + (week - 1) * 7));
if (date.getUTCDay() <= 4/* thursday */) {
date.setUTCDate(date.getUTCDate() - date.getUTCDay() + 1);
} else {
date.setUTCDate(date.getUTCDate() + 8 - date.getUTCDay());
}
return date;
}

View File

@ -0,0 +1,93 @@
'use strict';
import date_to_string from './date_to_string';
import string_to_number from './string_to_number';
import get_type from './get_type';
import _ from '../components/localization';
import { default_step, step_scale_factor, default_step_base, default_min,
default_max, } from '../components/step_defaults';
import { dates } from '../components/types';
/**
* get previous and next valid values for a stepped input element
*/
export default function(element, n=1) {
const type = get_type(element);
const aMin = element.getAttribute('min');
let min = default_min[type] || NaN;
if (aMin) {
const pMin = string_to_number(aMin, type);
if (! isNaN(pMin)) {
min = pMin;
}
}
const aMax = element.getAttribute('max');
let max = default_max[type] || NaN;
if (aMax) {
const pMax = string_to_number(aMax, type);
if (! isNaN(pMax)) {
max = pMax;
}
}
const aStep = element.getAttribute('step');
let step = default_step[type] || 1;
if (aStep && aStep.toLowerCase() === 'any') {
/* quick return: we cannot calculate prev and next */
return [_('any value'), _('any value')];
} else if (aStep) {
const pStep = string_to_number(aStep, type);
if (! isNaN(pStep)) {
step = pStep;
}
}
const default_value = string_to_number(element.getAttribute('value'), type);
const value = string_to_number(element.value ||
element.getAttribute('value'), type);
if (isNaN(value)) {
/* quick return: we cannot calculate without a solid base */
return [_('any valid value'), _('any valid value')];
}
const step_base = (
! isNaN(min)? min : (
! isNaN(default_value)? default_value : (
default_step_base[type] || 0
)
)
);
const scale = step_scale_factor[type] || 1;
var prev = step_base +
Math.floor((value - step_base) / (step * scale)) * (step * scale) * n;
var next = step_base +
(Math.floor((value - step_base) / (step * scale)) + 1) * (step * scale) * n;
if (prev < min) {
prev = null;
} else if (prev > max) {
prev = max;
}
if (next > max) {
next = null;
} else if (next < min) {
next = min;
}
/* convert to date objects, if appropriate */
if (dates.indexOf(type) > -1) {
prev = date_to_string(new Date(prev), type);
next = date_to_string(new Date(next), type);
}
return [prev, next];
}

View File

@ -0,0 +1,16 @@
'use strict';
/**
* get all radio buttons (including `element`) that belong to element's
* radio group
*/
export function get_radiogroup(element) {
if (element.form) {
return Array.prototype.filter.call(
element.form.elements,
radio => radio.type === 'radio' && radio.name === element.name
);
}
return [element];
}

View File

@ -0,0 +1,33 @@
'use strict';
import { inputs } from '../components/types';
/**
* get the element's type in a backwards-compatible way
*/
export default function(element) {
if (element instanceof window.HTMLTextAreaElement) {
return 'textarea';
} else if (element instanceof window.HTMLSelectElement) {
return element.hasAttribute('multiple')? 'select-multiple' : 'select-one';
} else if (element instanceof window.HTMLButtonElement) {
return (element.getAttribute('type') || 'submit').toLowerCase();
} else if (element instanceof window.HTMLInputElement) {
const attr = (element.getAttribute('type') || '').toLowerCase();
if (attr && inputs.indexOf(attr) > -1) {
return attr;
} else {
/* perhaps the DOM has in-depth knowledge. Take that before returning
* 'text'. */
return element.type || 'text';
}
}
return '';
}

View File

@ -0,0 +1,24 @@
'use strict';
import { get_wrapper } from '../components/wrapper';
/**
* filter a form's elements for the ones needing validation prior to
* a submit
*
* Returns an array of form elements.
*/
export function get_validated_elements(form) {
const wrapped_form = get_wrapper(form);
return Array.prototype.filter.call(form.elements, element => {
/* it must have a name (or validating nameless inputs is allowed) */
if (element.getAttribute('name') ||
(wrapped_form && wrapped_form.settings.validateNameless)) {
return true;
}
return false;
});
}

View File

@ -0,0 +1,35 @@
'use strict';
/* For a given date, get the ISO week number
*
* Source: http://stackoverflow.com/a/6117889/113195
*
* Based on information at:
*
* http://www.merlyn.demon.co.uk/weekcalc.htm#WNR
*
* Algorithm is to find nearest thursday, it's year
* is the year of the week number. Then get weeks
* between that date and the first day of that year.
*
* Note that dates in one year can be weeks of previous
* or next year, overlap is up to 3 days.
*
* e.g. 2014/12/29 is Monday in week 1 of 2015
* 2012/1/1 is Sunday in week 52 of 2011
*/
export default function(d) {
/* Copy date so don't modify original */
d = new Date(+d);
d.setUTCHours(0, 0, 0);
/* Set to nearest Thursday: current date + 4 - current day number
* Make Sunday's day number 7 */
d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay()||7));
/* Get first day of year */
const yearStart = new Date(d.getUTCFullYear(),0,1);
/* Calculate full weeks to nearest Thursday */
const weekNo = Math.ceil(( ( (d - yearStart) / 86400000) + 1)/7);
/* Return array of year and week number */
return [d.getUTCFullYear(), weekNo];
}

View File

@ -0,0 +1,17 @@
'use strict';
export default function(element) {
return (
element instanceof window.HTMLButtonElement ||
element instanceof window.HTMLInputElement ||
element instanceof window.HTMLSelectElement ||
element instanceof window.HTMLTextAreaElement ||
element instanceof window.HTMLFieldSetElement ||
element === window.HTMLButtonElement.prototype ||
element === window.HTMLInputElement.prototype ||
element === window.HTMLSelectElement.prototype ||
element === window.HTMLTextAreaElement.prototype ||
element === window.HTMLFieldSetElement.prototype
);
}

View File

@ -0,0 +1,92 @@
'use strict';
import { do_filter } from '../components/hooks';
import { validation_candidates, non_inputs } from '../components/types';
import { get_wrapper } from '../components/wrapper';
import get_type from '../tools/get_type';
/**
* check if an element should be ignored due to any of its parents
*
* Checks <fieldset disabled> and <datalist>.
*/
function is_in_disallowed_parent(element) {
let p = element.parentNode;
while (p && p.nodeType === 1) {
if (p instanceof window.HTMLFieldSetElement &&
p.hasAttribute('disabled')) {
/* quick return, if it's a child of a disabled fieldset */
return true;
} else if (p.nodeName.toUpperCase() === 'DATALIST') {
/* quick return, if it's a child of a datalist
* Do not use HTMLDataListElement to support older browsers,
* too.
* @see https://html.spec.whatwg.org/multipage/forms.html#the-datalist-element:barred-from-constraint-validation
*/
return true;
} else if (p === element.form) {
/* the outer boundary. We can stop looking for relevant elements. */
break;
}
p = p.parentNode;
}
return false;
}
/**
* check if an element is a candidate for constraint validation
*
* @see https://html.spec.whatwg.org/multipage/forms.html#barred-from-constraint-validation
*/
export default function(element) {
/* allow a shortcut via filters, e.g. to validate type=hidden fields */
const filtered = do_filter('is_validation_candidate', null, element);
if (filtered !== null) {
return !! filtered;
}
/* it must be any of those elements */
if (element instanceof window.HTMLSelectElement
||
element instanceof window.HTMLTextAreaElement
||
element instanceof window.HTMLButtonElement
||
element instanceof window.HTMLInputElement) {
const type = get_type(element);
/* its type must be in the whitelist */
if (non_inputs.indexOf(type) > -1 ||
validation_candidates.indexOf(type) > -1) {
/* it mustn't be disabled or readonly */
if (! element.hasAttribute('disabled') &&
! element.hasAttribute('readonly')) {
const wrapped_form = get_wrapper(element);
if (
/* the parent form doesn't allow non-standard "novalidate" attributes... */
(wrapped_form && ! wrapped_form.settings.novalidateOnElements) ||
/* ...or it doesn't have such an attribute/property */
(! element.hasAttribute('novalidate') && ! element.noValidate)
) {
/* it isn't part of a <fieldset disabled> */
if (! is_in_disallowed_parent(element)) {
/* then it's a candidate */
return true;
}
}
}
}
}
/* this is no HTML5 validation candidate... */
return false;
}

View File

@ -0,0 +1,21 @@
'use strict';
/**
* mark an object with a '__hyperform=true' property
*
* We use this to distinguish our properties from the native ones. Usage:
* js> mark(obj);
* js> assert(obj.__hyperform === true)
*/
export default function(obj) {
if (['object', 'function'].indexOf(typeof obj) > -1) {
delete obj.__hyperform;
Object.defineProperty(obj, '__hyperform', {
configurable: true,
enumerable: false,
value: true,
});
}
return obj;
}

View File

@ -0,0 +1,13 @@
'use strict';
/* shim layer for the Element.matches method */
const ep = window.Element.prototype;
const native_matches = ep.matches ||
ep.matchesSelector ||
ep.msMatchesSelector ||
ep.webkitMatchesSelector;
export default function(element, selector) {
return native_matches.call(element, selector);
}

View File

@ -0,0 +1,70 @@
'use strict';
import install_property from './property_installer';
import is_field from './is_field';
import mark from './mark';
import checkValidity from '../polyfills/checkValidity';
import reportValidity from '../polyfills/reportValidity';
import setCustomValidity from '../polyfills/setCustomValidity';
import stepDown from '../polyfills/stepDown';
import stepUp from '../polyfills/stepUp';
import validationMessage from '../polyfills/validationMessage';
import ValidityState from '../polyfills/validityState';
import valueAsDate from '../polyfills/valueAsDate';
import valueAsNumber from '../polyfills/valueAsNumber';
import willValidate from '../polyfills/willValidate';
import { install_properties } from '../polyfills/properties';
const polyfills = {
checkValidity: {
value: mark(function() { return checkValidity(this); }),
},
reportValidity: {
value: mark(function() { return reportValidity(this); }),
},
setCustomValidity: {
value: mark(function (msg) { return setCustomValidity(this, msg); }),
},
stepDown: {
value: mark(function(n=1) { return stepDown(this, n); }),
},
stepUp: {
value: mark(function(n=1) { return stepUp(this, n); }),
},
validationMessage: {
get: mark(function() { return validationMessage(this); }),
},
validity: {
get: mark(function() { return ValidityState(this); }),
},
valueAsDate: {
get: mark(function() { return valueAsDate(this); }),
set: mark(function(value) { valueAsDate(this, value); }),
},
valueAsNumber: {
get: mark(function() { return valueAsNumber(this); }),
set: mark(function(value) { valueAsNumber(this, value); }),
},
willValidate: {
get: mark(function() { return willValidate(this); }),
},
};
export default function(element) {
if (is_field(element)) {
for (let prop in polyfills) {
install_property(element, prop, polyfills[prop]);
}
install_properties(element);
} else if (element instanceof window.HTMLFormElement ||
element === window.HTMLFormElement.prototype) {
install_property(element, 'checkValidity', polyfills.checkValidity);
install_property(element, 'reportValidity', polyfills.reportValidity);
}
}

View File

@ -0,0 +1,29 @@
'use strict';
import is_field from './is_field';
import uninstall_property from './property_uninstaller';
import { uninstall_properties } from '../polyfills/properties';
export default function(element) {
if (is_field(element)) {
uninstall_property(element, 'checkValidity');
uninstall_property(element, 'reportValidity');
uninstall_property(element, 'setCustomValidity');
uninstall_property(element, 'stepDown');
uninstall_property(element, 'stepUp');
uninstall_property(element, 'validationMessage');
uninstall_property(element, 'validity');
uninstall_property(element, 'valueAsDate');
uninstall_property(element, 'valueAsNumber');
uninstall_property(element, 'willValidate');
uninstall_properties(element);
} else if (element instanceof window.HTMLFormElement) {
uninstall_property(element, 'checkValidity');
uninstall_property(element, 'reportValidity');
}
}

View File

@ -0,0 +1,63 @@
'use strict';
import { get_wrapper } from '../components/wrapper';
/**
* add `property` to an element
*
* ATTENTION! This function will search for an equally named property on the
* *prototype* of an element, if element is a concrete DOM node. Do not use
* it as general-purpose property installer.
*
* js> installer(element, 'foo', { value: 'bar' });
* js> assert(element.foo === 'bar');
*/
export default function(element, property, descriptor) {
descriptor.configurable = true;
descriptor.enumerable = true;
if ('value' in descriptor) {
descriptor.writable = true;
}
/* on concrete instances, i.e., <input> elements, the naive lookup
* yields undefined. We have to look on its prototype then. On elements
* like the actual HTMLInputElement object the first line works. */
let original_descriptor = Object.getOwnPropertyDescriptor(element, property);
if (original_descriptor === undefined) {
original_descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), property);
}
if (original_descriptor) {
if (original_descriptor.configurable === false) {
/* Safari <= 9 and PhantomJS will end up here :-( Nothing to do except
* warning */
const wrapper = get_wrapper(element);
if (wrapper && wrapper.settings.debug) {
/* global console */
console.log('[hyperform] cannot install custom property '+property);
}
return false;
}
/* we already installed that property... */
if ((original_descriptor.get && original_descriptor.get.__hyperform) ||
(original_descriptor.value && original_descriptor.value.__hyperform)) {
return;
}
/* publish existing property under new name, if it's not from us */
Object.defineProperty(
element,
'_original_'+property,
original_descriptor
);
}
delete element[property];
Object.defineProperty(element, property, descriptor);
return true;
}

View File

@ -0,0 +1,31 @@
'use strict';
import { get_wrapper } from '../components/wrapper';
/**
* remove `property` from element and restore _original_property, if present
*/
export default function(element, property) {
try {
delete element[property];
} catch (e) {
/* Safari <= 9 and PhantomJS will end up here :-( Nothing to do except
* warning */
const wrapper = get_wrapper(element);
if (wrapper && wrapper.settings.debug) {
/* global console */
console.log('[hyperform] cannot uninstall custom property '+property);
}
return false;
}
const original_descriptor = Object.getOwnPropertyDescriptor(element,
'_original_'+property);
if (original_descriptor) {
Object.defineProperty(element, property, original_descriptor);
}
}

View File

@ -0,0 +1,23 @@
'use strict';
import { call_hook } from '../components/hooks.js';
/**
* return either the data of a hook call or the result of action, if the
* former is undefined
*
* @return function a function wrapper around action
*/
export default function(hook, action) {
return function() {
const data = call_hook(hook, Array.prototype.slice.call(arguments));
if (data !== undefined) {
return data;
}
return action.apply(this, arguments);
};
}

View File

@ -0,0 +1,34 @@
'use strict';
export default function(str, ...args) {
const args_length = args.length;
var global_index = 0;
return str.replace(/%([0-9]+\$)?([sl])/g, (match, position, type) => {
var local_index = global_index;
if (position) {
local_index = Number(position.replace(/\$$/, '')) - 1;
}
global_index += 1;
var arg = '';
if (args_length > local_index) {
arg = args[local_index];
}
if (arg instanceof Date ||
typeof arg === 'number' ||
arg instanceof Number) {
/* try getting a localized representation of dates and numbers, if the
* browser supports this */
if (type === 'l') {
arg = (arg.toLocaleString || arg.toString).call(arg);
} else {
arg = arg.toString();
}
}
return arg;
});
}

View File

@ -0,0 +1,49 @@
'use strict';
import get_date_from_week from './get_date_from_week';
/**
* calculate a date from a string according to HTML5
*/
export default function(string, element_type) {
let date;
switch (element_type) {
case 'datetime':
if (! /^([0-9]{4})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9])(?::([0-5][0-9])(?:\.([0-9]{1,3}))?)?$/.test(string)) {
return null;
}
date = new Date(string+'z');
return isNaN(date.valueOf())? null : date;
case 'date':
if (! /^([0-9]{4})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/.test(string)) {
return null;
}
date = new Date(string);
return isNaN(date.valueOf())? null : date;
case 'month':
if (! /^([0-9]{4})-(0[1-9]|1[012])$/.test(string)) {
return null;
}
date = new Date(string);
return isNaN(date.valueOf())? null : date;
case 'week':
if (! /^([0-9]{4})-W(0[1-9]|[1234][0-9]|5[0-3])$/.test(string)) {
return null;
}
return get_date_from_week(Number(RegExp.$2), Number(RegExp.$1));
case 'time':
if (! /^([01][0-9]|2[0-3]):([0-5][0-9])(?::([0-5][0-9])(?:\.([0-9]{1,3}))?)?$/.test(string)) {
return null;
}
date = new Date('1970-01-01T'+string+'z');
return date;
}
return null;
}

View File

@ -0,0 +1,17 @@
'use strict';
import string_to_date from './string_to_date';
/**
* calculate a number from a string according to HTML5
*/
export default function(string, element_type) {
const rval = string_to_date(string, element_type);
if (rval !== null) {
return +rval;
}
/* not parseFloat, because we want NaN for invalid values like "1.2xxy" */
return Number(string);
}

View File

@ -0,0 +1,60 @@
'use strict';
/* the following code is borrowed from the WebComponents project, licensed
* under the BSD license. Source:
* <https://github.com/webcomponents/webcomponentsjs/blob/5283db1459fa2323e5bfc8b9b5cc1753ed85e3d0/src/WebComponents/dom.js#L53-L78>
*/
// defaultPrevented is broken in IE.
// https://connect.microsoft.com/IE/feedback/details/790389/event-defaultprevented-returns-false-after-preventdefault-was-called
const workingDefaultPrevented = (function() {
const e = document.createEvent('Event');
e.initEvent('foo', true, true);
e.preventDefault();
return e.defaultPrevented;
})();
if (!workingDefaultPrevented) {
const origPreventDefault = window.Event.prototype.preventDefault;
window.Event.prototype.preventDefault = function() {
if (!this.cancelable) {
return;
}
origPreventDefault.call(this);
Object.defineProperty(this, 'defaultPrevented', {
get: function() {
return true;
},
configurable: true
});
};
}
/* end of borrowed code */
export function create_event(name, { bubbles=true, cancelable=false, }={}) {
const event = document.createEvent('Event');
event.initEvent(name, bubbles, cancelable);
return event;
}
export default function(element, event, {
bubbles=true,
cancelable=false,
}={}, payload={}) {
if (! (event instanceof window.Event)) {
event = create_event(event, { bubbles, cancelable });
}
for (let key in payload) {
if (payload.hasOwnProperty(key)) {
event[key] = payload[key];
}
}
element.dispatchEvent(event);
return event;
}

View File

@ -0,0 +1,14 @@
'use strict';
const ws_on_start_or_end = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;
/**
* trim a string of whitespace
*
* We don't use String.trim() to remove the need to polyfill it.
*/
export default function(str) {
return str.replace(ws_on_start_or_end, '');
}

View File

@ -0,0 +1,13 @@
'use strict';
/**
* patch String.length to account for non-BMP characters
*
* @see https://mathiasbynens.be/notes/javascript-unicode
* We do not use the simple [...str].length, because it needs a ton of
* polyfills in older browsers.
*/
export default function(str) {
return str.match(/[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g).length;
}

View File

@ -0,0 +1,325 @@
'use strict';
/**
* Implement constraint checking functionality defined in the HTML5 standard
*
* @see https://html.spec.whatwg.org/multipage/forms.html#dom-cva-validity
* @return bool true if the test fails [!], false otherwise
*/
import format_date from './format_date';
import get_next_valid from './get_next_valid';
import get_type from './get_type';
import sprintf from './sprintf';
import string_to_number from './string_to_number';
import string_to_date from './string_to_date';
import unicode_string_length from './unicode_string_length';
import custom_messages from '../components/custom_messages';
import _ from '../components/localization';
import { message_store } from '../components/message_store';
import CustomValidatorRegistry from '../components/registry';
import { get_wrapper } from '../components/wrapper';
import test_bad_input from '../validators/bad_input';
import test_max from '../validators/max';
import test_maxlength from '../validators/maxlength';
import test_min from '../validators/min';
import test_minlength from '../validators/minlength';
import test_pattern from '../validators/pattern';
import test_required from '../validators/required';
import test_step from '../validators/step';
import test_type from '../validators/type';
/**
* boilerplate function for all tests but customError
*/
function check(test, react) {
return element => {
const invalid = ! test(element);
if (invalid) {
react(element);
}
return invalid;
};
}
/**
* create a common function to set error messages
*/
function set_msg(element, msgtype, _default) {
message_store.set(element, custom_messages.get(element, msgtype, _default));
}
const badInput = check(test_bad_input, element => set_msg(element, 'badInput',
_('Please match the requested type.')));
function customError(element) {
/* prevent infinite loops when the custom validators call setCustomValidity(),
* which in turn calls this code again. We check, if there is an already set
* custom validity message there. */
if (element.__hf_custom_validation_running) {
const msg = message_store.get(element);
return (msg && msg.is_custom);
}
/* check, if there are custom validators in the registry, and call
* them. */
const custom_validators = CustomValidatorRegistry.get(element);
const cvl = custom_validators.length;
var valid = true;
if (cvl) {
element.__hf_custom_validation_running = true;
for (let i = 0; i < cvl; i++) {
const result = custom_validators[i](element);
if (result !== undefined && ! result) {
valid = false;
/* break on first invalid response */
break;
}
}
delete(element.__hf_custom_validation_running);
}
/* check, if there are other validity messages already */
if (valid) {
const msg = message_store.get(element);
valid = ! (msg.toString() && ('is_custom' in msg));
}
return ! valid;
}
const patternMismatch = check(test_pattern, element => {
set_msg(element, 'patternMismatch',
element.title?
sprintf(_('PatternMismatchWithTitle'), element.title)
:
_('PatternMismatch')
);
});
/**
* TODO: when rangeOverflow and rangeUnderflow are both called directly and
* successful, the inRange and outOfRange classes won't get removed, unless
* element.validityState.valid is queried, too.
*/
const rangeOverflow = check(test_max, element => {
const type = get_type(element);
const wrapper = get_wrapper(element);
const outOfRangeClass = wrapper && wrapper.settings.classes.outOfRange || 'hf-out-of-range';
const inRangeClass = wrapper && wrapper.settings.classes.inRange || 'hf-in-range';
let msg;
switch (type) {
case 'date':
case 'datetime':
case 'datetime-local':
msg = sprintf(_('DateRangeOverflow'),
format_date(string_to_date(element.getAttribute('max'), type), type));
break;
case 'time':
msg = sprintf(_('TimeRangeOverflow'),
format_date(string_to_date(element.getAttribute('max'), type), type));
break;
// case 'number':
default:
msg = sprintf(_('NumberRangeOverflow'),
string_to_number(element.getAttribute('max'), type));
break;
}
set_msg(element, 'rangeOverflow', msg);
element.classList.add(outOfRangeClass);
element.classList.remove(inRangeClass);
});
const rangeUnderflow = check(test_min, element => {
const type = get_type(element);
const wrapper = get_wrapper(element);
const outOfRangeClass = wrapper && wrapper.settings.classes.outOfRange || 'hf-out-of-range';
const inRangeClass = wrapper && wrapper.settings.classes.inRange || 'hf-in-range';
let msg;
switch (type) {
case 'date':
case 'datetime':
case 'datetime-local':
msg = sprintf(_('DateRangeUnderflow'),
format_date(string_to_date(element.getAttribute('min'), type), type));
break;
case 'time':
msg = sprintf(_('TimeRangeUnderflow'),
format_date(string_to_date(element.getAttribute('min'), type), type));
break;
// case 'number':
default:
msg = sprintf(_('NumberRangeUnderflow'),
string_to_number(element.getAttribute('min'), type));
break;
}
set_msg(element, 'rangeUnderflow', msg);
element.classList.add(outOfRangeClass);
element.classList.remove(inRangeClass);
});
const stepMismatch = check(test_step, element => {
const list = get_next_valid(element);
const min = list[0];
const max = list[1];
let sole = false;
let msg;
if (min === null) {
sole = max;
} else if (max === null) {
sole = min;
}
if (sole !== false) {
msg = sprintf(_('StepMismatchOneValue'), sole);
} else {
msg = sprintf(_('StepMismatch'), min, max);
}
set_msg(element, 'stepMismatch', msg);
});
const tooLong = check(test_maxlength, element => {
set_msg(element, 'tooLong',
sprintf(_('TextTooLong'), element.getAttribute('maxlength'),
unicode_string_length(element.value)));
});
const tooShort = check(test_minlength, element => {
set_msg(element, 'tooShort',
sprintf(_('Please lengthen this text to %l characters or more (you are currently using %l characters).'),
element.getAttribute('minlength'),
unicode_string_length(element.value)));
});
const typeMismatch = check(test_type, element => {
let msg = _('Please use the appropriate format.');
const type = get_type(element);
if (type === 'email') {
if (element.hasAttribute('multiple')) {
msg = _('Please enter a comma separated list of email addresses.');
} else {
msg = _('InvalidEmail');
}
} else if (type === 'url') {
msg = _('InvalidURL');
} else if (type === 'file') {
msg = _('Please select a file of the correct type.');
}
set_msg(element, 'typeMismatch', msg);
});
const valueMissing = check(test_required, element => {
let msg = _('ValueMissing');
const type = get_type(element);
if (type === 'checkbox') {
msg = _('CheckboxMissing');
} else if (type === 'radio') {
msg = _('RadioMissing');
} else if (type === 'file') {
if (element.hasAttribute('multiple')) {
msg = _('Please select one or more files.');
} else {
msg = _('FileMissing');
}
} else if (element instanceof window.HTMLSelectElement) {
msg = _('SelectMissing');
}
set_msg(element, 'valueMissing', msg);
});
/**
* the "valid" property calls all other validity checkers and returns true,
* if all those return false.
*
* This is the major access point for _all_ other API methods, namely
* (check|report)Validity().
*/
const valid = element => {
const wrapper = get_wrapper(element);
const validClass = wrapper && wrapper.settings.classes.valid || 'hf-valid';
const invalidClass = wrapper && wrapper.settings.classes.invalid || 'hf-invalid';
const userInvalidClass = wrapper && wrapper.settings.classes.userInvalid || 'hf-user-invalid';
const userValidClass = wrapper && wrapper.settings.classes.userValid || 'hf-user-valid';
const inRangeClass = wrapper && wrapper.settings.classes.inRange || 'hf-in-range';
const outOfRangeClass = wrapper && wrapper.settings.classes.outOfRange || 'hf-out-of-range';
const validatedClass = wrapper && wrapper.settings.classes.validated || 'hf-validated';
element.classList.add(validatedClass);
for (let checker of [badInput, customError, patternMismatch, rangeOverflow,
rangeUnderflow, stepMismatch, tooLong, tooShort,
typeMismatch, valueMissing]) {
if (checker(element)) {
element.classList.add(invalidClass);
element.classList.remove(validClass);
element.classList.remove(userValidClass);
if ((
(element.type === 'checkbox' || element.type === 'radio') &&
element.checked !== element.defaultChecked
) ||
/* the following test is trivially false for checkboxes/radios */
element.value !== element.defaultValue) {
element.classList.add(userInvalidClass);
} else {
element.classList.remove(userInvalidClass);
}
element.setAttribute('aria-invalid', 'true');
return false;
}
}
message_store.delete(element);
element.classList.remove(invalidClass);
element.classList.remove(userInvalidClass);
element.classList.remove(outOfRangeClass);
element.classList.add(validClass);
element.classList.add(inRangeClass);
if (element.value !== element.defaultValue) {
element.classList.add(userValidClass);
} else {
element.classList.remove(userValidClass);
}
element.setAttribute('aria-invalid', 'false');
return true;
};
export default {
badInput,
customError,
patternMismatch,
rangeOverflow,
rangeUnderflow,
stepMismatch,
tooLong,
tooShort,
typeMismatch,
valueMissing,
valid,
};

View File

@ -0,0 +1,63 @@
'use strict';
import get_type from '../tools/get_type';
import string_to_date from '../tools/string_to_date';
import { input_checked } from '../components/types';
/**
* test whether the element suffers from bad input
*/
export default function(element) {
const type = get_type(element);
if (input_checked.indexOf(type) === -1) {
/* we're not interested, thanks! */
return true;
}
/* the browser hides some bad input from the DOM, e.g. malformed numbers,
* email addresses with invalid punycode representation, ... We try to resort
* to the original method here. The assumption is, that a browser hiding
* bad input will hopefully also always support a proper
* ValidityState.badInput */
if (! element.value) {
if ('_original_validity' in element &&
! element._original_validity.__hyperform) {
return ! element._original_validity.badInput;
}
/* no value and no original badInput: Assume all's right. */
return true;
}
var result = true;
switch (type) {
case 'color':
result = /^#[a-f0-9]{6}$/.test(element.value);
break;
case 'number':
case 'range':
result = ! isNaN(Number(element.value));
break;
case 'datetime':
case 'date':
case 'month':
case 'week':
case 'time':
result = string_to_date(element.value, type) !== null;
break;
case 'datetime-local':
result = /^([0-9]{4,})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9])(?::([0-5][0-9])(?:\.([0-9]{1,3}))?)?$/.test(element.value);
break;
case 'tel':
/* spec says No! Phone numbers can have all kinds of formats, so this
* is expected to be a free-text field. */
// TODO we could allow a setting 'phone_regex' to be evaluated here.
break;
case 'email':
break;
}
return result;
}

View File

@ -0,0 +1,36 @@
'use strict';
import get_type from '../tools/get_type';
import { dates } from '../components/types';
import string_to_date from '../tools/string_to_date';
/**
* test the max attribute
*
* we use Number() instead of parseFloat(), because an invalid attribute
* value like "123abc" should result in an error.
*/
export default function(element) {
const type = get_type(element);
if (! element.value || ! element.hasAttribute('max')) {
/* we're not responsible here */
return true;
}
let value, max;
if (dates.indexOf(type) > -1) {
value = string_to_date(element.value, type);
value = value === null? NaN : +value;
max = string_to_date(element.getAttribute('max'), type);
max = max === null? NaN : +max;
} else {
value = Number(element.value);
max = Number(element.getAttribute('max'));
}
/* we cannot validate invalid values and trust on badInput, if isNaN(value) */
return (isNaN(max) || isNaN(value) || value <= max);
}

View File

@ -0,0 +1,35 @@
'use strict';
import get_type from '../tools/get_type';
import unicode_string_length from '../tools/unicode_string_length';
import { text as text_types } from '../components/types';
/**
* test the maxlength attribute
*/
export default function(element) {
if (
! element.value
||
text_types.indexOf(get_type(element)) === -1
||
! element.hasAttribute('maxlength')
||
! element.getAttribute('maxlength') // catch maxlength=""
) {
return true;
}
const maxlength = parseInt(element.getAttribute('maxlength'), 10);
/* check, if the maxlength value is usable at all.
* We allow maxlength === 0 to basically disable input (Firefox does, too).
*/
if (isNaN(maxlength) || maxlength < 0) {
return true;
}
return unicode_string_length(element.value) <= maxlength;
}

View File

@ -0,0 +1,36 @@
'use strict';
import get_type from '../tools/get_type';
import { dates } from '../components/types';
import string_to_date from '../tools/string_to_date';
/**
* test the min attribute
*
* we use Number() instead of parseFloat(), because an invalid attribute
* value like "123abc" should result in an error.
*/
export default function(element) {
const type = get_type(element);
if (! element.value || ! element.hasAttribute('min')) {
/* we're not responsible here */
return true;
}
let value, min;
if (dates.indexOf(type) > -1) {
value = string_to_date(element.value, type);
value = value === null? NaN : +value;
min = string_to_date(element.getAttribute('min'), type);
min = min === null? NaN : +min;
} else {
value = Number(element.value);
min = Number(element.getAttribute('min'));
}
/* we cannot validate invalid values and trust on badInput, if isNaN(value) */
return (isNaN(min) || isNaN(value) || value >= min);
}

View File

@ -0,0 +1,33 @@
'use strict';
import get_type from '../tools/get_type';
import unicode_string_length from '../tools/unicode_string_length';
import { text as text_types } from '../components/types';
/**
* test the minlength attribute
*/
export default function(element) {
if (
! element.value
||
text_types.indexOf(get_type(element)) === -1
||
! element.hasAttribute('minlength')
||
! element.getAttribute('minlength') // catch minlength=""
) {
return true;
}
const minlength = parseInt(element.getAttribute('minlength'), 10);
/* check, if the minlength value is usable at all. */
if (isNaN(minlength) || minlength < 0) {
return true;
}
return unicode_string_length(element.value) >= minlength;
}

View File

@ -0,0 +1,15 @@
'use strict';
/**
* test the pattern attribute
*/
export default function(element) {
return (
! element.value
||
! element.hasAttribute('pattern')
||
(new RegExp('^(?:'+ element.getAttribute('pattern') +')$')).test(element.value)
);
}

View File

@ -0,0 +1,71 @@
'use strict';
import { get_radiogroup } from '../tools/get_radiogroup';
function has_submittable_option(select) {
/* Definition of the placeholder label option:
* https://www.w3.org/TR/html5/sec-forms.html#element-attrdef-select-required
* Being required (the first constraint in the spec) is trivially true, since
* this function is only called for such selects.
*/
const has_placeholder_option = (
! select.multiple &&
select.size <= 1 &&
select.options.length > 0 &&
select.options[0].parentNode == select &&
select.options[0].value === ''
);
return (
/* anything selected at all? That's redundant with the .some() call below,
* but more performant in the most probable error case. */
select.selectedIndex > -1 &&
Array.prototype.some.call(
select.options,
option => {
return (
/* it isn't the placeholder option */
(! has_placeholder_option || option.index !== 0) &&
/* it isn't disabled */
! option.disabled &&
/* and it is, in fact, selected */
option.selected);
}
)
);
}
/**
* test the required attribute
*/
export default function(element) {
if (element.type === 'radio') {
/* the happy (and quick) path for radios: */
if (element.hasAttribute('required') && element.checked) {
return true;
}
const radiogroup = get_radiogroup(element);
/* if any radio in the group is required, we need any (not necessarily the
* same) radio to be checked */
if (radiogroup.some(radio => radio.hasAttribute('required'))) {
return radiogroup.some(radio => radio.checked);
}
/* not required, validation passes */
return true;
}
if (! element.hasAttribute('required')) {
/* nothing to do */
return true;
}
if (element instanceof window.HTMLSelectElement) {
return has_submittable_option(element);
}
return (element.type === 'checkbox')? element.checked : (!! element.value);
}

View File

@ -0,0 +1,66 @@
'use strict';
import get_type from '../tools/get_type';
import { numbers } from '../components/types';
import { default_step, step_scale_factor, default_step_base } from '../components/step_defaults';
import string_to_number from '../tools/string_to_number';
/**
* test the step attribute
*/
export default function(element) {
const type = get_type(element);
if (! element.value ||
numbers.indexOf(type) === -1 ||
(element.getAttribute('step') || '').toLowerCase() === 'any') {
/* we're not responsible here. Note: If no step attribute is given, we
* need to validate against the default step as per spec. */
return true;
}
let step = element.getAttribute('step');
if (step) {
step = string_to_number(step, type);
} else {
step = default_step[type] || 1;
}
if (step <= 0 || isNaN(step)) {
/* error in specified "step". We cannot validate against it, so the value
* is true. */
return true;
}
const scale = step_scale_factor[type] || 1;
let value = string_to_number(element.value, type);
let min = string_to_number(element.getAttribute('min') ||
element.getAttribute('value') || '', type);
if (isNaN(value)) {
/* we cannot compare an invalid value and trust that the badInput validator
* takes over from here */
return true;
}
if (isNaN(min)) {
min = default_step_base[type] || 0;
}
if (type === 'month') {
/* type=month has month-wide steps. See
* https://html.spec.whatwg.org/multipage/forms.html#month-state-%28type=month%29
*/
min = (new Date(min)).getUTCFullYear() * 12 + (new Date(min)).getUTCMonth();
value = (new Date(value)).getUTCFullYear() * 12 + (new Date(value)).getUTCMonth();
}
const result = Math.abs(min - value) % (step * scale);
return (result < 0.00000001 ||
/* crappy floating-point arithmetics! */
result > (step * scale) - 0.00000001);
}

View File

@ -0,0 +1,105 @@
'use strict';
import comma_split from '../tools/comma_split';
import get_type from '../tools/get_type';
import trim from '../tools/trim';
import { type_checked } from '../components/types';
/* we use a dummy <a> where we set the href to test URL validity
* The definition is out of the "global" scope so that JSDOM can be instantiated
* after loading Hyperform for tests.
*/
var url_canary;
/* see https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address */
const email_pattern = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
/**
* test the type-inherent syntax
*/
export default function(element) {
const type = get_type(element);
if ((type !== 'file' && ! element.value) ||
(type !== 'file' && type_checked.indexOf(type) === -1)) {
/* we're not responsible for this element */
return true;
}
var is_valid = true;
switch (type) {
case 'url': {
if (! url_canary) {
url_canary = document.createElement('a');
}
const value = trim(element.value);
url_canary.href = value;
is_valid = (url_canary.href === value ||
url_canary.href === value+'/');
break;
}
case 'email':
if (element.hasAttribute('multiple')) {
is_valid = comma_split(element.value)
.every(value => email_pattern.test(value));
} else {
is_valid = email_pattern.test(trim(element.value));
}
break;
case 'file':
if ('files' in element && element.files.length &&
element.hasAttribute('accept')) {
const patterns = comma_split(element.getAttribute('accept'))
.map(pattern => {
if (/^(audio|video|image)\/\*$/.test(pattern)) {
pattern = new RegExp('^'+RegExp.$1+'/.+$');
}
return pattern;
});
if (! patterns.length) {
break;
}
fileloop:
for (let i = 0; i < element.files.length; i++) {
/* we need to match a whitelist, so pre-set with false */
let file_valid = false;
patternloop:
for (let j = 0; j < patterns.length; j++) {
const file = element.files[i];
const pattern = patterns[j];
let fileprop = file.type;
if (typeof pattern === 'string' && pattern.substr(0, 1) === '.') {
if (file.name.search('.') === -1) {
/* no match with any file ending */
continue patternloop;
}
fileprop = file.name.substr(file.name.lastIndexOf('.'));
}
if (fileprop.search(pattern) === 0) {
/* we found one match and can quit looking */
file_valid = true;
break patternloop;
}
}
if (! file_valid) {
is_valid = false;
break fileloop;
}
}
}
}
return is_valid;
}

View File

@ -0,0 +1,2 @@
'use strict';
export default '0.12.0';

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>Mocha</title>
</head>
<body>
<script src="weakmap.min.js"></script>
<script src="classList.min.js"></script>
<script src="../../dist/hyperform.js"></script>
</body>
</html>

View File

@ -0,0 +1,2 @@
/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */
if("document" in self){if(!("classList" in document.createElement("_"))){(function(j){"use strict";if(!("Element" in j)){return}var a="classList",f="prototype",m=j.Element[f],b=Object,k=String[f].trim||function(){return this.replace(/^\s+|\s+$/g,"")},c=Array[f].indexOf||function(q){var p=0,o=this.length;for(;p<o;p++){if(p in this&&this[p]===q){return p}}return -1},n=function(o,p){this.name=o;this.code=DOMException[o];this.message=p},g=function(p,o){if(o===""){throw new n("SYNTAX_ERR","An invalid or illegal string was specified")}if(/\s/.test(o)){throw new n("INVALID_CHARACTER_ERR","String contains an invalid character")}return c.call(p,o)},d=function(s){var r=k.call(s.getAttribute("class")||""),q=r?r.split(/\s+/):[],p=0,o=q.length;for(;p<o;p++){this.push(q[p])}this._updateClassName=function(){s.setAttribute("class",this.toString())}},e=d[f]=[],i=function(){return new d(this)};n[f]=Error[f];e.item=function(o){return this[o]||null};e.contains=function(o){o+="";return g(this,o)!==-1};e.add=function(){var s=arguments,r=0,p=s.length,q,o=false;do{q=s[r]+"";if(g(this,q)===-1){this.push(q);o=true}}while(++r<p);if(o){this._updateClassName()}};e.remove=function(){var t=arguments,s=0,p=t.length,r,o=false,q;do{r=t[s]+"";q=g(this,r);while(q!==-1){this.splice(q,1);o=true;q=g(this,r)}}while(++s<p);if(o){this._updateClassName()}};e.toggle=function(p,q){p+="";var o=this.contains(p),r=o?q!==true&&"remove":q!==false&&"add";if(r){this[r](p)}if(q===true||q===false){return q}else{return !o}};e.toString=function(){return this.join(" ")};if(b.defineProperty){var l={get:i,enumerable:true,configurable:true};try{b.defineProperty(m,a,l)}catch(h){if(h.number===-2146823252){l.enumerable=false;b.defineProperty(m,a,l)}}}else{if(b[f].__defineGetter__){m.__defineGetter__(a,i)}}}(self))}else{(function(){var b=document.createElement("_");b.classList.add("c1","c2");if(!b.classList.contains("c2")){var c=function(e){var d=DOMTokenList.prototype[e];DOMTokenList.prototype[e]=function(h){var g,f=arguments.length;for(g=0;g<f;g++){h=arguments[g];d.call(this,h)}}};c("add");c("remove")}b.classList.toggle("c3",false);if(b.classList.contains("c3")){var a=DOMTokenList.prototype.toggle;DOMTokenList.prototype.toggle=function(d,e){if(1 in arguments&&!this.contains(d)===!e){return e}else{return a.call(this,d)}}}b=null}())}};

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>Mocha</title>
<link rel="stylesheet" href="../../node_modules/mocha/mocha.css">
</head>
<body>
<div id="mocha"></div>
<script src="weakmap.min.js"></script>
<script src="classList.min.js"></script>
<script src="../../node_modules/mocha/mocha.js"></script>
<script src="../../dist/hyperform.js"></script>
<script>mocha.setup('bdd')</script>
<script src="test.regressions.js"></script>
<script src="test.functional.js"></script>
<script>mocha.run()</script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More