Files
2025-03-05 14:02:29 +09:00

702 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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);