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 @@
+
+
+
+
+")
.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 +=
- "
";
+ 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 "
";
+ }
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 = $("")
- .appendTo(this.element);
- }
- var o = this.element.offset();
- this.dayOverlay
- .css({
- top: props.top - o.top,
- left: props.left - o.left,
- width: props.width,
- height: props.height
- })
- .show();
+ renderOverlay: function(rect, parent) {
+ var e = $("")
+ .css(rect)
+ .appendTo(parent);
+ this.overlays.push(e);
},
- hideOverlay: function() {
- if (this.dayOverlay) {
- this.dayOverlay.hide();
- }
+ clearOverlays: function() {
+ $.each(this.overlays, function() {
+ this.remove();
+ });
+ this.overlays = [];
},
+
// common horizontal event resizing
resizableDayEvent: function(event, eventElement, colWidth) {
diff --git a/tests/plain.html b/tests/plain.html
index 13e6a84..18f3fc9 100644
--- a/tests/plain.html
+++ b/tests/plain.html
@@ -19,6 +19,18 @@
right: 'month,agendaWeek,basicWeek,agendaDay,basicDay'
},
editable: true,
+ //defaultView: 'agendaWeek',
+ selectable: true,
+ selectHelper: true,
+ select: function(start, end, allDay) {
+ console.log('select');
+ console.log(start);
+ console.log(end);
+ console.log(allDay);
+ },
+ unselect: function() {
+ console.log('unselect');
+ },
events: [
{
title: 'All Day Event',