"more" link on a cell when too many events. accompanying new options

This commit is contained in:
Adam Shaw
2014-07-27 16:15:00 -07:00
parent afe97d112a
commit 2053f4dc21
13 changed files with 428 additions and 65 deletions
+1
View File
@@ -30,6 +30,7 @@
"src/common/Grid.events.js",
"src/common/DayGrid.js",
"src/common/DayGrid.events.js",
"src/common/DayGrid.limit.js",
"src/common/TimeGrid.js",
"src/common/TimeGrid.events.js",
"src/common/View.js",
+21
View File
@@ -52,6 +52,7 @@ function Calendar(element, instanceOptions) {
t.today = today;
t.gotoDate = gotoDate;
t.incrementDate = incrementDate;
t.zoomToDay = zoomToDay;
t.getDate = getDate;
t.getCalendar = getCalendar;
t.getView = getView;
@@ -622,6 +623,26 @@ function Calendar(element, instanceOptions) {
date.add(moment.duration(delta));
renderView();
}
// Forces navigation to a day-view on the given date. `viewName` is the name of an explicit view to go to.
// If not specified, or 'auto', it will guess the best view based on which buttons are in the header toolbar.
function zoomToDay(newDate, viewName) {
var viewsWithButtons;
if (fcViews[viewName] === undefined) { // not an available view, or 'auto'
viewsWithButtons = header.getViewsWithButtons();
if ($.inArray('basicDay', viewsWithButtons) !== -1) {
viewName = 'basicDay';
}
else {
viewName = 'agendaDay'; // the fallback
}
}
date = newDate;
changeView(viewName);
}
function getDate() {
+8
View File
@@ -14,9 +14,11 @@ function Header(calendar, options) {
t.deactivateButton = deactivateButton;
t.disableButton = disableButton;
t.enableButton = enableButton;
t.getViewsWithButtons = getViewsWithButtons;
// locals
var el = $();
var viewsWithButtons = [];
var tm;
@@ -76,6 +78,7 @@ function Header(calendar, options) {
button.removeClass(tm + '-state-hover'); // forget why
calendar.changeView(buttonName);
};
viewsWithButtons.push(buttonName);
}
if (buttonClick) {
@@ -196,4 +199,9 @@ function Header(calendar, options) {
.removeClass(tm + '-state-disabled');
}
function getViewsWithButtons() {
return viewsWithButtons;
}
}
+3 -4
View File
@@ -336,10 +336,9 @@ $.extend(AgendaView.prototype, {
this.dayGrid.destroyEvents();
}
// When rerendering events in IE8, the event elements flash because of this line.
// Comment it out. It's not necessary because a renderEvents is always called subsequently,
// which updates the height.
//this.updateHeight();
// we DON'T need to call updateHeight() because:
// A) a renderEvents() call always happens after this, which will eventually call updateHeight()
// B) in IE8, this causes a flash whenever events are rerendered
View.prototype.destroyEvents.call(this); // call the super-method. will kill `this.segs`
},
+10 -2
View File
@@ -44,7 +44,7 @@ $.extend(BasicView.prototype, {
this.dayGrid.coordMap.containerEl = this.scrollerEl; // constrain clicks/etc to the dimensions of the scroller
this.dayGrid.el = this.el.find('.fc-day-grid');
this.dayGrid.render();
this.dayGrid.render(this.hasRigidRows());
View.prototype.render.call(this); // call the super-method
},
@@ -150,6 +150,12 @@ $.extend(BasicView.prototype, {
},
// Determines whether each row should have a constant height. Overridable by subclasses.
hasRigidRows: function() {
return false;
},
/* Dimensions
------------------------------------------------------------------------------------------------------------------*/
@@ -221,7 +227,9 @@ $.extend(BasicView.prototype, {
this.recordScroll(); // removing events will reduce height and mess with the scroll, so record beforehand
this.dayGrid.destroyEvents();
this.updateHeight();
// we DON'T need to call updateHeight() because:
// A) a renderEvents() call always happens after this, which will eventually call updateHeight()
// B) in IE8, this causes a flash whenever events are rerendered
View.prototype.destroyEvents.call(this); // call the super-method
},
+23 -1
View File
@@ -3,7 +3,10 @@
----------------------------------------------------------------------------------------------------------------------*/
setDefaults({
fixedWeekCount: true
fixedWeekCount: true,
eventLimit: false,
eventLimitText: 'more',
eventLimitClick: 'auto'
});
fcViews.month = MonthView; // register the view
@@ -56,6 +59,8 @@ $.extend(MonthView.prototype, {
// Overrides the default BasicView behavior to have special multi-week auto-height logic
setGridHeight: function(height, isAuto) {
var eventLimit = this.opt('eventLimit');
isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated
// if auto, make the height of each row the height that it would be if there were 6 weeks
@@ -63,7 +68,17 @@ $.extend(MonthView.prototype, {
height *= this.rowCnt / 6;
}
// is the event limit a constant level number?
if (eventLimit && typeof eventLimit === 'number') {
this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
}
distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows
// is the event limit dynamically calculated?
if (eventLimit && typeof eventLimit !== 'number') {
this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
}
},
@@ -74,6 +89,13 @@ $.extend(MonthView.prototype, {
}
return this.opt('fixedWeekCount');
},
// If dynamically limiting events, signals that all rows need to be a constant height.
hasRigidRows: function() {
var eventLimit = this.opt('eventLimit');
return eventLimit && typeof eventLimit !== 'number';
}
});
+30
View File
@@ -15,6 +15,19 @@
min-height: 4em; /* ensure that all rows are at least this tall */
}
/* a "rigid" row will take up a constant amount of height because content-skeleton is absolute */
.fc-row.fc-rigid {
overflow: hidden;
}
.fc-row.fc-rigid .fc-content-skeleton {
position: absolute;
top: 0;
left: 0;
right: 0;
}
/* week and day number styling */
.fc-basic-view .fc-week-number,
@@ -45,3 +58,20 @@
.fc-rtl .fc-basic-view .fc-day-number {
text-align: left;
}
/* "more" link that represents hidden events */
a.fc-more {
margin: 1px 2px;
font-size: .85em;
cursor: pointer;
text-decoration: none;
}
a.fc-more:hover {
text-decoration: underline;
}
.fc-limited { /* rows and cells that are hidden because of a "more" link */
display: none;
}
+60 -37
View File
@@ -4,43 +4,49 @@
$.extend(DayGrid.prototype, {
// A jQuery set of <tbody> elements, one for each row, with events inside. Attached to the content skeletons.
eventTbodyEls: null,
rowStructs: null, // an array of objects, each holding information about a row's event-rendering
// Render the given events onto the Grid and return the rendered segments
renderEvents: function(events) {
var res = this.renderEventRows(events);
var tbodyEls = this.eventTbodyEls = res.tbodyEls;
var rowStructs = this.rowStructs = this.renderEventRows(events);
var allSegs = [];
// append to each row's content skeleton
this.rowEls.each(function(i, rowNode) {
$(rowNode).find('.fc-content-skeleton > table').append(tbodyEls[i]);
$(rowNode).find('.fc-content-skeleton > table').append(
rowStructs[i].tbodyEl
);
allSegs.push.apply(allSegs, rowStructs[i].segs);
});
return res.segs; // return segment objects. for the view
return allSegs; // return segment objects. for the view
},
// Removes all rendered event elements
destroyEvents: function() {
if (this.eventTbodyEls) {
this.eventTbodyEls.remove();
this.eventTbodyEls = null;
var rowStructs = this.rowStructs || [];
var rowStruct;
while ((rowStruct = rowStructs.pop())) {
rowStruct.tbodyEl.remove();
}
},
// Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
// Returns an object with properties 'tbodyEls' and 'segs' (which contains all the rendered segment objects).
// Returns an array of rowStruct objects (see the bottom of `renderEventRow`).
renderEventRows: function(events) {
var view = this.view;
var allSegs = this.eventsToSegs(events);
var segRows = this.groupSegRows(allSegs); // group into nested arrays
var html = '';
var tbodyNodes = [];
var rowStructs = [];
var i;
var row;
var rowSegs;
// build a large concatenation of event segment HTML
for (i = 0; i < allSegs.length; i++) {
@@ -55,16 +61,14 @@ $.extend(DayGrid.prototype, {
// iterate each row of segment groupings
for (row = 0; row < segRows.length; row++) {
segRows[row] = $.grep(segRows[row], renderedSegFilter); // filter out non-rendered segments. reassign array
tbodyNodes.push(
this.renderSegSkeleton(segRows[row])[0]
rowSegs = segRows[row];
rowSegs = $.grep(rowSegs, renderedSegFilter); // filter out non-rendered segments
rowStructs.push(
this.renderEventRow(row, rowSegs)
);
}
return {
tbodyEls: $(tbodyNodes), // array -> jQuery set
segs: flattenArray(segRows) // flatten all rendered segments into one array
};
return rowStructs;
},
@@ -116,14 +120,17 @@ $.extend(DayGrid.prototype, {
},
// Given an array of segments all in the same row, render a <tbody> element, a skeleton that contains the segments
renderSegSkeleton: function(rowSegs) {
// Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
// the segments. Returns object with a bunch of internal data about how the render was calculated.
renderEventRow: function(row, rowSegs) {
var view = this.view;
var colCnt = view.colCnt;
var levels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
var tbody = $('<tbody/>');
var emptyTds = []; // a sparse array of references to the current row's empty cells, indexed by column
var aboveEmptyTds; // like emptyTds, but for the level above
var segMatrix = []; // lookup for which segments are rendered into which level+col cells
var cellMatrix = []; // lookup for all <td> elements of the level+col matrix
var loneCellMatrix = []; // lookup for <td> elements that only take up a single column
var i, levelSegs;
var col;
var tr;
@@ -133,8 +140,8 @@ $.extend(DayGrid.prototype, {
// populates empty cells from the current column (`col`) to `endCol`
function emptyCellsUntil(endCol) {
while (col < endCol) {
// try to grab an empty cell from the level above and extend its rowspan. otherwise, create a fresh cell
td = aboveEmptyTds[col];
// try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
td = (loneCellMatrix[i - 1] || [])[col];
if (td) {
td.attr(
'rowspan',
@@ -145,22 +152,24 @@ $.extend(DayGrid.prototype, {
td = $('<td/>');
tr.append(td);
}
emptyTds[col] = td;
cellMatrix[i][col] = td;
loneCellMatrix[i][col] = td;
col++;
}
}
// Iterate through all levels, and then beyond one. Do this so we have an empty row at the end.
// This empty row comes in handy when styling the height of the content skeleton.
for (i = 0; i < levels.length + 1; i++) {
levelSegs = levels[i];
for (i = 0; i < levelCnt; i++) { // iterate through all levels
levelSegs = segLevels[i];
col = 0;
tr = $('<tr/>');
aboveEmptyTds = emptyTds;
emptyTds = [];
segMatrix.push([]);
cellMatrix.push([]);
loneCellMatrix.push([]);
if (levelSegs) { // protect against non-existent last level
// levelCnt might be 1 even though there are no actual levels. protect against this.
// this single empty row is useful for styling.
if (levelSegs) {
for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
seg = levelSegs[j];
@@ -168,22 +177,36 @@ $.extend(DayGrid.prototype, {
// create a container that occupies or more columns. append the event element.
td = $('<td class="fc-event-container"/>').append(seg.el);
if (seg.rightCol > seg.leftCol) {
if (seg.leftCol != seg.rightCol) {
td.attr('colspan', seg.rightCol - seg.leftCol + 1);
}
else { // a single-column segment
loneCellMatrix[i][col] = td;
}
while (col <= seg.rightCol) {
cellMatrix[i][col] = td;
segMatrix[i][col] = seg;
col++;
}
tr.append(td);
col = seg.rightCol + 1;
}
}
emptyCellsUntil(colCnt); // finish off the row
this.bookendCells(tr, 'eventSkeleton');
tbody.append(tr);
}
return tbody;
return { // a "rowStruct"
row: row, // the row number
tbodyEl: tbody,
cellMatrix: cellMatrix,
segMatrix: segMatrix,
segLevels: segLevels,
segs: rowSegs
};
},
+10 -5
View File
@@ -21,14 +21,15 @@ $.extend(DayGrid.prototype, {
// Renders the rows and columns into the component's `this.el`, which should already be assigned.
// isRigid determins whether the individual rows should ignore the contents and be a constant height.
// Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
render: function() {
render: function(isRigid) {
var view = this.view;
var html = '';
var row;
for (row = 0; row < view.rowCnt; row++) {
html += this.dayRowHtml(row);
html += this.dayRowHtml(row, isRigid);
}
this.el.html(html);
@@ -46,10 +47,14 @@ $.extend(DayGrid.prototype, {
// Generates the HTML for a single row. `row` is the row number.
dayRowHtml: function(row) {
dayRowHtml: function(row, isRigid) {
var view = this.view;
var classes = [ 'fc-row', 'fc-week' ];
if (isRigid) {
classes.push('fc-rigid');
}
if (view.dayRowThemeClass) { // provides the view a hook to inject a theme className
classes.push(view.dayRowThemeClass);
}
@@ -197,7 +202,7 @@ $.extend(DayGrid.prototype, {
// Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
renderHelper: function(event, sourceSeg) {
var helperNodes = [];
var tbodyEls = this.renderEventRows([ event ]).tbodyEls; // render events as usual, receiving tbodys to inject
var rowStructs = this.renderEventRows([ event ]);
// inject each new event skeleton into each associated row
this.rowEls.each(function(row, rowNode) {
@@ -215,7 +220,7 @@ $.extend(DayGrid.prototype, {
skeletonEl.css('top', skeletonTop)
.find('table')
.append(tbodyEls[row]);
.append(rowStructs[row].tbodyEl);
rowEl.append(skeletonEl);
helperNodes.push(skeletonEl[0]);
+230
View File
@@ -0,0 +1,230 @@
/* Methods relate to limiting the number events for a given day on a DayGrid
----------------------------------------------------------------------------------------------------------------------*/
$.extend(DayGrid.prototype, {
// Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
// `levelLimit` can be false (don't limit), a number, or true (should be computed).
limitRows: function(levelLimit) {
var rowStructs = this.rowStructs || [];
var row; // row #
var rowLevelLimit;
for (row = 0; row < rowStructs.length; row++) {
this.unlimitRow(row);
if (!levelLimit) {
rowLevelLimit = false;
}
else if (typeof levelLimit === 'number') {
rowLevelLimit = levelLimit;
}
else {
rowLevelLimit = this.computeRowLevelLimit(row);
}
if (levelLimit !== false) {
this.limitRow(row, rowLevelLimit);
}
}
},
// Computes the number of levels a row will accomodate without going outside its bounds.
// Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
// `row` is the row number.
computeRowLevelLimit: function(row) {
var rowEl = this.rowEls.eq(row); // the containing "fake" row div
var rowHeight = rowEl.height(); // TODO: cache somehow?
var trEls = this.rowStructs[row].tbodyEl.children();
var i, trEl;
// Reveal one level <tr> at a time and stop when we find one out of bounds
for (i = 0; i < trEls.length; i++) {
trEl = trEls.eq(i).removeClass('fc-limited'); // get and reveal
if (trEl.position().top + trEl.outerHeight() > rowHeight) {
return i;
}
}
return false; // should not limit at all
},
// Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
// `row` is the row number.
// `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
limitRow: function(row, levelLimit) {
var _this = this;
var view = this.view;
var rowStruct = this.rowStructs[row];
var moreNodes = []; // array of "more" <a> links and <td> DOM nodes
var col = 0; // col #
var cell;
var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right
var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row
var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes
var i, seg;
var segsBelow; // array of segment objects below `seg` in the current `col`
var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies
var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column)
var td, rowspan;
var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell
var j;
var moreTd, moreLink;
// Iterates through empty level cells and places "more" links inside if need be
function emptyCellsUntil(endCol) { // goes from current `col` to `endCol`
while (col < endCol) {
cell = { row: row, col: col };
segsBelow = _this.getCellSegs(cell, levelLimit);
if (segsBelow.length) {
td = cellMatrix[levelLimit - 1][col];
moreLink = _this.renderMoreLink(cell, segsBelow);
td.append(moreLink);
moreNodes.push(moreLink[0]);
}
col++;
}
}
if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit?
levelSegs = rowStruct.segLevels[levelLimit - 1];
cellMatrix = rowStruct.cellMatrix;
limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit
.addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array
// iterate though segments in the last allowable level
for (i = 0; i < levelSegs.length; i++) {
seg = levelSegs[i];
emptyCellsUntil(seg.leftCol); // process empty cells before the segment
// determine *all* segments below `seg` that occupy the same columns
colSegsBelow = [];
totalSegsBelow = 0;
while (col <= seg.rightCol) {
cell = { row: row, col: col };
segsBelow = this.getCellSegs(cell, levelLimit);
colSegsBelow.push(segsBelow);
totalSegsBelow += segsBelow.length;
col++;
}
if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links?
td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell
rowspan = td.attr('rowspan') || 1;
segMoreNodes = [];
// make a replacement <td> for each column the segment occupies. will be one for each colspan
for (j = 0; j < colSegsBelow.length; j++) {
moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan);
segsBelow = colSegsBelow[j];
cell = { row: row, col: seg.leftCol + j };
moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too
moreTd.append(moreLink);
segMoreNodes.push(moreTd[0]);
moreNodes.push(moreTd[0]);
}
td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements
limitedNodes.push(td[0]);
}
}
emptyCellsUntil(view.colCnt); // finish off the level
rowStruct.moreEls = $(moreNodes); // for easy undoing later
rowStruct.limitedEls = $(limitedNodes); // for easy undoing later
}
},
// Reveals all levels and removes all "more"-related elements for a grid's row.
// `row` is a row number.
unlimitRow: function(row) {
var rowStruct = this.rowStructs[row];
if (rowStruct.moreEls) {
rowStruct.moreEls.remove();
rowStruct.moreEls = null;
}
if (rowStruct.limitedEls) {
rowStruct.limitedEls.removeClass('fc-limited');
rowStruct.limitedEls = null;
}
},
// Renders an <a> element that represents hidden event element for a cell.
// Responsible for attaching click handler as well.
renderMoreLink: function(cell, hiddenSegs) {
var _this = this;
var view = this.view;
return $('<a class="fc-more"/>')
.text(
this.getMoreLinkText(hiddenSegs.length)
)
.on('click', function() {
var date = view.cellToDate(cell);
var clickOption = view.opt('eventLimitClick');
if (typeof clickOption === 'string') { // a view name or 'auto'
view.calendar.zoomToDay(date, clickOption);
}
else {
view.trigger(
'eventLimitClick',
null,
date,
_this.getCellDayEl(cell), // element for the cell's day
hiddenSegs,
_this.getCellSegs(cell) // all segments on that day
);
}
});
},
// Generates the text that should be inside a "more" link, given the number of events it represents
getMoreLinkText: function(num) {
var view = this.view;
var opt = view.opt('eventLimitText');
if (typeof opt === 'function') {
return opt(num);
}
else {
if (view.opt('isRTL')) {
return opt + ' ' + num + '+';
}
else {
return '+' + num + ' ' + opt;
}
}
},
// Returns segments within a given cell.
// If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
getCellSegs: function(cell, startLevel) {
var segMatrix = this.rowStructs[cell.row].segMatrix;
var level = startLevel || 0;
var segs = [];
var seg;
while (level < segMatrix.length) {
seg = segMatrix[level][cell.col];
if (seg) {
segs.push(seg);
}
level++;
}
return segs;
}
});
+1 -1
View File
@@ -62,7 +62,7 @@ $.extend(Grid.prototype, {
var _this = this;
this.el.on('mousedown', function(ev) {
if (!$(ev.target).is('.fc-event-container *')) { // not an event element
if (!$(ev.target).is('.fc-event-container *, .fc-more')) { // not an event element or "more" link
_this.dayMousedown(ev);
}
});
+29 -13
View File
@@ -51,31 +51,47 @@ tbody,
/* don't force a min-height on rows (for DayGrid) */
.fc tbody .fc-row {
min-height: 0 !important;
height: auto !important; /* undo height that JS set in distributeHeight */
min-height: 0 !important; /* undo the min-height from each view's specific stylesheet */
}
/* don't have chunky padding underneath events (for Agenda, basicWeek, basicDay) */
.fc tbody .fc-row .fc-content-skeleton {
position: static; /* undo .fc-rigid */
/* don't have chunky padding underneath events (for Agenda, basicWeek, basicDay) */
padding-bottom: 0 !important;
}
/* Make space at the bottom by putting a non-breaking space in the last row's cells.
:last-child only works with newer browsers */
.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td:after {
content: "\000A0";
/* give back the bottom spacing that was taken away from the content-skeleton's padding (above) */
/* only works on modern browsers */
.fc tbody .fc-row .fc-content-skeleton table tr:last-child td {
padding-bottom: 1px;
}
/* For IE8, which doesn't understand the above line, give a min-height.
For some reason, IE8 really overexaggerates this value.
Other browsers will already be taller than this. */
/* sets a min-height on the event skeleton. for IE8. was overexaggerating this, so make small */
.fc tbody .fc-row .fc-content-skeleton table {
height: 2em;
}
/* For IE8, the table height might get too tall and create gaps between events and day lines.
Vertically centering events within these cells makes it look better */
.fc-day-grid .fc-event-container {
vertical-align: middle !important;
/* sets a min-height on the event skeleton. for modern browsers (not IE8) */
.fc tbody .fc-row .fc-content-skeleton table:last-child {
height: 4em;
}
/* Undo month-view event limiting. Display all events and hide the "more" links
--------------------------------------------------------------------------------------------------*/
.fc-more-cell,
.fc-more {
display: none !important;
}
.fc tr.fc-limited {
display: table-row !important;
}
.fc td.fc-limited {
display: table-cell !important;
}
+2 -2
View File
@@ -82,7 +82,7 @@ function distributeHeight(els, availableHeight, shouldRedistribute) {
var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
$(el).css('min-height', newHeight);
$(el).height(newHeight);
}
});
}
@@ -90,7 +90,7 @@ function distributeHeight(els, availableHeight, shouldRedistribute) {
// Undoes distrubuteHeight, restoring all els to their natural height
function undistributeHeight(els) {
els.css('min-height', '');
els.height('');
}