(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 = $('
').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: '' + '' + '
%s
' + '
%s
' + '' + '%s' + '%s' + '', HOUR_HEAD_CELL: '%s', DAY_ROW: '%s%s', HOUR_CELL: '', FOOT_ROW: '
%s%s
' }; // 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 = $('').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 = $('').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 = $('').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('%s', 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);