This commit is contained in:
Jaybe
2025-03-05 14:02:29 +09:00
commit 247a7808d3
357 changed files with 161196 additions and 0 deletions
+701
View File
@@ -0,0 +1,701 @@
(function ($) {
'use strict';
var serialize = function (data, accuracy) {
accuracy = accuracy > 0 ? accuracy : 1;
var chunkSize = 24 * accuracy;
var res = [];
var i = 0;
for (i = 0; i < chunkSize * 7; i++) {
res[i] = 0;
}
for (i = 0; i < 7; i++) {
var row = data[i + 1];
if (!row) {continue;}
for (var j = 0, rowLen = row.length; j < rowLen; j++) {
res[i * chunkSize + row[j]] = 1;
}
}
return res.join('');
};
var parse = function (strSequence, accuracy) {
accuracy = accuracy > 0 ? accuracy : 1;
var chunkSize = 24 * accuracy;
var res = {};
for (var i = 0, row = 1, len = strSequence.length; i < len; i++) {
var col = i % chunkSize;
if (strSequence[i] === '1') {
!res[row] && (res[row] = []);
res[row].push(col);
}
if ((i + 1) % chunkSize === 0) {
row++;
}
}
return res;
};
var toStr = function (currentSelectRange) {
return Object.prototype.toString.call(currentSelectRange);
};
// it only does '%s', and return '' when arguments are undefined
var sprintf = function (str) {
var args = arguments;
var flag = true;
var i = 1;
str = str.replace(/%s/g, function () {
var arg = args[i++];
if (typeof arg === 'undefined') {
flag = false;
return '';
}
return arg;
});
return flag ? str : '';
};
/**
* Return an interger array of ascending range form 'form' to 'to'.
* @param {Number} form
* @param {Number} to
* @return {Array}
*/
var makeRange = function (from, to) {
// 保证 from <= to
if (from > to) {
from = from + to;
to = from - to;
from = from - to;
}
var res = [];
for (var i = from; i <= to; i++) {
res.push(i);
}
return res;
};
var makeMatrix = function (startCoord, endCoord) {
var matrix = {};
var colArr = makeRange(startCoord[1], endCoord[1]);
var fromRow = startCoord[0] < endCoord[0] ? startCoord[0] : endCoord[0];
var steps = Math.abs(startCoord[0] - endCoord[0]) + 1;
for (var i = 0; i < steps; i++) {
matrix[fromRow + i] = colArr.slice(0);
}
return matrix;
};
/**
* Merge to arrays, return an new array.
* @param {Array} origin
* @param {Array} addition
*/
var mergeArray = function (origin, addition) {
var hash = {};
var res = [];
origin.forEach(function (item, i) {
hash[item] = 1;
res[i] = item;
});
addition.forEach(function (item) {
if (!hash[item]) {
res.push(item);
}
});
return res.sort(function (num1, num2) {
return num1 - num2;
});
};
/**
* 去当前数组中去除指定集合,返回新数组
* @param {Array} origin 原数组
* @param {Array} reject 要去除的数组
*/
var rejectArray = function (origin, reject) {
var hash = {};
var res = [];
reject.forEach(function (item, i) {
hash[item] = i;
});
origin.forEach(function (item) {
if (!hash.hasOwnProperty(item)) {
res.push(item);
}
});
return res.sort(function (num1, num2) {
return num1 - num2;
});
};
// 选择模式
var SelectMode = {
JOIN: 1, // 合并模式,添加到选区
MINUS: 2, // 减去模式,从之前的选区中减去
REPLACE: 3, // 替换模式,弃用之前的选区,直接使用给定的选区作为最终值
NONE: 0
};
var Scheduler = function (el, options) {
this.$el = $(el);
if (!this.$el.is('table')) {
this.$el = $('<table></table>').appendTo(this.$el);
}
// 自定义项
this.options = options;
// 选择模式
this.selectMode = SelectMode.NONE;
this.startCoord = null;
this.endCoord = null;
// 控件的数据对象,所有操作不会更改 this.options.data
this.data = $.extend(true, {}, this.options.data);
this.init();
};
// 默认项
Scheduler.DEFAULTS = {
locale: 'en', // i18n
accuracy: 1, // how many cells of an hour
data: [], // selected cells
footer: true,
multiple: true,
disabled: false,
// event callbacks
onDragStart: $.noop,
onDragMove: $.noop,
onDragEnd: $.noop,
onSelect: $.noop,
onRender: $.noop
};
// Language
Scheduler.LOCALES = {};
// Simplified Chinese
Scheduler.LOCALES['zh-cn'] = Scheduler.LOCALES.zh = {
AM: '上午',
PM: '下午',
TIME_TITLE: '时间',
WEEK_TITLE: '星期',
WEEK_DAYS: ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'],
DRAG_TIP: '可拖动鼠标选择时间段',
RESET: '清空选择'
};
// English
Scheduler.LOCALES['en-US'] = Scheduler.LOCALES.en = {
AM: 'AM',
PM: 'PM',
TIME_TITLE: 'TIME',
WEEK_TITLE: 'DAY',
WEEK_DAYS: ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'],
DRAG_TIP: 'Drag to select hours',
RESET: 'Reset Selected'
};
// Template
Scheduler.TEMPLATES = {
HALF_DAY_ROW: '<tr>' +
'<th rowspan="2" class="slash">' +
'<div class="scheduler-time-title">%s</div>' +
'<div class="scheduler-week-title">%s</div>' +
'</th>' +
'<th class="scheduler-half-toggle" data-half-toggle="1" colspan="%s">%s</th>' +
'<th class="scheduler-half-toggle" data-half-toggle="2" colspan="%s">%s</th>' +
'</tr>',
HOUR_HEAD_CELL: '<th class="scheduler-hour-toggle" data-hour-toggle="%s" colspan="%s">%s</th>',
DAY_ROW: '<tr data-index="%s"><td class="scheduler-day-toggle" data-day-toggle="%s">%s</td>%s</tr>',
HOUR_CELL: '<td class="scheduler-hour%s" data-row="%s" data-col="%s"></td>',
FOOT_ROW: '<tr><td colspan="%s"><div class="scheduler-control-wrap"><span class="scheduler-tips">%s</span><a class="scheduler-reset">%s</a></div></td></tr>'
};
// Util
Scheduler.Util = {
parse: parse,
serialize: serialize
};
var proto = Scheduler.prototype;
proto.init = function () {
this.initLocale();
this.initTable();
this.options.onRender.call(this.$el);
};
proto.initLocale = function () {
var me = this;
if (me.options.locale) {
var parts = me.options.locale.toLowerCase().split(/-|_/);
if (parts[1]) {
parts[1] = parts[1].toUpperCase();
}
if ($.fn.scheduler.locales[me.options.locale]) {
// locale as requested
$.extend(me.options, $.fn.scheduler.locales[me.options.locale]);
} else if ($.fn.scheduler.locales[parts.join('-')]) {
// locale with sep set to - (in case original was specified with _)
$.extend(me.options, $.fn.scheduler.locales[parts.join('-')]);
} else if ($.fn.scheduler.locales[parts[0]]) {
// short locale language code (i.e. 'en')
$.extend(me.options, $.fn.scheduler.locales[parts[0]]);
}
}
};
proto.initTable = function () {
this.$el.addClass('scheduler');
if (this.options.disabled) {
this.$el.addClass('scheduler-disabled');
}
this.initHead();
this.initBody();
if (this.options.footer) {
this.initFoot();
}
};
proto.initHead = function () {
var me = this;
me.$head = me.$el.find('>thead');
if (!me.$head.length) {
me.$head = $('<thead></thead>').appendTo(me.$el);
}
me.$head.append(me.getHeadHtml());
// toggle select half day
me.$head.on('click', '.scheduler-half-toggle', me.onToggleHalfDay.bind(me));
// toggle select an hour
me.$head.on('click', '.scheduler-hour-toggle', me.onToggleHour.bind(me));
};
proto.initBody = function () {
var me = this;
me.$body = me.$el.find('>tbody');
if (!me.$body.length) {
me.$body = $('<tbody></tbody>').appendTo(me.$el);
}
me.$body.append(me.getBodyHtml(me.options.data));
// toggle select day
me.$body.on('click', '.scheduler-day-toggle', me.onToggleDay.bind(me));
// range toggle select hour
me.$body.on('mousedown', '.scheduler-hour', me.onMouseDown.bind(me))
.on('mousemove', '.scheduler-hour', me.onMouseMove.bind(me))
.on('mouseup', '.scheduler-hour', me.onMouseUp.bind(me));
};
proto.initFoot = function () {
var me = this;
me.$foot = me.$el.find('>tfoot');
if (!me.$foot.length) {
me.$foot = $('<tfoot></tfoot>').appendTo(me.$el);
}
me.$foot.append(me.getFootHtml());
me.$foot.on('click', '.scheduler-reset', me.onReset.bind(me));
};
proto.getHeadHtml = function (data) {
var me = this;
var options = me.options;
me.$head.append(sprintf($.fn.scheduler.templates.HALF_DAY_ROW,
options.TIME_TITLE, // title: time
options.WEEK_TITLE, // title: week
me.options.accuracy * 12, // row span
options.AM, // title: 上午
me.options.accuracy * 12, // row span
options.PM // title: 下午
));
var hours = '';
for (var i = 0; i < 24; i++) {
hours += sprintf($.fn.scheduler.templates.HOUR_HEAD_CELL,
i, // hour indexs
options.accuracy, // row span
i // hour text
);
}
return sprintf('<tr>%s</tr>', hours);
};
proto.getFootHtml = function () {
var me = this;
var options = me.options;
return sprintf(
$.fn.scheduler.templates.FOOT_ROW,
options.accuracy * 24 + 1,
options.DRAG_TIP,
options.RESET
);
};
proto.getBodyHtml = function (data) {
var me = this;
var options = me.options;
var rows = '';
var cellOfRow = options.accuracy * 24;
for (var i = 1; i <= 7; i++) {
var cells = '';
var selectedHours = data[i];
for (var j = 0; j < cellOfRow; j++) {
cells += sprintf(
$.fn.scheduler.templates.HOUR_CELL,
selectedHours && ~selectedHours.indexOf(j) ? ' scheduler-active' : '',
i,
j
);
}
rows += sprintf(
$.fn.scheduler.templates.DAY_ROW,
i,
i,
options.WEEK_DAYS[i - 1],
cells
);
}
return rows;
};
// toggle select one day
proto.onToggleDay = function (e) {
if (this.options.disabled) {
return;
}
var index = $(e.target).parent().data('index');
var startCoord = [index, 0]; // [row, col] row start form 1
var endCoord = [index, 24 * this.options.accuracy - 1];
var selectMode = this.getRangeSelectMode(startCoord, endCoord);
this.updateToggle(startCoord, endCoord, selectMode);
};
// toggle select half day
proto.onToggleHalfDay = function (e) {
if (this.options.disabled) {
return;
}
var index = $(e.target).data('halfToggle'); // index = 1 | 2
var fromIndex = (index - 1) * 12 * this.options.accuracy;
var toIndex = fromIndex + 12 * this.options.accuracy - 1;
var startCoord = [1, fromIndex]; // [row, col] row start form 1
var endCoord = [7, toIndex];
var selectMode = this.getRangeSelectMode(startCoord, endCoord);
this.updateToggle(startCoord, endCoord, selectMode);
};
// toggle select an hour
proto.onToggleHour = function (e) {
if (this.options.disabled) {
return;
}
var index = $(e.target).data('hourToggle'); // index = 1 | 2
var fromIndex = index * this.options.accuracy;
var toIndex = fromIndex + this.options.accuracy - 1;
var startCoord = [1, fromIndex]; // [row, col] row start form 1
var endCoord = [7, toIndex];
var selectMode = this.getRangeSelectMode(startCoord, endCoord);
this.updateToggle(startCoord, endCoord, selectMode);
};
proto.onMouseDown = function (e) {
if (this.options.disabled) {
return;
}
this.moving = true;
var $cell = $(e.target);
this.startCoord = [$cell.data('row'), $cell.data('col')];
this.endCoord = this.startCoord.slice(0);
this.selectMode = this.getCellSelectMode(this.startCoord);
this.updateRange(this.startCoord, this.endCoord, this.selectMode);
this.options.onDragStart.call(this.$el, this.cache);
};
proto.onMouseMove = function (e) {
if (!this.moving) {
return false;
}
var $cell = $(e.target);
var row = $cell.data('row');
var col = $cell.data('col');
if (!this.selectMode || !this.startCoord || (this.endCoord &&
this.endCoord[0] === row &&
this.endCoord[1] === col)
) {
return false;
}
this.endCoord = [$cell.data('row'), $cell.data('col')];
this.updateRange(this.startCoord, this.endCoord, this.selectMode);
this.options.onDragMove.call(this.$el, this.cache);
};
proto.onMouseUp = function (e) {
if (!this.moving) {
return false;
}
// 起始点都在同一个位置
if (this.startCoord[0] === this.endCoord[0] &&
this.startCoord[1] === this.endCoord[1]) {
this.updateRange(this.startCoord, this.endCoord, this.selectMode);
}
this.options.onDragEnd.call(this.$el, this.cache);
this.end();
};
/**
* 根据当前的选中坐标系更新视图,并更新选中数据
* @param {Array} startCoord 起始坐标 [row, col]
* @param {Array} endCoord 终结坐标 [row, col]
* @param {SelectMode} selectMode 选择模式
*/
proto.updateToggle = function (startCoord, endCoord, selectMode) {
this.updateRange(startCoord, endCoord, selectMode);
this.end();
};
/**
* 根据当前的选中坐标系更新视图
* @param {Array} startCoord 起始坐标 [row, col]
* @param {Array} endCoord 终结坐标 [row, col]
* @param {SelectMode} selectMode 选择模式
*/
proto.updateRange = function (startCoord, endCoord, selectMode) {
var currentSelectRange = makeMatrix(startCoord, endCoord);
this.cache = this.merge(this.data, currentSelectRange, selectMode);
this.update(this.cache);
};
/**
* 更新视图
* @param {Object} data 选中的时间集合
*/
proto.update = function (data) {
this.$body.html(this.getBodyHtml(data));
};
// 并更新选中数据
proto.end = function () {
this.data = this.cache;
this.cache = null;
this.moving = false;
this.startCoord = null;
this.endCoord = null;
this.selectMode = SelectMode.NONE;
this.options.onSelect.call(this.$el, this.val());
};
proto.onReset = function () {
if (this.options.disabled) {
return;
}
this.val({});
this.options.onSelect.call(this.$el, this.val());
};
/**
* 根据选择模式合并合个集合
* @param {Object} origin 上一次选中的数据
* @param {Object} current 当前选中的数据
* @param {Number} selectMode 选择模式 {0: none, 1: 选择(合并)模式, 2: 排除模式(从选区中减去)}
*/
proto.merge = function (origin, current, selectMode) {
var res = {};
// 替换模式下,弃用之前的选区,直接使用当前选区
if (selectMode === SelectMode.REPLACE) {
for (var i = 1; i <= 7; i++) {
if (current[i] && current[i].length) {
res[i] = current[i].slice(0);
}
}
return res;
}
for (var i = 1; i <= 7; i++) {
if (!current[i]) {
if (origin[i] && origin[i].length) {
res[i] = origin[i].slice(0);
}
continue;
}
if (origin[i] && origin[i].length) {
var m = selectMode === SelectMode.JOIN ?
mergeArray(origin[i], current[i]) :
rejectArray(origin[i], current[i]);
m.length && (res[i] = m);
} else if (selectMode === SelectMode.JOIN) {
res[i] = current[i].slice(0);
}
}
return res;
};
/**
* 根据当前选中的范围内时间格式的空闲情况,决定是全选还是全不选
* 全空闲:总时间格目 === 空闲时间格数目
* 部分空闲:总时间格目 !== 空闲时间格数目
* 无空闲:空闲时间格数目 === 0
* 状态切换:
* 当前范围全空闲 > 采用合并模式,全选当前范围
* 部分空闲 > 采用合并模式,全选当前范围
* 无空闲 > 采用合并模式,全不选当前范围
*
* @param {Array} startCoord 起始坐标 [row, col]
* @param {Array} endCoord 终结坐标 [row, col]
* @return {SelectMode}
*/
proto.getRangeSelectMode = function (startCoord, endCoord) {
if (!this.options.multiple) {
return SelectMode.REPLACE;
}
var rowRange = this.sortCoord(startCoord[0], endCoord[0]);
var colRange = this.sortCoord(startCoord[1], endCoord[1]);
var startRow = rowRange[0];
var endRow = rowRange[1];
var startCol = colRange[0];
var endCol = colRange[1];
var rows = endRow - startRow + 1;
var cols = endCol - startCol + 1;
var total = rows * cols;
// 计算已使用的时间格子
// TODO 未过滤 disabled 的格子
var used = 0;
for (var i = 0; i < rows; i++) {
var day = startRow + i;
var data = this.data[day];
if (!data) {
continue;
}
for (var j = 0; j < data.length; j++) {
if (data[j] >= startCol && data[j] <= endCol) {
used++;
}
}
}
return total === used ? SelectMode.MINUS : SelectMode.JOIN;
};
/**
* 根据当前选中的时间格式的空闲情况,决定是全选还是全不选
* 状态切换:
* 当前为单选模式(multiple=false)-> 替换模式
* 当前选中时间段为空闲 -> 全选不
* 当前选中时间段为无空闲 - > 全不选
*
* @param {Array} startCoord 起始坐标 [row, col]
* @return {SelectMode}
*/
proto.getCellSelectMode = function (coord) {
if (!this.options.multiple) {
return SelectMode.REPLACE;
}
// TODO 未过滤 disabled 的格子
var day = this.data[coord[0]];
return day && ~day.indexOf(coord[1]) ? SelectMode.MINUS : SelectMode.JOIN;
};
proto.sortCoord = function (num1, num2) {
if (num1 > num2) {
return [num2, num1];
}
return [num1, num2];
};
proto.disable = function () {
this.$el.toggleClass('scheduler-disabled', true);
this.options.disabled = true;
};
proto.enable = function () {
this.$el.toggleClass('scheduler-disabled', false);
this.options.disabled = false;
};
/**
* 如果无传参,则作为 Getter, 否则为 Setter
* @param {Array} data optional 选中的内容
* @return {Array} 返回当前选中的内容
*/
proto.val = function (data) {
// setter
if (toStr(data) === '[object Object]') {
// TODO 数据结构校验
this.data = data;
this.update(data);
} else { // getter
return this.merge(this.data, {}, SelectMode.JOIN);
}
};
proto.destroy = function () {
this.$el.removeClass('scheduler').empty();
};
$.extend(Scheduler.DEFAULTS, Scheduler.LOCALES.zh);
// SCHEDULER PLUGIN DEFINITION
// ---------------------------
var apiMethods = [
'val',
'destroy',
'disable',
'enable'
];
// Set as a jQuery plugin
$.fn.scheduler = function (option) {
var value;
var args = Array.prototype.slice.call(arguments, 1);
this.each(function () {
var $this = $(this);
var data = $this.data('scheduler');
var options = $.extend({}, Scheduler.DEFAULTS, $this.data(),
typeof option === 'object' && option);
if (typeof option === 'string') {
if ($.inArray(option, apiMethods) < 0) {
throw new Error('Unknown method: ' + option);
}
if (!data) {
return;
}
value = data[option].apply(data, args);
if (option === 'destroy') {
$this.removeData('scheduler');
}
}
if (!data) {
$this.data('scheduler', (data = new Scheduler(this, options)));
}
});
return typeof value === 'undefined' ? this : value;
};
// Exports settings
$.fn.scheduler.defaults = Scheduler.DEFAULTS;
$.fn.scheduler.templates = Scheduler.TEMPLATES;
$.fn.scheduler.locales = Scheduler.LOCALES;
$.fn.scheduler.util = Scheduler.Util;
})(jQuery);