diff --git a/docs/.vuepress/components/guide/layouts/weekly-view.vue b/docs/.vuepress/components/guide/layouts/weekly-view.vue
new file mode 100644
index 000000000..2257c347b
--- /dev/null
+++ b/docs/.vuepress/components/guide/layouts/weekly-view.vue
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/docs/layouts.md b/docs/layouts.md
index 9bcdc24fb..3a069308a 100644
--- a/docs/layouts.md
+++ b/docs/layouts.md
@@ -49,6 +49,16 @@ However, these empty weeks can be 'trimmed' by setting the `trim-weeks` prop.
```
+## Weekly View
+
+Set the `view` prop to display the calendar in 'weekly' view.
+
+
+
+```html
+
+```
+
## Week Numbers :tada:
Show week numbers in the calendar using the `show-weeknumbers` prop.
diff --git a/src/components/Calendar.vue b/src/components/Calendar.vue
index f82546c77..7a670294b 100755
--- a/src/components/Calendar.vue
+++ b/src/components/Calendar.vue
@@ -243,7 +243,7 @@ export default {
attrs: {
...this.$attrs,
},
- key: arrayHasItems(this.pages) ? this.pages[0].key : '',
+ key: arrayHasItems(this.pages) ? `${this.pages[0].key}${this.pages[0].view === 'weekly' ? this.pages[0].currentWeek : ''}` : '',
},
panes,
),
@@ -291,6 +291,14 @@ export default {
attributes: [Object, Array],
trimWeeks: Boolean,
disablePageSwipe: Boolean,
+ view: {
+ type: String,
+ default: 'monthly',
+ require: false,
+ validator(value) {
+ return ['monthly', 'weekly'].includes(value);
+ },
+ },
},
data() {
return {
@@ -394,6 +402,10 @@ export default {
}
}
},
+ view() {
+ this.refreshPages({ page: this.pages[0], ignoreCache: true });
+ this.refreshAttrs(this.pages, this.store.list, null, true);
+ },
},
created() {
this.refreshLocale();
@@ -481,6 +493,65 @@ export default {
}
// Hide nav popover for good measure
this.$refs.navPopover.hide({ hideDelay: 0 });
+
+ // Move pages on weekly view
+ const [currentPage] = this.pages;
+
+ const isHeaderNav = arg && typeof arg === 'object' && Object.keys(arg).includes(...['month', 'year']);
+
+ if (!isHeaderNav && currentPage && currentPage.view === 'weekly') {
+ if (opts.focusOnDay) {
+ return Promise.all([
+ this.refreshPages({
+ ...opts,
+ page: opts.fromPage,
+ position: 1,
+ force: true,
+ }),
+ this.setCurrentWeekByDay(opts.focusOnDay.day)
+ ]);
+ }
+
+ const shouldMovePageWeek = this.adjustWeeklyPage(currentPage, opts.fromPage);
+
+ if (shouldMovePageWeek) {
+ const isFirstWeek = currentPage.currentWeek === 0;
+ const isLastWeek = currentPage.currentWeek === currentPage.lastWeek;
+
+ if (isFirstWeek && !currentPage.weekDays[0][0].inMonth) {
+ const currentDate = {
+ month: currentPage.month,
+ year: currentPage.year
+ };
+
+ const prevMonthDate = {
+ month: currentPage.prevMonthComps.month,
+ year: currentPage.prevMonthComps.year
+ };
+
+ currentPage.title = this.mixedWeekTitle(prevMonthDate, currentDate);
+ } else if (isLastWeek && !currentPage.weekDays[currentPage.lastWeek][6].inMonth) {
+ const currentDate = {
+ month: currentPage.month,
+ year: currentPage.year
+ };
+
+ const nextMonthDate = {
+ month: currentPage.nextMonthComps.month,
+ year: currentPage.nextMonthComps.year
+ };
+
+ currentPage.title = this.mixedWeekTitle(currentDate, nextMonthDate);
+ } else {
+ currentPage.title = this.$locale.format(new Date(currentPage.year, currentPage.month - 1, 15), this.$locale.masks.title);
+ }
+
+ this.refreshAttrs(this.pages, this.store.list, null, true);
+
+ return Promise.resolve(true);
+ }
+ }
+
// Move to new `fromPage` if it's different from the current one
if (opts.fromPage && !pageIsEqualToPage(opts.fromPage, this.firstPage)) {
return this.refreshPages({
@@ -488,10 +559,81 @@ export default {
page: opts.fromPage,
position: 1,
force: true,
+ currentWeek: isHeaderNav ? 0 : null
});
}
+
return Promise.resolve(true);
},
+ adjustWeeklyPage(currentPage, fromPage) {
+ if (!currentPage || currentPage.view !== 'weekly') {
+ return false;
+ }
+
+ if (fromPage.year !== currentPage.year) {
+ if (fromPage.year > currentPage.year && currentPage.currentWeek < currentPage.lastWeek) {
+ currentPage.currentWeek++;
+ this.transitionName = this.getWeekpageTransition(false);
+ return true;
+ }
+
+ if (fromPage.year < currentPage.year && currentPage.currentWeek > 0) {
+ currentPage.currentWeek--;
+ this.transitionName = this.getWeekpageTransition(true);
+ return true;
+ }
+
+ return false;
+ }
+
+ const shouldIncrementWeek = fromPage.month > currentPage.month && currentPage.currentWeek < currentPage.lastWeek;
+ if (shouldIncrementWeek) {
+ currentPage.currentWeek++;
+ this.transitionName = this.getWeekpageTransition(false);
+ return true;
+ }
+
+ const shouldDecrementWeek = fromPage.month < currentPage.month && currentPage.currentWeek > 0;
+ if (shouldDecrementWeek) {
+ currentPage.currentWeek--;
+ this.transitionName = this.getWeekpageTransition(true);
+ return true;
+ }
+
+ return false;
+ },
+ setCurrentWeekByDay(day) {
+ return new Promise((resolve, reject) => {
+ const currentPage = this.pages[0];
+ const { month } = currentPage;
+
+ const { currentWeek, lastWeek } = currentPage;
+ const weekDay = currentPage.days.find(d => d.day === day && d.month === month);
+
+ if (!weekDay) {
+ reject(new Error('Day not found in current page.'));
+ return;
+ }
+
+ const week = weekDay.week;
+
+ const isSameWeek = week === currentWeek;
+
+ if (isSameWeek) {
+ resolve(false);
+ return;
+ }
+
+ if (week > lastWeek) {
+ currentPage.currentWeek = lastWeek;
+ resolve(true);
+ return;
+ }
+
+ currentPage.currentWeek = week - 1;
+ resolve(true);
+ });
+ },
focusDate(date, opts = {}) {
// Move to the given date
return this.move(date, opts).then(() => {
@@ -580,7 +722,7 @@ export default {
}
return page;
},
- refreshPages({ page, position = 1, force, transition, ignoreCache } = {}) {
+ refreshPages({ page, position = 1, force, transition, ignoreCache, currentWeek } = {}) {
return new Promise((resolve, reject) => {
const { fromPage, toPage } = this.getTargetPageRange(page, {
position,
@@ -589,7 +731,7 @@ export default {
// Create the new pages
const pages = [];
for (let i = 0; i < this.count; i++) {
- pages.push(this.buildPage(addPages(fromPage, i), ignoreCache));
+ pages.push(this.buildPage({ ...addPages(fromPage, i), ignoreCache, currentWeek }));
}
// Refresh disabled days for new pages
this.refreshDisabledDays(pages);
@@ -649,6 +791,16 @@ export default {
// Horizontal slide
return movePrev ? 'slide-right' : 'slide-left';
},
+ getWeekpageTransition(isPrev, transition = this.transition) {
+ if (transition === 'none') return transition;
+
+ // Vertical slide
+ if (transition === 'slide-v') {
+ return isPrev ? 'slide-down' : 'slide-up';
+ }
+ // Horizontal slide
+ return isPrev ? 'slide-right' : 'slide-left';
+ },
getPageForAttributes() {
let page = null;
const attr = this.store.pinAttr;
@@ -659,7 +811,7 @@ export default {
}
return page;
},
- buildPage({ month, year }, ignoreCache) {
+ buildPage({ month, year, currentWeek }, ignoreCache) {
const key = `${year.toString()}-${month.toString()}`;
let page = this.pages.find(p => p.key === key);
if (!page || ignoreCache) {
@@ -671,6 +823,7 @@ export default {
key,
month,
year,
+ view: this.view,
weeks: this.trimWeeks ? monthComps.weeks : 6,
title: this.$locale.format(date, this.$locale.masks.title),
shortMonthLabel: this.$locale.format(date, 'MMM'),
@@ -680,6 +833,7 @@ export default {
monthComps,
prevMonthComps,
nextMonthComps,
+ weekDays: [],
canMove: pg => this.canMove(pg),
move: pg => this.move(pg),
moveThisMonth: () => this.moveThisMonth(),
@@ -689,9 +843,74 @@ export default {
};
// Assign day info
page.days = this.$locale.getCalendarDays(page);
+
+ if (this.view === 'weekly') {
+ const pageWeeks = page.days.reduce((acc, day) => {
+ const { weeks } = acc;
+ const lastWeekFull = weeks[weeks.length - 1] && weeks[weeks.length - 1].length >= 7;
+
+ if (day.inNextMonth && lastWeekFull) {
+ return acc;
}
+
+ if (!weeks[weeks.length - 1] || lastWeekFull) {
+ return {
+ weeks: [...weeks, [day]]
+ };
+ }
+
+ return {
+ weeks: [...weeks.slice(0, -1), [...weeks[weeks.length - 1], day]]
+ };
+ }, { weeks: [] });
+
+ page.weekDays = pageWeeks.weeks;
+
+ page.lastWeek = page.weekDays.length - 1;
+
+ const [currentPage] = this.pages;
+
+ if (typeof currentWeek === 'number') {
+ page.currentWeek = currentWeek;
+ } else if (currentPage) {
+ const isFirstWeekdayInNewMonth = page.weekDays[0][0].inMonth ? 0 : 1;
+ const isLastWeekdayInNewMonth = page.weekDays[page.weekDays.length - 1][6].inMonth ? 0 : 1;
+
+ const isNewPageAfterCurrent = page.year > currentPage.year || (page.year === currentPage.year && page.month > currentPage.month);
+ page.currentWeek = isNewPageAfterCurrent ? isFirstWeekdayInNewMonth : page.lastWeek - isLastWeekdayInNewMonth;
+ } else {
+ page.currentWeek = 0;
+ }
+
+ if (page.currentWeek === 0) {
+ const currentDate = {
+ month: page.month,
+ year: page.year
+ };
+
+ const prevMonthDate = {
+ month: page.prevMonthComps.month,
+ year: page.prevMonthComps.year
+ };
+
+ page.title = this.mixedWeekTitle(prevMonthDate, currentDate);
+ }
+ }
+ }
+
return page;
},
+ mixedWeekTitle(oldDate, newDate) {
+ const { month: oldMonth, year: oldYear } = oldDate;
+ const { month: newMonth, year: newYear } = newDate;
+
+ const oldTitle = this.$locale.format(new Date(oldYear, oldMonth - 1, 1), 'MMM');
+ const newTitle = this.$locale.format(new Date(newYear, newMonth - 1, 1), 'MMM');
+
+ return oldYear === newYear
+ ? `${oldTitle} - ${newTitle} ${oldYear}`
+ : `${oldTitle} ${oldYear} - ${newTitle} ${newYear}`;
+ },
initStore() {
// Create a new attribute store
this.store = new AttributeStore(
diff --git a/src/components/CalendarDay.vue b/src/components/CalendarDay.vue
index 9f63673c6..c0bb932ef 100644
--- a/src/components/CalendarDay.vue
+++ b/src/components/CalendarDay.vue
@@ -112,7 +112,7 @@ export default {
'vc-day',
...this.day.classes,
{ 'vc-day-box-center-center': !this.$scopedSlots['day-content'] },
- { 'is-not-in-month': !this.inMonth },
+ { 'is-not-in-month': !this.inMonth && !this.isWeeklyView },
],
},
[backgroundsLayer(), contentLayer(), dotsLayer(), barsLayer()],
@@ -121,6 +121,7 @@ export default {
inject: ['sharedState'],
props: {
day: { type: Object, required: true },
+ isWeeklyView: { type: Boolean, default: false },
},
data() {
return {
@@ -185,7 +186,7 @@ export default {
let tabindex;
if (this.day.isFocusable) {
tabindex = '0';
- } else if (this.day.inMonth) {
+ } else if (this.day.inMonth || this.isWeeklyView) {
tabindex = '-1';
}
return {
diff --git a/src/components/CalendarPane.vue b/src/components/CalendarPane.vue
index 85c16ed40..376a62fb3 100644
--- a/src/components/CalendarPane.vue
+++ b/src/components/CalendarPane.vue
@@ -90,7 +90,9 @@ export default {
// Day cells
const dayCells = [];
const { daysInWeek } = this.locale;
- this.page.days.forEach((day, i) => {
+
+ const renderDays = this.page.view !== 'weekly' ? this.page.days : this.page.weekDays[this.page.currentWeek];
+ renderDays.forEach((day, i) => {
const mod = i % daysInWeek;
// Insert weeknumber cell on left side if needed
if (
@@ -103,6 +105,7 @@ export default {
h(CalendarDay, {
attrs: {
day,
+ isWeeklyView: this.page.view === 'weekly',
},
on: {
...this.$listeners,