JS日期级联组件代码分析及demo
最近研究下JS日期级联效果 感觉还不错,然后看了下kissy也正好有这么一个组件,也看了下源码,写的还不错,通过google最早是在2011年 淘宝的虎牙(花名)用原审JS写了一个(貌似据说是从YUI那边重构下的) 具体的可以看他的 博客园 , 感觉kissy组件源码 思路也是和YUI类似 所以我今天的基本思路也和他们的一样 只是通过自己分析下及用自己的方式包装下。
基本原理
1.传参中有 '年份下拉框dom节点', '月份下拉框dom节点', '天数下拉框dom节点', "开始日期","结束日期","默认日期"配置项
1.如果开始传参日期为空 那么默认是从"1900-01-01"开始
2.如果"结束日期为空" 那么默认结束日期为当前的时间。
3. 如果默认日期为空 那么默认日期默认为当前的时间。
2. 月份对应的天数可以直接写死 如:_dayInMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] ; 分别为1月份到12月份的各个月份的默认天数,当然还有2月份闰年29天的情况 待会在代码中会有判断的。
3. 分别渲染出年份区间,月份区间,及相应的天数。(如果有默认的日期的话 且默认日期大于或者等于开始日期 且小于或者等于结束日期的话) 那么页面加载的时候 显示默认日期。
4. 绑定change事件 当切换到不同年份的时候 月份和天数也要分别渲染出来。
基本配置项如下:
| nodeYear |
'#year', 年份下拉框dom节点 |
| nodeMonth | '#month', 月份下拉框dom节点 |
| nodeDay | '#day', 日期下拉框dom节点 |
| dateStart | '', 开始日期(为空 默认日期从1900-01-01开始) |
| dateEnd | '', 结束日期(可选 默认为空就为当前时间) |
| dateDefault | '' 默认日期(可选 默认为空就为当前时间) |
对外提供的方法
1. getDate() 返回当前时间,格式为yyyy-mm-dd
2. getYear() 返回当前的年份
3. getMonth() 返回当前的月份
4. getDay() 返回当前月份中的天数.
JSFiddle demo链接如下:
下面代码分析如下:
1. 初始化调用init方法:分别获取开始时间 结束时间 默认时间的 "年,月,天"。如下代码:
// 开始时间可选 如果为空的话 那么默认开始时间是1900-01-01
if(_config.dateStart != '') { this.startDate = {
y: new Date(_config.dateStart).getFullYear(),
m: new Date(_config.dateStart).getMonth() + 1,
d: new Date(_config.dateStart).getDate()
};
}else {
var dateStart = '1900/01/01'; this.startDate = {
y: new Date(dateStart).getFullYear(),
m: new Date(dateStart).getMonth() + 1,
d: new Date(dateStart).getDate()
};
} // dateEnd 默认为空 如果没有传入的话 就取当前的时间
if(_config.dateEnd == '') {
this.endDate = {
y: new Date().getFullYear(),
m: new Date().getMonth() + 1,
d: new Date().getDate()
};
}else {
this.endDate = {
y: new Date(_config.dateEnd).getFullYear(),
m: new Date(_config.dateEnd).getMonth() + 1,
d: new Date(_config.dateEnd).getDate()
};
} // 默认时间可选 如果默认时间为空的话 那么就取当前的时间
if(_config.dateDefault != '') {
this.defaultDate = {
y: new Date(_config.dateDefault).getFullYear(),
m: new Date(_config.dateDefault).getMonth() + 1,
d: new Date(_config.dateDefault).getDate()
};
}else {
this.defaultDate = {
y: new Date().getFullYear(),
m: new Date().getMonth() + 1,
d: new Date().getDate()
};
}
// 判断时间是否合理
if((Date.parse(self._changeFormat(_config.dateStart)) > Date.parse(self._changeFormat(_config.dateEnd))) ||
(Date.parse(self._changeFormat(_config.dateDefault)) > Date.parse(self._changeFormat(_config.dateEnd)))){
return;
}
2. 渲染下拉框的年份:调用 y = self._renderYear();这个方法。
1. 获取年份的区间范围,获取方法就是:获取开始时间的年份 和 结束时的年份 如下代码:
/*
* 获取年份的范围 最小-最大
* @method _getYearRange
* @return {min,max}
*/
_getYearRange: function(){
var self = this,
_config = self.config;
return {
min: self.startDate.y,
max: self.endDate.y
}
},
2. 接着渲染年份,从最近的年份开始渲染,如果有默认的年份 且 满足条件的话 那么默认的年份显示出来。如下代码:
/*
* 渲染年份下拉框
* @method _renderYear
* private
*/
_renderYear: function(){
var self = this,
_config = self.config,
_cache = self.cache;
var nodeyear = $(_config.nodeYear)[0],
y = self.defaultDate.y,
range,
option; if(nodeyear) {
range = self._getYearRange();
for(var i = range.max; i >= range.min; i--) {
option = new Option(i,i); // 如果有默认年份的话
if(i == y) {
option.selected = true;
}
// 兼容所有浏览器 插入到最后
nodeyear.add(option,undefined);
}
}
$(nodeyear).attr('year',y);
return y;
},
3. 接着渲染月份 调用这个方法 y参数就是刚刚返回的年份 m = self._renderMonth(y);
1. 同理 渲染月份也要获取月份的范围 默认都是从1月份到12月份 但是也有列外。比如如下2个判断。
/*
* 获取月份的范围
* @method _getMonthRange
* @param {y} Number
*/
_getMonthRange: function(y){
var self = this,
_config = self.config;
var startDate = self.startDate,
endDate = self.endDate,
min = 1,
max = 12;
/*
* 如果默认年份等于开始年份的话 那么月份最小取得是开始的月份
* 因为如果开始是1900-05-01 如果默认的是 1900-03-02 那么最小月份肯定取得是5
* 因为默认时间不可能小于开始时间
*/
if(y == startDate.y) { // 开始年份
min = startDate.m;
} /*
* 同理 如果默认年份等于2014-04-01 那么取得是当前的年份(endDate未传的情况下)
* 那么最大的肯定取得是当前年份的 月份 不可能取的是4 因为只渲染出当前月份出来
* 后面的月份没有渲染出来
*/
if(y == endDate.y) {
max = endDate.m;
}
return {
min: min,
max: max
}
},
2. 知道月份的范围后 然后根据上面的年份渲染相应的月份:代码如下:
/*
* 根据年份 渲染所有的月份
* @method _renderMonth
* @param {y} 年份
*/
_renderMonth: function(y){
var self = this,
_config = self.config;
var nodeMonth = $(_config.nodeMonth)[0],
m = $(nodeMonth).attr('month') || self.defaultDate.m,
range,
option,
t = false;
if(nodeMonth) {
range = self._getMonthRange(y); nodeMonth.innerHTML = '';
for(var i = range.min; i <= range.max; i++) {
option = new Option(self.bitExpand(i),self.bitExpand(i)); // 如果有默认的月份的话
if(i == m) {
option.selected = true;
m = i;
t = true;
}
// 兼容所有浏览器 插入到最后
nodeMonth.add(option,undefined);
}
if(!t) {
m = range.min;
}
} $(nodeMonth).attr('month',m);
return m;
},
上面的代码 用了这句判断 m = $(nodeMonth).attr('month') || self.defaultDate.m, 默认情况下 也就是说页面一加载的时候 可以获取默认的月份,但是当我触发change事件后 我取的月份 是从m = $(nodeMonth).attr('month') 这个里面取得。上面代码 nodeMonth.innerHTML = ''; 也是为了change时候 请清空掉 然后重新生成的。
4. 渲染天数 通过这个方法: self._renderDay(y,m);
1. 渲染天数 同理也要获得相应的天数。调用_getDayRange方法。此方法中有判断是闰年的情况的。如下代码:
/*
* 获得天数的范围
* @method _getDayRange
* @param {y,m} {number,number}
*/
_getDayRange: function(y,m){
var self = this,
_config = self.config,
_cache = self.cache;
var startDate = self.startDate,
endDate = self.endDate,
min = 1,
max; if(m) {
if(m == 2) {
max = self._isLeapYear(y) ? 29 : 28;
}else {
max = _cache._dayInMonth[m-1];
}
// 如果年月份都等于开始日期的话 那么min也等于开始日
if(y == startDate.y && m == startDate.m) {
min = startDate.d;
}
// 如果年月份都等于结束日期的话 那么max也等于结束日
if(y == endDate.y && m == endDate.m) {
max = endDate.d;
}
}
return {
min: min,
max: max
}
},
2.接着渲染天数的方法如下:
_renderDay: function(y,m) {
var self = this,
_config = self.config;
var nodeDay = $(_config.nodeDay)[0],
d = $(nodeDay).attr('day') || self.defaultDate.d,
range,
option,
t = false;
if(nodeDay) {
range = self._getDayRange(y,m);
nodeDay.innerHTML = '';
for(var i = range.min; i <= range.max; i++) {
option = new Option(self.bitExpand(i),self.bitExpand(i));
// 如果有默认的天数的话
if(i == d) {
option.selected = true;
d = i;
t = true;
}
// 兼容所有浏览器 插入到最后
nodeDay.add(option,undefined);
}
if(!t) {
d = range.min;
}
}
$(nodeDay).attr('day',d);
return d;
},
5 最后用绑定change事件 调用_bindEnv方法。如:
/*
* 绑定所有事件
* @method _bindEnv
* private
*/
_bindEnv:function(){
var self = this,
_config = self.config,
_cache = self.cache;
//年份改变
$(_config.nodeYear).change(function(e){ var y = e.target.value,
m = self._renderMonth(y); self._renderDay(y,m);
$(_config.nodeYear).attr('year',y);
});
//月份改变
$(_config.nodeMonth).change(function(e){ var m = e.target.value,
y = $(_config.nodeYear).attr('year'); self._renderDay(y,m);
$(_config.nodeMonth).attr('month',m);
}); //日期改变
$(_config.nodeDay).change(function(e){
var d = e.target.value;
$(_config.nodeDay).attr('day',d);
});
},
HTML代码如下:
<label>出生日期: </label>
<select id="year"> </select>年
<select id="month"> </select>月
<select id="day"> </select>日 <ul>
<li><em>getDate</em> : <button id="testDate">日期</button><input id="textDate"/></li>
<li><em>getYear</em> : <button id="testYear">年</button><input id="textYear"/></li>
<li><em>getMonth</em> : <button id="testMonth">月</button><input id="textMonth"/></li>
<li><em>getDay</em> : <button id="testDay">日</button><input id="textDay"/></li>
</ul>
JS代码如下:
/**
* JS日期级联组件
* @constructor DateCascade
* @param {object} 可配置的对象
* @time 2014-1-13
* @author 879083421@qq.com
*/ function DateCascade(options) { this.config = {
nodeYear : '#year', // 年份下拉框dom
nodeMonth : '#month', // 月份下拉框dom
nodeDay : '#day', // 日期下拉框dom
dateStart : '', // 开始日期
dateEnd : '', // 结束日期(可选 默认为空就为当前时间)
dateDefault : '' // 默认日期
}; this.cache = {
_dayInMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] // 月份对应的天数
};
this.init(options);
} DateCascade.prototype = { constructor: DateCascade, init: function(options) { this.config = $.extend(this.config,options || {});
var self = this,
_config = self.config,
_cache = self.cache; var y,
m; /* 开始时间 和 截至时间 默认时间*/ // 开始时间可选 如果为空的话 那么默认开始时间是1900-01-01
if(_config.dateStart != '') { this.startDate = {
y: new Date(_config.dateStart).getFullYear(),
m: new Date(_config.dateStart).getMonth() + 1,
d: new Date(_config.dateStart).getDate()
};
}else {
var dateStart = '1900/01/01'; this.startDate = {
y: new Date(dateStart).getFullYear(),
m: new Date(dateStart).getMonth() + 1,
d: new Date(dateStart).getDate()
};
} // dateEnd 默认为空 如果没有传入的话 就取当前的时间
if(_config.dateEnd == '') {
this.endDate = {
y: new Date().getFullYear(),
m: new Date().getMonth() + 1,
d: new Date().getDate()
};
}else {
this.endDate = {
y: new Date(_config.dateEnd).getFullYear(),
m: new Date(_config.dateEnd).getMonth() + 1,
d: new Date(_config.dateEnd).getDate()
};
} // 默认时间可选 如果默认时间为空的话 那么就取当前的时间
if(_config.dateDefault != '') {
this.defaultDate = {
y: new Date(_config.dateDefault).getFullYear(),
m: new Date(_config.dateDefault).getMonth() + 1,
d: new Date(_config.dateDefault).getDate()
};
}else {
this.defaultDate = {
y: new Date().getFullYear(),
m: new Date().getMonth() + 1,
d: new Date().getDate()
};
}
// 判断时间是否合理
if((Date.parse(self._changeFormat(_config.dateStart)) > Date.parse(self._changeFormat(_config.dateEnd))) ||
(Date.parse(self._changeFormat(_config.dateDefault)) > Date.parse(self._changeFormat(_config.dateEnd)))){
return;
} // 渲染年份
y = self._renderYear(); // 渲染月份
m = self._renderMonth(y); // 渲染天
self._renderDay(y,m); // 所有绑定事件
self._bindEnv();
},
/*
* 渲染年份下拉框
* @method _renderYear
* private
*/
_renderYear: function(){
var self = this,
_config = self.config,
_cache = self.cache;
var nodeyear = $(_config.nodeYear)[0],
y = self.defaultDate.y,
range,
option; if(nodeyear) {
range = self._getYearRange();
for(var i = range.max; i >= range.min; i--) {
option = new Option(i,i); // 如果有默认年份的话
if(i == y) {
option.selected = true;
} // 兼容所有浏览器 插入到最后
nodeyear.add(option,undefined);
}
}
$(nodeyear).attr('year',y);
return y;
},
/*
* 根据年份 渲染所有的月份
* @method _renderMonth
* @param {y} 年份
*/
_renderMonth: function(y){
var self = this,
_config = self.config;
var nodeMonth = $(_config.nodeMonth)[0],
m = $(nodeMonth).attr('month') || self.defaultDate.m,
range,
option,
t = false;
if(nodeMonth) {
range = self._getMonthRange(y); nodeMonth.innerHTML = '';
for(var i = range.min; i <= range.max; i++) {
option = new Option(self.bitExpand(i),self.bitExpand(i)); // 如果有默认的月份的话
if(i == m) {
option.selected = true;
m = i;
t = true;
}
// 兼容所有浏览器 插入到最后
nodeMonth.add(option,undefined);
}
if(!t) {
m = range.min;
}
} $(nodeMonth).attr('month',m);
return m;
},
_renderDay: function(y,m) {
var self = this,
_config = self.config;
var nodeDay = $(_config.nodeDay)[0],
d = $(nodeDay).attr('day') || self.defaultDate.d,
range,
option,
t = false;
if(nodeDay) {
range = self._getDayRange(y,m); nodeDay.innerHTML = '';
for(var i = range.min; i <= range.max; i++) {
option = new Option(self.bitExpand(i),self.bitExpand(i)); // 如果有默认的天数的话
if(i == d) {
option.selected = true;
d = i;
t = true;
}
// 兼容所有浏览器 插入到最后
nodeDay.add(option,undefined);
}
if(!t) {
d = range.min;
}
} $(nodeDay).attr('day',d);
return d;
},
/*
* 绑定所有事件
* @method _bindEnv
* private
*/
_bindEnv:function(){
var self = this,
_config = self.config,
_cache = self.cache;
//年份改变
$(_config.nodeYear).change(function(e){ var y = e.target.value,
m = self._renderMonth(y); self._renderDay(y,m);
$(_config.nodeYear).attr('year',y);
});
//月份改变
$(_config.nodeMonth).change(function(e){ var m = e.target.value,
y = $(_config.nodeYear).attr('year'); self._renderDay(y,m);
$(_config.nodeMonth).attr('month',m);
}); //日期改变
$(_config.nodeDay).change(function(e){
var d = e.target.value;
$(_config.nodeDay).attr('day',d);
});
},
/*
* 获取年份的范围 最小-最大
* @method _getYearRange
* @return {min,max}
*/
_getYearRange: function(){
var self = this,
_config = self.config;
return {
min: self.startDate.y,
max: self.endDate.y
}
},
/*
* 获取月份的范围
* @method _getMonthRange
* @param {y} Number
*/
_getMonthRange: function(y){
var self = this,
_config = self.config;
var startDate = self.startDate,
endDate = self.endDate,
min = 1,
max = 12;
/*
* 如果默认年份等于开始年份的话 那么月份最小取得是开始的月份
* 因为如果开始是1900-05-01 如果默认的是 1900-03-02 那么最小月份肯定取得是5
* 因为默认时间不可能小于开始时间
*/
if(y == startDate.y) { // 开始年份
min = startDate.m;
} /*
* 同理 如果默认年份等于2014-04-01 那么取得是当前的年份(endDate未传的情况下)
* 那么最大的肯定取得是当前年份的 月份 不可能取的是4 因为只渲染出当前月份出来
* 后面的月份没有渲染出来
*/
if(y == endDate.y) {
max = endDate.m;
}
return {
min: min,
max: max
}
},
/*
* 获得天数的范围
* @method _getDayRange
* @param {y,m} {number,number}
*/
_getDayRange: function(y,m){
var self = this,
_config = self.config,
_cache = self.cache;
var startDate = self.startDate,
endDate = self.endDate,
min = 1,
max; if(m) {
if(m == 2) {
max = self._isLeapYear(y) ? 29 : 28;
}else {
max = _cache._dayInMonth[m-1];
}
// 如果年月份都等于开始日期的话 那么min也等于开始日
if(y == startDate.y && m == startDate.m) {
min = startDate.d;
}
// 如果年月份都等于结束日期的话 那么max也等于结束日
if(y == endDate.y && m == endDate.m) {
max = endDate.d;
}
}
return {
min: min,
max: max
}
},
/*
* 判断是否是闰年
*/
_isLeapYear: function(y){
return (y % 4 === 0 && y % 100 !== 0) || (y % 400 === 0);
},
/**
* 是否是Date格式
* @method _isDate
* @param {Date} d
* @private
* @return {Boolean}
*/
_isDate: function(d){
return Object.prototype.toString.call(d) === '[object Date]' && d.toString() !== 'Invalid Date' && !isNaN(d);
},
/*
* 小于10的数字加零
* @method bitExpand
*/
bitExpand: function(num) {
var num = num * 1;
if(/\d/.test(num)) {
if(num < 10) {
return '0' + num;
}else {
return num;
}
}
},
/*
* 判断开始日期 默认日期 结束日期的格式
*/
_changeFormat: function(date) {
return date.replace(/'-'/g,'/');
},
/*
* 获取日期
*/
getDate: function(){
var self = this,
_config = self.config;
var year = $(_config.nodeYear).attr('year'),
month = $(_config.nodeMonth).attr('month'),
day = $(_config.nodeDay).attr('day'); return (year + '-' + self.bitExpand(month) + '-' + self.bitExpand(day));
},
/*
* 获取年份
*/
getYear: function(){
var self = this,
_config = self.config;
var year = $(_config.nodeYear).attr('year');
return year;
},
/*
* 获取月份
*/
getMonth: function(){
var self = this,
_config = self.config;
var month = $(_config.nodeMonth).attr('month');
return month;
},
/*
* 获取天数
*/
getDay: function(){
var self = this,
_config = self.config; var day = $(_config.nodeDay).attr('day');
return day;
}
}
初始化方式如下:
// 初始化
$(function(){
var date = new DateCascade({});
$('#testDate').click(function(e){
$('#textDate').val(date.getDate());
}); $('#testYear').click(function(e){ $('#textYear').val(date.bitExpand(date.getYear()));
}); $('#testMonth').click(function(e){
$('#textMonth').val(date.bitExpand(date.getMonth()));
}); $('#testDay').click(function(e){
$('#textDay').val(date.bitExpand(date.getDay()));
});
});
JS日期级联组件代码分析及demo的更多相关文章
- 纯js时钟特效详细代码分析实例教程
电子时钟是网上常见的功能,在学习date对象和定时器功能时,来完成一个电子时钟的制作是不错的选择.学习本教程之前,读者需要具备html和css技能,同时需要有简单的javascript基础. 先准备一 ...
- tensorflow faster rcnn 代码分析一 demo.py
os.environ["CUDA_VISIBLE_DEVICES"]=2 # 设置使用的GPU tfconfig=tf.ConfigProto(allow_soft_placeme ...
- arcgis api for js之echarts开源js库实现地图统计图分析
前面写过一篇关于arcgis api for js实现地图统计图的,具体见:http://www.cnblogs.com/giserhome/p/6727593.html 那是基于dojo组件来实现图 ...
- arcgis api 3.x for js 之 echarts 开源 js 库实现地图统计图分析(附源码下载)
前言 关于本篇功能实现用到的 api 涉及类看不懂的,请参照 esri 官网的 arcgis api 3.x for js:esri 官网 api,里面详细的介绍 arcgis api 3.x 各个类 ...
- 微信小游戏 demo 飞机大战 代码分析(四)(enemy.js, bullet.js, index.js)
微信小游戏 demo 飞机大战 代码分析(四)(enemy.js, bullet.js, index.js) 微信小游戏 demo 飞机大战 代码分析(一)(main.js) 微信小游戏 demo 飞 ...
- 微信小游戏 demo 飞机大战 代码分析 (三)(spirit.js, animation.js)
微信小游戏 demo 飞机大战 代码分析(三)(spirit.js, animation.js) 微信小游戏 demo 飞机大战 代码分析(一)(main.js) 微信小游戏 demo 飞机大战 代码 ...
- 微信小游戏 demo 飞机大战 代码分析 (二)(databus.js)
微信小游戏 demo 飞机大战 代码分析(二)(databus.js) 微信小游戏 demo 飞机大战 代码分析(一)(main.js) 微信小游戏 demo 飞机大战 代码分析(三)(spirit. ...
- 微信小游戏 demo 飞机大战 代码分析 (一)(game.js, main.js)
微信小游戏 demo 飞机大战 代码分析(一)(main.js) 微信小游戏 demo 飞机大战 代码分析(二)(databus.js) 微信小游戏 demo 飞机大战 代码分析(三)(spirit. ...
- js日期控件demo
最近在钻研前端,写了个日期控件,内涵代码注释,希望能帮助到大家~ 1.html代码 <!DOCTYPE html> <html xmlns="http://www.w3.o ...
随机推荐
- 解决:oracle+myBatis ResultMap 类型为 map 时,表字段类型有 Long/Blob/Clob 时报错
前言:最近在做一个通用查询单表的组件,所以 sql 的写法就是 select *,然后 resultType="map" .如果数据库中的表里有字段类型为 Long 等类型时,my ...
- javascript如何获取URL参数的值
function getUrlParameter(strParame){ var args = new Object( ); var query = location.search.substring ...
- 本地服务器搭建服务:ftp
开启FTP 服务针对局域网上需要管理的一些文件共享还是有一些帮助的,感兴趣的小伙伴可以尝试下: 1.开启internt 中ftp协议服务 完成即可 -> 可以访问了. tip: UTF-8 选f ...
- 为JavaScript正名--读你不知道的JavaScript(持续更新..)
你不知道的JavaScript上卷 JavaScript和Java的关系就像Carnival和Car的关系一样,八竿子打不着. JavaScript易上手,但由于其本身的特殊性,相比其他语言能真正掌握 ...
- unity中生成一个GUI格子(始终居中)
1.Script程序 using UnityEngine; using System.Collections; public class GUITest : MonoBehaviour { [Seri ...
- Atcoder:AGC004F Namori
传送门 先考虑树,树是一个二分图. 看到是二分图并且每次是对两边的同色的点反色可以想到转化:让奇数层的点为黑,偶数为白,变成每次可以交换两个点的颜色. 把黑看成 \(-1\),白看成 \(1\),那么 ...
- BZOJ2707: [SDOI2012]走迷宫(期望 tarjan 高斯消元)
题意 题目链接 Sol 设\(f[i]\)表示从\(i\)走到\(T\)的期望步数 显然有\(f[x] = \sum_{y} \frac{f[y]}{deg[x]} + 1\) 证明可以用全期望公式. ...
- <Android 基础(三十一)> ObjectAnimator
简介 ObjectAnimator,是ValueAnimator的子类,支持利用目标视图的属性来实现动画效果.构造ObjectAnimator的时候,将会提取对应的参数来定义动画对象和对象属性.合适的 ...
- 免安装PostgreSQL启动服务及创建数据库
安装环境windows7 64位系统 免安装版本postgresql-10.3-1-windows-x64-binaries ===================================== ...
- Oracle 密码失灵
java.sql.SQLException: ORA-28001: 密碼已經屆滿 at oracle.jdbc.driver.DatabaseError.throwSqlException(Datab ...