diff --git a/examples/selectable.html b/examples/selectable.html new file mode 100644 index 0000000..192fc54 --- /dev/null +++ b/examples/selectable.html @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/src/agenda.js b/src/agenda.js index 03630cb..490c30f 100644 --- a/src/agenda.js +++ b/src/agenda.js @@ -73,6 +73,7 @@ function Agenda(element, options, methods) { var head, body, bodyContent, bodyTable, bg, colCnt, + slotCnt=0, // spanning all the way across axisWidth, colWidth, slotHeight, viewWidth, viewHeight, savedScrollTop, @@ -178,7 +179,7 @@ function Agenda(element, options, methods) { } s+= ""; head = $(s).appendTo(element); - head.find('td').click(slotClick); + bindDayHandlers(head.find('td')); // all-day event container daySegmentContainer = $("
").appendTo(head); @@ -197,13 +198,14 @@ function Agenda(element, options, methods) { "
 
"; addMinutes(d, options.slotMinutes); + slotCnt++; } s += ""; body = $("
") .append(bodyContent = $("
") .append(bodyTable = $(s))) .appendTo(element); - body.find('td').click(slotClick); + bindSlotHandlers(body.find('td')); // .click(slotClick); // slot event container slotSegmentContainer = $("
").appendTo(bodyContent); @@ -264,6 +266,8 @@ function Agenda(element, options, methods) { } + unselect(); + } @@ -337,6 +341,12 @@ function Agenda(element, options, methods) { + /* Slot/Day clicking and selecting + -----------------------------------------------------------------------*/ + + var selected=false, + selectHelper, + selectMatrix; function slotClick(ev) { var col = Math.floor((ev.pageX - bg.offset().left) / colWidth), @@ -353,6 +363,173 @@ function Agenda(element, options, methods) { } } + function unselect() { + if (selected) { + if (selectHelper) { + selectHelper.remove(); + selectHelper = null; + } + view.clearOverlays(); + view.trigger('unselect', view); + selected = false; + } + } + view.unselect = unselect; + + if (view.option('selectable') && view.option('unselectable')) { + $(document).mousedown(function() { + unselect(); + }); + } + + + // all-day + + var daySelectStart, // the "column" of the day + daySelectEnd, // the "column" of the day + daySelectRange; + + function bindDayHandlers(tds) { + tds.click(slotClick); + if (view.option('selectable')) { + tds.mousedown(daySelectMousedown); + } + } + + function daySelectMousedown(ev) { + daySelectStart = undefined; + selectMatrix = buildMainMatrix(function(cell) { + view.clearOverlays(); + if (selectHelper) { + selectHelper.remove(); // todo: turn these lines into _unselect() + selectHelper = null; + } + if (cell) { + selected = true; + daySelectEnd = cell.col; + if (daySelectStart === undefined) { + daySelectStart = daySelectEnd; + } + daySelectRange = [daySelectStart, daySelectEnd].sort(cmp); + renderDayOverlay(selectMatrix, daySelectRange[0], daySelectRange[1]+1); + bindDayHandlers(view.overlays[0]); + }else{ + selected = false; + } + }); + $(document) + .mousemove(daySelectMousemove) + .mouseup(daySelectMouseup); + selectMatrix.mouse(ev.pageX, ev.pageY); + ev.stopPropagation(); + } + + function daySelectMousemove(ev) { + selectMatrix.mouse(ev.pageX, ev.pageY); + } + + function daySelectMouseup(ev) { + $(document) + .unbind('mousemove', daySelectMousemove) + .unbind('mouseup', daySelectMouseup); + if (selected) { + view.trigger( + 'select', + view, + addDays(cloneDate(view.visStart), daySelectRange[0]), + addDays(cloneDate(view.visStart), daySelectRange[1]+1), + true + ); + } + } + + + // slot + + function bindSlotHandlers(tds) { + tds.click(slotClick); + if (view.option('selectable')) { + tds.mousedown(slotSelectMousedown); + } + } + + var slotSelectDay, + slotSelectStart, // the "row" of the slot + slotSelectEnd, // the "row" of the slot + slotSelectRange; + + function slotSelectMousedown(ev) { + slotSelectDay = undefined; + selectMatrix = buildSlotMatrix(function(cell) { + view.clearOverlays(); + if (slotSelectDay === undefined) { + slotSelectDay = cell.col; + slotSelectStart = cell.row; + } + if (selectHelper) { + selectHelper.remove(); + selectHelper = null; + } + if (cell) { + selected = true; + slotSelectEnd = cell.row; + slotSelectRange = [slotSelectStart, slotSelectEnd].sort(cmp); + if (view.option('selectHelper')) { + var rect = selectMatrix.rect(slotSelectRange[0], slotSelectDay, slotSelectRange[1]+1, slotSelectDay+1, bodyContent); + selectHelper = + $(segHtml( + { title: '', start: slotTime(slotSelectDay, slotSelectRange[0]), end: slotTime(slotSelectDay, slotSelectRange[1]+1), className: [], editable:false }, + { top: rect.top, left: rect.left+2 }, + 'fc-event fc-event-vert fc-corner-top fc-corner-bottom ' + )); + if (!$.browser.msie) { + // IE makes the event completely clear!!? + selectHelper.css('opacity', view.option('dragOpacity')); + } + // TODO: change cursor + bindSlotHandlers(selectHelper); + bodyContent.append(selectHelper); + setOuterWidth(selectHelper, rect.width-5, true); + setOuterHeight(selectHelper, rect.height, true); + }else{ + view.renderOverlay( + selectMatrix.rect(slotSelectRange[0], slotSelectDay, slotSelectRange[1]+1, slotSelectDay+1, bodyContent), + bodyContent + ); + bindSlotHandlers(view.overlays[0]); + } + }else{ + selected = false; + slotSelectEnd = undefined; + } + }); + selectMatrix.mouse(ev.pageX, ev.pageY); + $(document) + .mousemove(slotSelectMousemove) + .mouseup(slotSelectMouseup); + ev.stopPropagation(); + } + + function slotSelectMousemove(ev) { + selectMatrix.mouse(ev.pageX, ev.pageY); + } + + function slotSelectMouseup(ev) { + $(document) + .unbind('mousemove', slotSelectMousemove) + .unbind('mouseup', slotSelectMouseup); + if (selected) { + view.trigger('select', + view, + slotTime(slotSelectDay, slotSelectRange[0]), + slotTime(slotSelectDay, slotSelectRange[1]+1), + false + ); + } + } + + + /* Event Rendering @@ -526,17 +703,7 @@ function Agenda(element, options, methods) { seg.left = left; seg.outerWidth = outerWidth; seg.outerHeight = bottom - top; - html += - "
" + - "" + - "" + - "" + htmlEscape(formatDates(event.start, event.end, view.option('timeFormat'))) + "" + - "" + htmlEscape(event.title) + "" + - "" + - ((event.editable || event.editable === undefined && options.editable) && !options.disableResizing && $.fn.resizable ? - "
=
" - : '') + - "
"; + html += segHtml(event, seg, className); } slotSegmentContainer[0].innerHTML = html; // faster than html() eventElements = slotSegmentContainer.children(); @@ -608,17 +775,23 @@ function Agenda(element, options, methods) { } - + function segHtml(event, seg, className) { + return "
" + + "" + + "" + + "" + htmlEscape(formatDates(event.start, event.end, view.option('timeFormat'))) + "" + + "" + htmlEscape(event.title) + "" + + "" + + ((event.editable || event.editable === undefined && options.editable) && !options.disableResizing && $.fn.resizable ? + "
=
" + : '') + + "
"; + } function visEventEnd(event) { // returns exclusive 'visible' end, for rendering if (event.allDay) { - if (event.end) { - var end = cloneDate(event.end); - return (event.allDay || end.getHours() || end.getMinutes()) ? addDays(end, 1) : end; - }else{ - return addDays(cloneDate(event.start), 1); - } + return visEventEndAllDay(event); } if (event.end) { return cloneDate(event.end); @@ -628,6 +801,16 @@ function Agenda(element, options, methods) { } + function visEventEndAllDay(event) { + if (event.end) { + var end = cloneDate(event.end); + return (event.allDay || end.getHours() || end.getMinutes()) ? addDays(end, 1) : end; + }else{ + return addDays(cloneDate(event.start), 1); + } + } + + function bindDaySegHandlers(event, eventElement, seg) { view.eventElementHandlers(event, eventElement); @@ -686,13 +869,20 @@ function Agenda(element, options, methods) { allDay = true; } }; - matrix = new HoverMatrix(function(cell) { + matrix = buildMainMatrix(function(cell) { eventElement.draggable('option', 'revert', !cell || !cell.rowDelta && !cell.colDelta); + view.clearOverlays(); if (cell) { - if (!cell.row) { // on full-days + if (!cell.row) { + // on full-days + renderDayOverlay( + matrix, + cellOffset(addDays(cloneDate(event.start), cell.colDelta)), + cellOffset(addDays(visEventEnd(event), cell.colDelta)) // visEventEnd returns a clone + ); resetElement(); - view.showOverlay(cell); - }else{ // mouse is over bottom slots + }else{ + // mouse is over bottom slots if (isStart && allDay) { // convert event to temporary slot-event setOuterHeight( @@ -704,31 +894,23 @@ function Agenda(element, options, methods) { eventElement.draggable('option', 'grid', [colWidth, 1]); allDay = false; } - view.hideOverlay(); } - }else{ // mouse is outside of everything - view.hideOverlay(); } }); - matrix.row(head.find('td')); - bg.find('td').each(function() { - matrix.col(this); - }); - matrix.row(body); matrix.mouse(ev.pageX, ev.pageY); }, drag: function(ev, ui) { matrix.mouse(ev.pageX, ev.pageY); }, stop: function(ev, ui) { - view.hideOverlay(); view.trigger('eventDragStop', eventElement, event, ev, ui); - var cell = matrix.cell, - dayDelta = dis * ( - allDay ? // can't trust cell.colDelta when using slot grid + view.clearOverlays(); + var cell = matrix.cell; + var dayDelta = dis * ( + allDay ? // can't trust cell.colDelta when using slot grid (cell ? cell.colDelta : 0) : Math.floor((ui.position.left - origPosition.left) / colWidth) - ); + ); if (!cell || !dayDelta && !cell.rowDelta) { // over nothing (has reverted) resetElement(); @@ -787,8 +969,9 @@ function Agenda(element, options, methods) { } }; prevSlotDelta = 0; - matrix = new HoverMatrix(function(cell) { + matrix = buildMainMatrix(function(cell) { eventElement.draggable('option', 'revert', !cell); + view.clearOverlays(); if (cell) { if (!cell.row && options.allDaySlot) { // over full days if (!allDay) { @@ -797,22 +980,16 @@ function Agenda(element, options, methods) { timeElement.hide(); eventElement.draggable('option', 'grid', null); } - view.showOverlay(cell); + renderDayOverlay( + matrix, + cellOffset(addDays(cloneDate(event.start), cell.colDelta)), + cellOffset(addDays(visEventEndAllDay(event), cell.colDelta)) // visEventEnd returns a clone + ); }else{ // on slots resetElement(); - view.hideOverlay(); } - }else{ - view.hideOverlay(); } }); - if (options.allDaySlot) { - matrix.row(head.find('td')); - } - bg.find('td').each(function() { - matrix.col(this); - }); - matrix.row(body); matrix.mouse(ev.pageX, ev.pageY); }, drag: function(ev, ui) { @@ -833,7 +1010,7 @@ function Agenda(element, options, methods) { matrix.mouse(ev.pageX, ev.pageY); }, stop: function(ev, ui) { - view.hideOverlay(); + view.clearOverlays(); view.trigger('eventDragStop', eventElement, event, ev, ui); var cell = matrix.cell, dayDelta = dis * ( @@ -945,12 +1122,71 @@ function Agenda(element, options, methods) { } - - function day2col(dayOfWeek) { return ((dayOfWeek - Math.max(firstDay,nwe)+colCnt) % colCnt)*dis+dit; } + + function cellOffset(date) { // the "offset" index in the matrix + var d = rtl ? addDays(cloneDate(view.visEnd), -1) : cloneDate(view.visStart); + for (var i=0; i date) { + return i; + } + } + return i; + } + + + function slotTime(dayIndex, slotIndex) { // TODO: immune to DST??? + var d = clearTime(addDays(cloneDate(view.visStart), dayIndex)); + addMinutes(d, minMinute); + for (var i=0; i < slotIndex; i++) { // need a loop here !!!!!!!!!!!?????????? + addMinutes(d, options.slotMinutes); + } + return d; + } + + + // matrix building + + function buildMainMatrix(changeCallback) { + var matrix = new HoverMatrix(changeCallback); + if (options.allDaySlot) { + matrix.row(head.find('td')); + } + bg.find('td').each(function() { + matrix.col(this); + }); + matrix.row(body); + return matrix; + } + + function buildSlotMatrix(changeCallback) { + var matrix = new HoverMatrix(changeCallback); + bodyTable.find('td').each(function() { + matrix.row(this); + }); + bg.find('td').each(function() { + matrix.col(this); + }); + return matrix; + } + + + // overlay for dropping and selecting + + function renderDayOverlay(matrix, startCol, endCol) { + view.renderOverlay( + matrix.rect(0, startCol, 1, endCol, head), + head + ); + } + } diff --git a/src/grid.js b/src/grid.js index 4d355e1..f964199 100644 --- a/src/grid.js +++ b/src/grid.js @@ -209,7 +209,7 @@ function Grid(element, options, methods) { s += ""; } tbody = $(s + "").appendTo(table); - tbody.find('td').click(dayClick); + bindDayHandlers(tbody.find('td')); segmentContainer = $("
").appendTo(element); @@ -242,7 +242,7 @@ function Grid(element, options, methods) { } tbody.append(s); } - tbody.find('td.fc-new').removeClass('fc-new').click(dayClick); + bindDayHandlers(tbody.find('td.fc-new').removeClass('fc-new')); // re-label and re-class existing cells d = cloneDate(view.visStart); @@ -297,20 +297,12 @@ function Grid(element, options, methods) { } } + + unselect(); } - function dayClick(ev) { - var n = parseInt(this.className.match(/fc\-day(\d+)/)[1]), - date = addDays( - cloneDate(view.visStart), - Math.floor(n/colCnt) * 7 + n % colCnt - ); - view.trigger('dayClick', this, date, true, ev); - } - - function setHeight(height) { viewHeight = height; @@ -447,7 +439,8 @@ function Grid(element, options, methods) { function draggableEvent(event, eventElement) { if (!options.disableDragging && eventElement.draggable) { - var matrix; + var matrix, + dayDelta = 0; eventElement.draggable({ zIndex: 9, delay: 50, @@ -456,41 +449,36 @@ function Grid(element, options, methods) { start: function(ev, ui) { view.hideEvents(event, eventElement); view.trigger('eventDragStart', eventElement, event, ev, ui); - matrix = new HoverMatrix(function(cell) { + matrix = buildMatrix(function(cell) { eventElement.draggable('option', 'revert', !cell || !cell.rowDelta && !cell.colDelta); + view.clearOverlays(); if (cell) { - view.showOverlay(cell); + dayDelta = cell.rowDelta*7 + cell.colDelta*dis; + renderOverlays( + matrix, + cellOffset(addDays(cloneDate(event.start), dayDelta)), + cellOffset(addDays(visEventEnd(event), dayDelta)) // visEventEnd returns a clone + ); }else{ - view.hideOverlay(); + dayDelta = 0; } }); - tbody.find('tr').each(function() { - matrix.row(this); - }); - var tds = tbody.find('tr:first td'); - if (rtl) { - tds = $(tds.get().reverse()); - } - tds.each(function() { - matrix.col(this); - }); matrix.mouse(ev.pageX, ev.pageY); }, drag: function(ev) { matrix.mouse(ev.pageX, ev.pageY); }, stop: function(ev, ui) { - view.hideOverlay(); + view.clearOverlays(); view.trigger('eventDragStop', eventElement, event, ev, ui); - var cell = matrix.cell; - if (!cell || !cell.rowDelta && !cell.colDelta) { + if (dayDelta) { + eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link + view.eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui); + }else{ if ($.browser.msie) { eventElement.css('filter', ''); // clear IE opacity side-effects } view.showEvents(event, eventElement); - }else{ - eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link - view.eventDrop(this, event, cell.rowDelta*7+cell.colDelta*dis, 0, event.allDay, ev, ui); } } }); @@ -498,7 +486,161 @@ function Grid(element, options, methods) { } - // event resizing w/ 'view' methods... + + /* Day clicking and selecting + ---------------------------------------------------------*/ + + + function bindDayHandlers(days) { + days.click(dayClick) + if (view.option('selectable')) { + days.mousedown(selectMousedown); + } + } + + + function dayClick(ev) { + var n = parseInt(this.className.match(/fc\-day(\d+)/)[1]), + date = addDays( + cloneDate(view.visStart), + Math.floor(n/colCnt) * 7 + n % colCnt + ); + view.trigger('dayClick', this, date, true, ev); + } + + + var selected=false, + selectMatrix, + selectStart, // the "offset" (row*colCnt+col) of the cell + selectEnd, // the "offset" (row*colCnt+col) of the cell (inclusive) + selectRange; + + function selectMousedown(ev) { + selectStart = undefined; + selectMatrix = buildMatrix(function(cell) { + view.clearOverlays(); + if (cell) { + selected = true; + selectEnd = cell.row * colCnt + cell.col; + if (selectStart === undefined) { + selectStart = selectEnd; + } + selectRange = [selectStart, selectEnd].sort(cmp); + renderOverlays(selectMatrix, selectRange[0], selectRange[1]+1); + $.each(view.overlays, function() { + bindDayHandlers(this); + }); + }else{ + selected = false; + } + }); + $(document) + .mousemove(selectMousemove) + .mouseup(selectMouseup); + selectMatrix.mouse(ev.pageX, ev.pageY); + ev.stopPropagation(); + } + + function selectMousemove(ev) { + selectMatrix.mouse(ev.pageX, ev.pageY); + } + + function selectMouseup(ev) { + $(document) + .unbind('mousemove', selectMousemove) + .unbind('mouseup', selectMouseup); + if (selected) { + view.trigger( + 'select', + view, + offset2date(selectRange[0]), + offset2date(selectRange[1]+1), + true + ); + } + // todo: if a selection was made before this one, and this one ended up unselected, fire unselect + } + + function unselect() { + if (selected) { + view.clearOverlays(); + selected = false; + view.trigger('unselect', view); + } + } + view.unselect = unselect; + + if (view.option('selectable') && view.option('unselectable')) { + $(document).mousedown(function() { + unselect(); + }); + } + + + + + /* Utilities + ---------------------------------------------------*/ + + + function cellOffset(date) { // always returns index in range + var d = cloneDate(view.visStart), + i, j, k=0; + for (i=0; i date) { + return k; + } + k++; + } + } + return k; + } + + + function offset2date(cellOffset) { + return addDays(cloneDate(view.visStart), cellOffset); + } + + + function renderOverlays(matrix, offset, endOffset) { + var len = endOffset - offset, + localLen, + r = Math.floor(offset / colCnt), + c = offset % colCnt, + origin = element.offset(); + while (len > 0) { + localLen = Math.min(len, colCnt - c); + view.renderOverlay( + matrix.rect(r, c, r+1, c+localLen, element), + element + ); + len -= localLen; + r += 1; + c = 0; + } + } + + + function buildMatrix(changeCallback) { + var matrix = new HoverMatrix(changeCallback); + tbody.find('tr').each(function() { + matrix.row(this); + }); + var tds = tbody.find('tr:first td'); + if (rtl) { + tds = $(tds.get().reverse()); + } + tds.each(function() { + matrix.col(this); + }); + return matrix; + } + } diff --git a/src/main.js b/src/main.js index 2fc7aa3..1f464b1 100644 --- a/src/main.js +++ b/src/main.js @@ -68,7 +68,10 @@ var defaults = { buttonIcons: { prev: 'circle-triangle-w', next: 'circle-triangle-e' - } + }, + + //selectable: false, + unselectable: true }; @@ -647,6 +650,14 @@ $.fn.fullCalendar = function(options) { refetchEvents: function() { fetchEvents(eventsChanged); + }, + + + + unselect: function() { + for (n in viewInstances) { + viewInstances[n].unselect(); + } } }; diff --git a/src/util.js b/src/util.js index 9f7795e..c8a7c63 100644 --- a/src/util.js +++ b/src/util.js @@ -430,6 +430,16 @@ function HoverMatrix(changeCallback) { changeCallback(t.cell); } }; + + t.rect = function(row0, col0, row1, col1, originElement) { + var origin = originElement.offset(); + return { + top: tops[row0] - origin.top, + left: lefts[col0] - origin.left, + width: lefts[col1] - lefts[col0], + height: tops[row1] - tops[row0] + }; + }; } @@ -506,4 +516,9 @@ function cssKey(_element) { } +function cmp(a,b) { + return a - b; +} + + diff --git a/src/view.js b/src/view.js index 5104f66..c81396f 100644 --- a/src/view.js +++ b/src/view.js @@ -33,6 +33,7 @@ var viewMethods = { this.eventsByID = {}; this.eventElements = []; this.eventElementsByID = {}; + this.overlays = []; }, @@ -172,32 +173,25 @@ var viewMethods = { - // semi-transparent overlay (while dragging) + // semi-transparent overlay (while dragging or selecting) - showOverlay: function(props) { - if (!this.dayOverlay) { - this.dayOverlay = $("