mirror of
https://github.com/pnp/sp-dev-fx-webparts.git
synced 2025-03-06 19:59:46 +00:00
Merge pull request #1229 from hugoabernier/react-calendar-feed-110
UPDATED SAMPLE: react-calendar-feed
This commit is contained in:
commit
5b3634043a
@ -2,7 +2,7 @@
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"isCreatingSolution": true,
|
||||
"environment": "spo",
|
||||
"version": "1.9.1",
|
||||
"version": "1.10.0",
|
||||
"libraryName": "react-calendar-feed",
|
||||
"libraryId": "25653136-fc83-4abe-b9d2-a4ac041959d5",
|
||||
"packageManager": "npm",
|
||||
|
@ -24,7 +24,7 @@ For more information about how this solution was built, including some design de
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
|
||||

|
||||

|
||||
|
||||
## Applies to
|
||||
|
||||
@ -40,9 +40,9 @@ Before you can use this web part example, you will need one of the following:
|
||||
- A WordPress WP-FullCalendar feed
|
||||
- An Exchange Public Calendar
|
||||
|
||||
It is important that all feeds do not require authentication. Also, make sure that your calendar includes upcoming events, as the web part will filter out evens that are earlier than today's date.
|
||||
This web part only supports anonymous external feeds. Also, make sure that your calendar includes upcoming events, as the web part will filter out evens that are earlier than today's date.
|
||||
|
||||
If your feed supports filtering by dates, you can specify **{s}** in the URL where the start date should be inserted, and the web part will automatically replace the **{s}** placeholder with today's date. Similarly, you can specify **{e}** in the URL where you wish the end date to be inserted, and the web part will automatically replace the placeholder for the end date, as determined by the date range you select.
|
||||
If your feed supports filtering by dates, you can specify `{s}` in the URL where the start date should be inserted, and the web part will automatically replace the `{s}` placeholder with today's date. Similarly, you can specify `{e}` in the URL where you wish the end date to be inserted, and the web part will automatically replace the placeholder for the end date, as determined by the date range you select.
|
||||
|
||||
## Solution
|
||||
|
||||
@ -59,6 +59,7 @@ Version|Date|Comments
|
||||
3.0|November 9, 2018|Converted to SPFx 1.7; Added SharePoint Calendar feed
|
||||
4.0|January 16, 2019|Converted to SPFx 1.7.1; Removed NPM libraries associated with issue #708.
|
||||
5.0|August 17, 2019|Converted to SPFx 1.9.1; Refreshed carousel code; Addresses #735, #909. Also added **Convert from UTC** option to handle feeds which do not provide time zone information.
|
||||
5.1|April 16, 2020|Converted to SPFx 1.10.0; Fixed issue with UTC mode when in narrow view. Updated resizing behavior and styles to match OOB calendar view. Added support for themes and theme variants.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
"solution": {
|
||||
"name": "react-calendar-feed-client-side-solution",
|
||||
"id": "25653136-fc83-4abe-b9d2-a4ac041959d5",
|
||||
"version": "1.0.0.0",
|
||||
"version": "5.1.0.0",
|
||||
"includeClientSideAssets": true
|
||||
},
|
||||
"paths": {
|
||||
|
9045
samples/react-calendar-feed/package-lock.json
generated
9045
samples/react-calendar-feed/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-calendar-feed",
|
||||
"version": "1.1.0",
|
||||
"version": "5.1.0",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"engines": {
|
||||
@ -12,10 +12,11 @@
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "1.9.1",
|
||||
"@microsoft/sp-lodash-subset": "1.9.1",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.9.1",
|
||||
"@microsoft/sp-webpart-base": "1.9.1",
|
||||
"@microsoft/sp-core-library": "1.10.0",
|
||||
"@microsoft/sp-lodash-subset": "1.10.0",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
|
||||
"@microsoft/sp-property-pane": "1.10.0",
|
||||
"@microsoft/sp-webpart-base": "1.10.0",
|
||||
"@pnp/common": "^1.2.8",
|
||||
"@pnp/logging": "^1.2.8",
|
||||
"@pnp/odata": "^1.2.8",
|
||||
@ -29,6 +30,7 @@
|
||||
"feedparser": "^2.2.9",
|
||||
"ical.js": "^1.3.0",
|
||||
"ics-js": "^0.10.2",
|
||||
"moment": "^2.24.0",
|
||||
"office-ui-fabric-react": "6.189.2",
|
||||
"react": "16.8.5",
|
||||
"react-dom": "16.8.5",
|
||||
@ -41,10 +43,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/rush-stack-compiler-2.9": "0.7.16",
|
||||
"@microsoft/sp-build-web": "1.9.1",
|
||||
"@microsoft/sp-module-interfaces": "1.9.1",
|
||||
"@microsoft/sp-tslint-rules": "1.9.1",
|
||||
"@microsoft/sp-webpart-workbench": "1.9.1",
|
||||
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
|
||||
"@microsoft/sp-build-web": "1.10.0",
|
||||
"@microsoft/sp-module-interfaces": "1.10.0",
|
||||
"@microsoft/sp-tslint-rules": "1.10.0",
|
||||
"@microsoft/sp-webpart-workbench": "1.10.0",
|
||||
"@types/chai": "3.4.34",
|
||||
"@types/mocha": "2.2.38",
|
||||
"ajv": "~5.2.2",
|
||||
|
@ -2,54 +2,71 @@ import * as moment from "moment";
|
||||
import { css } from "office-ui-fabric-react/lib/Utilities";
|
||||
import * as React from "react";
|
||||
import styles from "./DateBox.module.scss";
|
||||
import { DateBoxSize, IDateBoxProps, IDateBoxState } from "./DateBox.types";
|
||||
import { DateBoxSize, IDateBoxProps } from ".";
|
||||
|
||||
/**
|
||||
* Shows a date in a SharePoint-looking date
|
||||
*/
|
||||
export class DateBox extends React.Component<IDateBoxProps, IDateBoxState> {
|
||||
public render(): React.ReactElement<IDateBoxProps> {
|
||||
// convert start and end date into moments so that we can manipulate them
|
||||
const startMoment: moment.Moment = moment(this.props.startDate);
|
||||
export const DateBox = (props: IDateBoxProps) => {
|
||||
// convert start and end date into moments so that we can manipulate them
|
||||
const startMoment: moment.Moment = moment(props.startDate);
|
||||
|
||||
// event actually ends one second before the end date
|
||||
const endMoment: moment.Moment = moment(this.props.endDate).add(-1, "s");
|
||||
// event actually ends one second before the end date
|
||||
const endMoment: moment.Moment = moment(props.endDate).add(-1, "s");
|
||||
|
||||
// check if both dates are on the same day
|
||||
const isSameDay: boolean = startMoment.isSame(endMoment, "day");
|
||||
// check if both dates are on the same day
|
||||
const isSameDay: boolean = startMoment.isSame(endMoment, "day");
|
||||
|
||||
if (isSameDay) {
|
||||
return this._renderSingleDay(startMoment);
|
||||
} else {
|
||||
return this._renderMultiDay(startMoment, endMoment);
|
||||
}
|
||||
}
|
||||
|
||||
private _renderSingleDay(startMoment: moment.Moment): JSX.Element {
|
||||
const { className, size } = this.props;
|
||||
if (isSameDay) {
|
||||
return (
|
||||
<div className={css(styles.box,
|
||||
styles.boxIsSingleDay,
|
||||
(size === DateBoxSize.Small ? styles.boxIsSmall : styles.boxIsMedium), className)}
|
||||
data-automation-id="singleDayDayContainer">
|
||||
(props.size === DateBoxSize.Small ? styles.boxIsSmall : styles.boxIsMedium), props.className)}
|
||||
style={
|
||||
props.themeVariant &&
|
||||
{
|
||||
// KLUDGE: It seems like the themeVariant palette doesn't expose primaryBackground
|
||||
backgroundColor: props.themeVariant.palette["primaryBackground"],
|
||||
borderColor: props.themeVariant.semanticColors.bodyDivider
|
||||
}}>
|
||||
<div className={styles.month}
|
||||
data-automation-id="singleDayMonthContainer">{startMoment.format("MMM").toUpperCase()}</div>
|
||||
style={
|
||||
props.themeVariant &&
|
||||
{ color: props.themeVariant.semanticColors.bodyText }}>{startMoment.format("MMM").toUpperCase()}</div>
|
||||
<div className={styles.day}
|
||||
data-automation-id="singleDayDayContainer">{startMoment.format("D")}</div>
|
||||
style={
|
||||
props.themeVariant &&
|
||||
{ color: props.themeVariant.semanticColors.bodyText }}>{startMoment.format("D")}</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
private _renderMultiDay(startMoment: moment.Moment, endMoment: moment.Moment): JSX.Element {
|
||||
const { className, size } = this.props;
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className={css(styles.box,
|
||||
styles.boxIsSingleDay,
|
||||
(size === DateBoxSize.Small ? styles.boxIsSmall : styles.boxIsMedium), className)}
|
||||
data-automation-id="multipleDayBox">
|
||||
<div className={styles.date} data-automation-id="multipleDayStartDateContainer">{startMoment.format("MMM D").toUpperCase()}</div>
|
||||
<hr className={styles.separator} />
|
||||
<div className={styles.date} data-automation-id="multipleDayEndDateContainer">{endMoment.format("MMM D").toUpperCase()}</div>
|
||||
(props.size === DateBoxSize.Small ? styles.boxIsSmall : styles.boxIsMedium), props.className)}
|
||||
style={
|
||||
props.themeVariant &&
|
||||
{
|
||||
backgroundColor: props.themeVariant.palette["primaryBackground"],
|
||||
borderColor: props.themeVariant.semanticColors.bodyDivider
|
||||
}}>
|
||||
|
||||
<div className={styles.date} style={
|
||||
props.themeVariant &&
|
||||
{ color: props.themeVariant.semanticColors.bodyText }}>{startMoment.format("MMM D").toUpperCase()}</div>
|
||||
<hr className={styles.separator}
|
||||
style={
|
||||
props.themeVariant &&
|
||||
{
|
||||
borderColor: props.themeVariant.semanticColors.bodyText
|
||||
}}
|
||||
/>
|
||||
<div className={styles.date} style={
|
||||
props.themeVariant &&
|
||||
{ color: props.themeVariant.semanticColors.bodyText }}>{endMoment.format("MMM D").toUpperCase()}</div>
|
||||
</div>);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -1,15 +0,0 @@
|
||||
export interface IDateBoxProps {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
className?: string;
|
||||
size: DateBoxSize;
|
||||
}
|
||||
|
||||
export interface IDateBoxState {
|
||||
// you just proved advertising works!
|
||||
}
|
||||
|
||||
export enum DateBoxSize {
|
||||
Small,
|
||||
Medium
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
export enum DateBoxSize {
|
||||
Small,
|
||||
Medium
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { DateBoxSize } from "./DateBoxSize";
|
||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
|
||||
export interface IDateBoxProps {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
className?: string;
|
||||
size: DateBoxSize;
|
||||
themeVariant?: IReadonlyTheme;
|
||||
}
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from "./DateBox";
|
||||
export * from "./DateBox.types";
|
||||
export * from "./IDateBoxProps";
|
||||
export * from "./DateBoxSize";
|
||||
|
@ -23,7 +23,7 @@
|
||||
}
|
||||
|
||||
.cardWrapper .dateBox {
|
||||
border-color: $ms-color-neutralTertiaryAlt;
|
||||
border-color: $ms-color-neutralLight;
|
||||
}
|
||||
|
||||
.normalCard .dateBoxContainer {
|
||||
@ -44,6 +44,7 @@
|
||||
.compactCard .dateBox {
|
||||
margin: auto 14px auto 0;
|
||||
min-width: 64px;
|
||||
background-color: $ms-color-white;
|
||||
}
|
||||
|
||||
[dir=rtl] .compactCard .dateBox {
|
||||
@ -111,8 +112,8 @@
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background-color: $ms-color-white;
|
||||
border: 1px solid #eaeaea;
|
||||
//background-color: $ms-color-white;
|
||||
border: 1px solid $ms-color-neutralLight;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
max-width: 320px;
|
||||
@ -162,7 +163,7 @@
|
||||
|
||||
.category,
|
||||
.location {
|
||||
color:$ms-color-neutralSecondary;
|
||||
color: $ms-color-neutralSecondary;
|
||||
}
|
||||
|
||||
.category,
|
||||
@ -176,8 +177,3 @@
|
||||
word-wrap: normal;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
// :global(.slick-slide) .cardWrapper {
|
||||
// padding-left:8px;
|
||||
// padding-right:8px;
|
||||
// }
|
||||
|
@ -4,172 +4,167 @@ import * as ICS from "ics-js";
|
||||
import * as moment from "moment";
|
||||
import { ActionButton, DocumentCard, DocumentCardType, FocusZone, css } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IEventCardProps, IEventCardState } from ".";
|
||||
import { IEventCardProps } from ".";
|
||||
import { DateBox, DateBoxSize } from "../DateBox";
|
||||
import styles from "./EventCard.module.scss";
|
||||
import { Text } from "@microsoft/sp-core-library";
|
||||
import { useCallback } from 'react';
|
||||
/**
|
||||
* Shows an event in a document card
|
||||
*/
|
||||
export class EventCard extends React.Component<IEventCardProps, IEventCardState> {
|
||||
public render(): React.ReactElement<IEventCardProps> {
|
||||
const { isNarrow } = this.props;
|
||||
export const EventCard = (props: IEventCardProps) => {
|
||||
const { isNarrow, themeVariant, isEditMode, event } = props;
|
||||
|
||||
if (isNarrow) {
|
||||
return this._renderNarrowCell();
|
||||
} else {
|
||||
return this._renderNormalCell();
|
||||
}
|
||||
// Get the cell information
|
||||
const { start,
|
||||
end,
|
||||
allDay,
|
||||
title,
|
||||
url,
|
||||
category,
|
||||
location
|
||||
} = event;
|
||||
|
||||
const eventDate: moment.Moment = moment(start);
|
||||
const dateString: string = allDay ? eventDate.format(strings.AllDayDateFormat) : eventDate.format(strings.LocalizedTimeFormat);
|
||||
|
||||
/**
|
||||
* Handle adding to calendar
|
||||
*/
|
||||
const _onAddToMyCalendar = useCallback((): void => {
|
||||
|
||||
// create a calendar to hold the event
|
||||
const cal: ICS.VCALENDAR = new ICS.VCALENDAR();
|
||||
cal.addProp("VERSION", 2.0);
|
||||
cal.addProp("PRODID", "//SPFX//NONSGML v1.0//EN");
|
||||
|
||||
// create an event
|
||||
const icsEvent: ICS.VEVENT = new ICS.VEVENT();
|
||||
|
||||
// generate a unique id
|
||||
icsEvent.addProp("UID", Guid.newGuid().toString());
|
||||
|
||||
// if the event is all day, just pass the date component
|
||||
if (event.allDay) {
|
||||
icsEvent.addProp("DTSTAMP", event.start, { VALUE: "DATE" });
|
||||
icsEvent.addProp("DTSTART", event.start, { VALUE: "DATE" });
|
||||
} else {
|
||||
icsEvent.addProp("DTSTAMP", event.start, { VALUE: "DATE-TIME" });
|
||||
icsEvent.addProp("DTSTART", event.start, { VALUE: "DATE-TIME" });
|
||||
icsEvent.addProp("DTEND", event.start, { VALUE: "DATE-TIME" });
|
||||
}
|
||||
|
||||
private _renderNormalCell(): JSX.Element {
|
||||
const { start,
|
||||
end,
|
||||
allDay,
|
||||
title,
|
||||
url,
|
||||
category,
|
||||
// description,
|
||||
location } = this.props.event;
|
||||
const eventDate: moment.Moment = moment(start);
|
||||
const dateString: string = allDay ? eventDate.format(strings.AllDayDateFormat) : eventDate.format(strings.LocalizedTimeFormat);
|
||||
const { isEditMode } = this.props;
|
||||
return (
|
||||
// add a title
|
||||
icsEvent.addProp("SUMMARY", event.title);
|
||||
|
||||
// add a url if there is one
|
||||
if (event.url !== undefined) {
|
||||
icsEvent.addProp("URL", event.url);
|
||||
}
|
||||
|
||||
// add a description if there is one
|
||||
if (event.description !== undefined) {
|
||||
icsEvent.addProp("DESCRIPTION", event.description);
|
||||
}
|
||||
|
||||
// add a location if there is one
|
||||
if (event.location !== undefined) {
|
||||
icsEvent.addProp("LOCATION", event.location);
|
||||
}
|
||||
|
||||
// add the event to the calendar
|
||||
cal.addComponent(icsEvent);
|
||||
|
||||
// export the calendar
|
||||
// my spidey senses are telling me that there are sitaations where this isn't going to work, but none of my tests could prove it.
|
||||
// i suspect we're not encoding events properly
|
||||
window.open("data:text/calendar;charset=utf8," + encodeURIComponent(cal.toString()));
|
||||
}, [event]);
|
||||
|
||||
if (isNarrow) {
|
||||
// Calculate the date and string format
|
||||
|
||||
// Define theme variant styles if themevariant was passed
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={css(styles.cardWrapper, styles.compactCard, styles.root, styles.rootIsCompact)}
|
||||
style={themeVariant && { backgroundColor: themeVariant.semanticColors.bodyBackground }}
|
||||
data-is-focusable={true}
|
||||
data-is-focus-item={true}
|
||||
role="listitem"
|
||||
aria-label={Text.format(strings.EventCardWrapperArialLabel, title, dateString)}
|
||||
>
|
||||
<DocumentCard
|
||||
className={css(styles.root, styles.rootIsActionable, styles.rootIsCompact)}
|
||||
type={DocumentCardType.compact}
|
||||
style={themeVariant && { backgroundColor: themeVariant.semanticColors.bodyBackground }}
|
||||
onClickHref={url}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={css(styles.cardWrapper)}
|
||||
data-is-focusable={true}
|
||||
data-is-focus-item={true}
|
||||
role="listitem"
|
||||
aria-label={Text.format(strings.EventCardWrapperArialLabel, title, `${dateString}`)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<DocumentCard
|
||||
className={css(styles.root, !isEditMode && styles.rootIsActionable, styles.normalCard)}
|
||||
type={DocumentCardType.normal}
|
||||
onClickHref={isEditMode ? null : url}
|
||||
>
|
||||
<FocusZone>
|
||||
<div className={styles.dateBoxContainer} style={{ height: 160 }} data-automation-id="normal-card-preview">
|
||||
<DateBox
|
||||
className={styles.dateBox}
|
||||
startDate={start}
|
||||
endDate={end}
|
||||
size={DateBoxSize.Medium}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.detailsContainer}>
|
||||
<div className={styles.category}>{category}</div>
|
||||
<div className={styles.title} data-automation-id="event-card-title">{title}</div>
|
||||
<div className={styles.datetime}>{dateString}</div>
|
||||
<div className={styles.location}>{location}</div>
|
||||
<ActionButton
|
||||
className={styles.addToMyCalendar}
|
||||
iconProps={{ iconName: "AddEvent" }}
|
||||
ariaLabel={strings.AddToCalendarAriaLabel}
|
||||
onClick={this._onAddToMyCalendar}
|
||||
>
|
||||
{strings.AddToCalendarButtonLabel}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</FocusZone>
|
||||
</DocumentCard>
|
||||
</div>
|
||||
<DateBox
|
||||
className={styles.dateBox}
|
||||
startDate={start}
|
||||
endDate={end}
|
||||
size={DateBoxSize.Small}
|
||||
themeVariant={themeVariant}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _renderNarrowCell(): JSX.Element {
|
||||
const { start,
|
||||
end,
|
||||
allDay,
|
||||
title,
|
||||
url,
|
||||
// category,
|
||||
// location
|
||||
} = this.props.event;
|
||||
const eventDate: moment.Moment = moment.utc(start);
|
||||
const dateString: string = allDay ? eventDate.format(strings.AllDayDateFormat) : eventDate.format(strings.LocalizedTimeFormat);
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={css(styles.cardWrapper, styles.compactCard, styles.root, styles.rootIsCompact)}
|
||||
data-is-focusable={true}
|
||||
data-is-focus-item={true}
|
||||
role="listitem"
|
||||
aria-label={Text.format(strings.EventCardWrapperArialLabel, title, dateString)}
|
||||
>
|
||||
<DocumentCard
|
||||
className={css(styles.root, styles.rootIsActionable, styles.rootIsCompact)}
|
||||
type={DocumentCardType.compact}
|
||||
onClickHref={url}
|
||||
>
|
||||
<div data-automation-id="normal-card-preview">
|
||||
<DateBox
|
||||
className={styles.dateBox}
|
||||
startDate={start}
|
||||
endDate={end}
|
||||
size={DateBoxSize.Small}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.title} data-automation-id="event-card-title">{title}</div>
|
||||
<div className={styles.datetime}>{dateString}</div>
|
||||
</div>
|
||||
</DocumentCard>
|
||||
</div>
|
||||
<div className={styles.title} style={themeVariant && { color: themeVariant.semanticColors.bodyText }}>{title}</div>
|
||||
<div className={styles.datetime} style={themeVariant && { color: themeVariant.semanticColors.bodySubtext }}>{dateString}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _onAddToMyCalendar = (): void => {
|
||||
const { event } = this.props;
|
||||
|
||||
// create a calendar to hold the event
|
||||
const cal: ICS.VCALENDAR = new ICS.VCALENDAR();
|
||||
cal.addProp("VERSION", 2.0);
|
||||
cal.addProp("PRODID", "//SPFX//NONSGML v1.0//EN");
|
||||
|
||||
// create an event
|
||||
const icsEvent: ICS.VEVENT = new ICS.VEVENT();
|
||||
|
||||
// generate a unique id
|
||||
icsEvent.addProp("UID", Guid.newGuid().toString());
|
||||
|
||||
// if the event is all day, just pass the date component
|
||||
if (event.allDay) {
|
||||
icsEvent.addProp("DTSTAMP", event.start, { VALUE: "DATE" });
|
||||
icsEvent.addProp("DTSTART", event.start, { VALUE: "DATE" });
|
||||
} else {
|
||||
icsEvent.addProp("DTSTAMP", event.start, { VALUE: "DATE-TIME" });
|
||||
icsEvent.addProp("DTSTART", event.start, { VALUE: "DATE-TIME" });
|
||||
icsEvent.addProp("DTEND", event.start, { VALUE: "DATE-TIME" });
|
||||
}
|
||||
|
||||
// add a title
|
||||
icsEvent.addProp("SUMMARY", event.title);
|
||||
|
||||
// add a url if there is one
|
||||
if (event.url !== undefined) {
|
||||
icsEvent.addProp("URL", event.url);
|
||||
}
|
||||
|
||||
// add a description if there is one
|
||||
if (event.description !== undefined) {
|
||||
icsEvent.addProp("DESCRIPTION", event.description);
|
||||
}
|
||||
|
||||
// add a location if there is one
|
||||
if (event.location !== undefined) {
|
||||
icsEvent.addProp("LOCATION", event.location);
|
||||
}
|
||||
|
||||
// add the event to the calendar
|
||||
cal.addComponent(icsEvent);
|
||||
|
||||
// export the calendar
|
||||
// my spidey senses are telling me that there are sitaations where this isn't going to work, but none of my tests could prove it.
|
||||
// i suspect we're not encoding events properly
|
||||
window.open("data:text/calendar;charset=utf8," + encodeURIComponent(cal.toString()));
|
||||
}
|
||||
}
|
||||
</DocumentCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={css(styles.cardWrapper)}
|
||||
style={themeVariant && { backgroundColor: themeVariant.semanticColors.bodyBackground }}
|
||||
data-is-focusable={true}
|
||||
data-is-focus-item={true}
|
||||
role="listitem"
|
||||
aria-label={Text.format(strings.EventCardWrapperArialLabel, title, `${dateString}`)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<DocumentCard
|
||||
className={css(styles.root, !isEditMode && styles.rootIsActionable, styles.normalCard)}
|
||||
type={DocumentCardType.normal}
|
||||
onClickHref={isEditMode ? null : url}
|
||||
style={themeVariant && { borderColor: themeVariant.semanticColors.bodyDivider }}
|
||||
>
|
||||
<FocusZone>
|
||||
<div className={styles.dateBoxContainer} style={{ height: 160 }}>
|
||||
<DateBox
|
||||
className={styles.dateBox}
|
||||
startDate={start}
|
||||
endDate={end}
|
||||
size={DateBoxSize.Medium}
|
||||
themeVariant={themeVariant}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.detailsContainer}>
|
||||
<div className={styles.category} style={themeVariant && { color: themeVariant.semanticColors.bodySubtext }}>{category}</div>
|
||||
<div className={styles.title} style={themeVariant && { color: themeVariant.semanticColors.bodyText }}>{title}</div>
|
||||
<div className={styles.datetime} style={themeVariant && { color: themeVariant.semanticColors.bodySubtext }}>{dateString}</div>
|
||||
<div className={styles.location} style={themeVariant && { color: themeVariant.semanticColors.bodySubtext }}>{location}</div>
|
||||
<ActionButton
|
||||
className={styles.addToMyCalendar}
|
||||
style={themeVariant && { color: themeVariant.semanticColors.bodyText }}
|
||||
iconProps={{ iconName: "AddEvent" }}
|
||||
ariaLabel={strings.AddToCalendarAriaLabel}
|
||||
onClick={_onAddToMyCalendar}
|
||||
>
|
||||
{strings.AddToCalendarButtonLabel}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</FocusZone>
|
||||
</DocumentCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -1,9 +0,0 @@
|
||||
import { ICalendarEvent } from "../../../shared/services/CalendarService";
|
||||
|
||||
export interface IEventCardProps {
|
||||
isEditMode: boolean;
|
||||
event: ICalendarEvent;
|
||||
isNarrow: boolean;
|
||||
}
|
||||
|
||||
export interface IEventCardState {}
|
@ -0,0 +1,9 @@
|
||||
import { ICalendarEvent } from "../../services/CalendarService";
|
||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
|
||||
export interface IEventCardProps {
|
||||
isEditMode: boolean;
|
||||
event: ICalendarEvent;
|
||||
isNarrow: boolean;
|
||||
themeVariant?: IReadonlyTheme;
|
||||
}
|
@ -1,2 +1,2 @@
|
||||
export * from "./EventCard";
|
||||
export * from "./EventCard.types";
|
||||
export * from "./IEventCardProps";
|
||||
|
@ -1,9 +1,7 @@
|
||||
export interface IPagingProps {
|
||||
export interface IPaginationProps {
|
||||
currentPage: number;
|
||||
totalItems: number;
|
||||
itemsCountPerPage: number;
|
||||
showPageNum: boolean;
|
||||
onPageUpdate: (pageNumber: number) => void;
|
||||
}
|
||||
|
||||
export interface IPagingState { }
|
@ -0,0 +1,66 @@
|
||||
@import "~office-ui-fabric-react/dist/sass/References.scss";
|
||||
|
||||
.Pagination {
|
||||
width: 100%;
|
||||
min-width: 240px;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 2px 0;
|
||||
border: 1px solid transparent;
|
||||
position: relative;
|
||||
.next,
|
||||
.prev {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
padding: 8px 4px;
|
||||
margin: 0 8px;
|
||||
cursor: pointer;
|
||||
cursor: hand;
|
||||
position: relative;
|
||||
height: 32px !important;
|
||||
display: none;
|
||||
outline: 0;
|
||||
|
||||
&:hover, &:active {
|
||||
color: "[theme:buttonTextCheckedHovered, default: #000]"
|
||||
}
|
||||
}
|
||||
|
||||
&.noPageNum {
|
||||
text-align: left;
|
||||
|
||||
.next {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// .Pagination .next,
|
||||
// .Pagination .prev {
|
||||
// margin: 0;
|
||||
// display: inline-block;
|
||||
// border: none;
|
||||
// }
|
||||
|
||||
// .Pagination button {
|
||||
// font-size: 14px;
|
||||
// font-weight: 400;
|
||||
// padding: 8px 4px;
|
||||
// margin: 0 8px;
|
||||
// cursor: pointer;
|
||||
// cursor: hand;
|
||||
// position: relative;
|
||||
// height: 32px !important;
|
||||
// display: none;
|
||||
// outline: 0;
|
||||
// }
|
@ -0,0 +1,76 @@
|
||||
import { ActionButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
|
||||
import { Icon } from "office-ui-fabric-react/lib/Icon";
|
||||
import { css } from "office-ui-fabric-react/lib/Utilities";
|
||||
import * as React from "react";
|
||||
import { IPaginationProps } from ".";
|
||||
import styles from "./Pagination.module.scss";
|
||||
import * as strings from "CalendarFeedSummaryWebPartStrings";
|
||||
import { useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* A custom pagination control designed to look & feel like Office UI Fabric
|
||||
*/
|
||||
export const Pagination = (props: IPaginationProps) => {
|
||||
const { currentPage, totalItems, itemsCountPerPage } = props;
|
||||
|
||||
// calculate the page situation
|
||||
const numberOfPages: number = Math.round(totalItems / itemsCountPerPage);
|
||||
|
||||
// we disable the previous button if we're on page 1
|
||||
const prevDisabled: boolean = currentPage <= 1;
|
||||
|
||||
// we disable the next button if we're on the last page
|
||||
const nextDisabled: boolean = currentPage >= numberOfPages;
|
||||
|
||||
|
||||
/**
|
||||
* Increments the page number unless we're on the last page
|
||||
*/
|
||||
const _nextPage = useCallback((): void => {
|
||||
if (props.currentPage < numberOfPages) {
|
||||
props.onPageUpdate(props.currentPage + 1);
|
||||
}
|
||||
}, [props, numberOfPages]);
|
||||
|
||||
/**
|
||||
* Decrements the page number unless we're on the first page
|
||||
*/
|
||||
const _prevPage = useCallback((): void => {
|
||||
if (props.currentPage > 1) {
|
||||
props.onPageUpdate(props.currentPage - 1);
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
return (
|
||||
<div className={css(styles.Pagination, props.showPageNum ? null : styles.noPageNum)}>
|
||||
<ActionButton className={styles.prev}
|
||||
onRenderIcon={(_props: IButtonProps) => {
|
||||
// we use the render custom icon method to render the icon consistently with the right icon
|
||||
return (
|
||||
<Icon iconName="ChevronLeft" />
|
||||
);
|
||||
}}
|
||||
disabled={prevDisabled}
|
||||
onClick={_prevPage}
|
||||
ariaLabel={strings.PrevButtonAriaLabel}
|
||||
>
|
||||
{strings.PrevButtonLabel}
|
||||
</ActionButton>
|
||||
{/* NOT IMPLEMENTED: Page numbers aren't shown here, but we'll need them if we want this control to be reusable */}
|
||||
<ActionButton className={styles.next}
|
||||
data-automation-id="nextPage"
|
||||
disabled={nextDisabled}
|
||||
onRenderMenuIcon={(_props: IButtonProps) => {
|
||||
// we use the render custom menu icon method to render the icon to the right of the text
|
||||
return (
|
||||
<Icon iconName="ChevronRight" />
|
||||
);
|
||||
}}
|
||||
onClick={_nextPage}
|
||||
ariaLabel={strings.NextButtonAriaLabel}
|
||||
>
|
||||
{strings.NextButtonLabel}
|
||||
</ActionButton>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export * from "./Pagination";
|
||||
export * from "./IPagingProps";
|
@ -1,59 +0,0 @@
|
||||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
.Paging {
|
||||
width: 100%;
|
||||
min-width: 240px;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 2px 0;
|
||||
border: 1px solid transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Paging .next,
|
||||
.Paging .prev {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.Paging button {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
padding: 8px 4px;
|
||||
margin: 0 8px;
|
||||
cursor: pointer;
|
||||
cursor: hand;
|
||||
position: relative;
|
||||
height: 32px!important;
|
||||
display: none;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.Paging.noPageNum {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.noPageNum .next {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.Paging .next,
|
||||
.Paging .prev {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.Paging button {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
padding: 8px 4px;
|
||||
margin: 0 8px;
|
||||
cursor: pointer;
|
||||
cursor: hand;
|
||||
position: relative;
|
||||
height: 32px!important;
|
||||
display: none;
|
||||
outline: 0;
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
import { ActionButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
|
||||
import { Icon } from "office-ui-fabric-react/lib/Icon";
|
||||
import { css } from "office-ui-fabric-react/lib/Utilities";
|
||||
import * as React from "react";
|
||||
import { IPagingProps, IPagingState } from ".";
|
||||
import styles from "./Paging.module.scss";
|
||||
import * as strings from "CalendarFeedSummaryWebPartStrings";
|
||||
|
||||
/**
|
||||
* A custom pagination control designed to look & feel like Office UI Fabric
|
||||
*/
|
||||
export class Paging extends React.Component<IPagingProps, IPagingState> {
|
||||
public render(): React.ReactElement<IPagingProps> {
|
||||
|
||||
const { currentPage } = this.props;
|
||||
|
||||
// calculate the page situation
|
||||
const numberOfPages: number = this._getNumberOfPages();
|
||||
|
||||
// we disable the previous button if we're on page 1
|
||||
const prevDisabled: boolean = currentPage < 1;
|
||||
|
||||
// we disable the next button if we're on the last page
|
||||
const nextDisabled: boolean = currentPage >= numberOfPages;
|
||||
|
||||
return (
|
||||
<div className={css(styles.Paging, this.props.showPageNum ? null : styles.noPageNum)}>
|
||||
<ActionButton className={styles.prev}
|
||||
data-automation-id="previousPage"
|
||||
onRenderIcon={(props: IButtonProps) => {
|
||||
// we use the render custom icon method to render the icon consistently with the right icon
|
||||
return (
|
||||
<Icon iconName="ChevronLeft" />
|
||||
);
|
||||
}}
|
||||
disabled={prevDisabled}
|
||||
onClick={this._prevPage}
|
||||
ariaLabel={strings.PrevButtonAriaLabel}
|
||||
>
|
||||
{strings.PrevButtonLabel}
|
||||
</ActionButton>
|
||||
{/* NOT IMPLEMENTED: Page numbers aren't shown here, but we'll need them if we want this control to be reusable */}
|
||||
<ActionButton className={styles.next}
|
||||
data-automation-id="nextPage"
|
||||
disabled={nextDisabled}
|
||||
onRenderMenuIcon={(props: IButtonProps) => {
|
||||
// we use the render custom menu icon method to render the icon to the right of the text
|
||||
return (
|
||||
<Icon iconName="ChevronRight" />
|
||||
);
|
||||
}}
|
||||
onClick={this._nextPage}
|
||||
ariaLabel={strings.NextButtonAriaLabel}
|
||||
>
|
||||
{strings.NextButtonLabel}
|
||||
</ActionButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the page number unless we're on the last page
|
||||
*/
|
||||
private _nextPage = (): void => {
|
||||
const numberOfPages: number = this._getNumberOfPages();
|
||||
if (this.props.currentPage < numberOfPages) {
|
||||
this.props.onPageUpdate(this.props.currentPage + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrements the page number unless we're on the first page
|
||||
*/
|
||||
private _prevPage = (): void => {
|
||||
if (this.props.currentPage > 1) {
|
||||
this.props.onPageUpdate(this.props.currentPage - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates how many pages there will be
|
||||
*/
|
||||
private _getNumberOfPages(): number {
|
||||
const { totalItems, itemsCountPerPage } = this.props;
|
||||
let numPages: number = Math.round(totalItems / itemsCountPerPage);
|
||||
return numPages;
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from "./Paging";
|
||||
export * from "./Paging.types";
|
@ -1,5 +1,4 @@
|
||||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
@import "~office-ui-fabric-react/dist/sass/References.scss";
|
||||
:export {
|
||||
centerPadding: 50px;
|
||||
}
|
||||
@ -8,7 +7,9 @@
|
||||
position: relative;
|
||||
|
||||
&.filmStrip {
|
||||
margin: 0 -10px;
|
||||
margin-bottom: 27px;
|
||||
margin-left: -10px;
|
||||
margin-right: -10px;
|
||||
|
||||
:global(.slick-slide) {
|
||||
box-sizing: border-box;
|
||||
@ -20,14 +21,6 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sliderButtonRight {
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.sliderButtonLeft {
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
&:hover .sliderButtons {
|
||||
opacity: 1;
|
||||
|
||||
@ -45,8 +38,8 @@
|
||||
}
|
||||
|
||||
.indexButton {
|
||||
font-size: 17px;
|
||||
font-weight: 300;
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
@ -67,6 +60,8 @@
|
||||
|
||||
&:hover {
|
||||
color: $ms-color-white;
|
||||
background-color: $ms-color-black;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@ -74,6 +69,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
.carouselDotsContainer {
|
||||
.carouselDot {
|
||||
display: inline-block;
|
||||
background-color: $ms-color-black;
|
||||
height: 4px;
|
||||
width: 4px;
|
||||
opacity: 0.5;
|
||||
border: 2px solid $ms-color-black;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
opacity: 0.25;
|
||||
outline: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.slick-active) {
|
||||
.carouselDotsContainer {
|
||||
.carouselDot {
|
||||
background-color: "[theme:themeDark, default: #005a9e]";
|
||||
opacity: 0.75;
|
||||
border-color: "[theme:themeDark, default: #005a9e]";
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.indexButton:global(.ms-Button-flexContainer):hover:global(.ms-Icon),
|
||||
.indexButton:global(.ms-Icon:hover),
|
||||
.indexButton:hover:global(.ms-Icon) {
|
||||
|
@ -1,114 +1,117 @@
|
||||
import { css } from '@uifabric/utilities/lib/css';
|
||||
import { IconButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import * as React from 'react';
|
||||
// import * as slick from 'slick-carousel';
|
||||
import Slider from 'react-slick';
|
||||
import { IFilmstripLayoutProps, IFilmstripLayoutState } from "./FilmstripLayout.types";
|
||||
|
||||
|
||||
import { SPComponentLoader } from '@microsoft/sp-loader';
|
||||
import styles from "./FilmstripLayout.module.scss";
|
||||
import { useRef } from 'react';
|
||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
|
||||
function useBreakpoints(currentWidth: number, breakpoints: number[]) {
|
||||
return breakpoints.map(breakpoint => currentWidth < breakpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filmstrip layout
|
||||
* Presents the child compoments as a slick slide
|
||||
*/
|
||||
export class FilmstripLayout extends React.Component<
|
||||
IFilmstripLayoutProps,
|
||||
IFilmstripLayoutState
|
||||
> {
|
||||
// the slick slider used in normal views
|
||||
private _slider: Slider;
|
||||
export const FilmstripLayout = (props: { children: any; clientWidth: number; themeVariant?: IReadonlyTheme, ariaLabel?: string; }) => {
|
||||
SPComponentLoader.loadCss('https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.8.1/slick.min.css');
|
||||
SPComponentLoader.loadCss('https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.8.1/slick-theme.min.css');
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor(props: IFilmstripLayoutProps) {
|
||||
super(props);
|
||||
let topElem: React.MutableRefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
|
||||
let _slider: React.MutableRefObject<Slider> = useRef<Slider>(null);
|
||||
|
||||
SPComponentLoader.loadCss('https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.6.0/slick.min.css');
|
||||
SPComponentLoader.loadCss('https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.6.0/slick-theme.min.css');
|
||||
const [isSmall, isMedium] = useBreakpoints(props.clientWidth, [696, 928]);
|
||||
|
||||
let numSlides: number = 3;
|
||||
if (isSmall) {
|
||||
numSlides = 2;
|
||||
} else if (isMedium) {
|
||||
numSlides = 3;
|
||||
} else {
|
||||
numSlides = 4;
|
||||
}
|
||||
/**
|
||||
* Renders a slick switch, a slide for each child, and next/previous arrows
|
||||
*/
|
||||
public render(): React.ReactElement<IFilmstripLayoutProps> {
|
||||
// slick seems to have an issue with having "infinite" mode set to true and having less items than the number of slides per page
|
||||
// set infinite to true only if there are more than 3 children
|
||||
var isInfinite: boolean = React.Children.count(this.props.children) > 3;
|
||||
var settings: any = {
|
||||
accessibility: true,
|
||||
arrows: false,
|
||||
autoplaySpeed: 5000,
|
||||
dots: true,
|
||||
infinite: isInfinite,
|
||||
slidesToShow: 4,
|
||||
slidesToScroll: 4,
|
||||
speed: 500,
|
||||
centerPadding: styles.centerPadding,
|
||||
pauseOnHover: true,
|
||||
variableWidth: false,
|
||||
useCSS: true,
|
||||
rows: 1,
|
||||
respondTo: "slider",
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 499,
|
||||
settings: {
|
||||
slidesToShow: 1,
|
||||
slidesToScroll: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
breakpoint: 731,
|
||||
settings: {
|
||||
slidesToShow: 2,
|
||||
slidesToScroll: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
breakpoint: 963,
|
||||
settings: {
|
||||
slidesToShow: 3,
|
||||
slidesToScroll: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
breakpoint: 1028,
|
||||
settings: {
|
||||
slidesToShow: 4,
|
||||
slidesToScroll: 4
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return (
|
||||
var isInfinite: boolean = React.Children.count(props.children) > numSlides;
|
||||
var settings: any = {
|
||||
accessibility: true,
|
||||
arrows: false,
|
||||
autoplaySpeed: 5000,
|
||||
dots: true,
|
||||
customPaging: (i: number) => {
|
||||
return (
|
||||
<a>
|
||||
<div role="button" className={styles.carouselDotsContainer} aria-label={`Carousel Dot ${i}`} data-is-focusable={true} tabIndex={0}>
|
||||
<span className={styles.carouselDot} tabIndex={-1}></span>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
},
|
||||
infinite: isInfinite,
|
||||
slidesToShow: numSlides,
|
||||
slidesToScroll: numSlides,
|
||||
speed: 500,
|
||||
centerPadding: styles.centerPadding,
|
||||
pauseOnHover: true,
|
||||
variableWidth: false,
|
||||
useCSS: true,
|
||||
rows: 1,
|
||||
respondTo: "slider",
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*
|
||||
KLUDGE:
|
||||
This is a cheaty way to inject styles from the theme variant when the component does not support theme variant customizations.
|
||||
We can do this without too much nastiness because this component is added only once per web part
|
||||
... but I still wish there was a better way
|
||||
*/}
|
||||
{props.themeVariant && <style>{`
|
||||
.${styles.carouselDot} {
|
||||
background-color: ${props.themeVariant.palette.black}!important;
|
||||
border-color: ${props.themeVariant.palette.black}!important;
|
||||
}
|
||||
|
||||
.slick-active .${styles.carouselDot} {
|
||||
background-color: ${props.themeVariant.palette.themeDark}!important;
|
||||
border-color: ${props.themeVariant.palette.themeDark}!important;
|
||||
}
|
||||
|
||||
.${styles.filmstripLayout} .ms-DocumentCard--actionable:hover {
|
||||
border-color: ${props.themeVariant.semanticColors.variantBorderHovered}!important;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
}
|
||||
<div>
|
||||
<div className={css(styles.filmstripLayout, styles.filmStrip)} aria-label={this.props.ariaLabel}>
|
||||
<Slider ref={c => (this._slider = c)} {...settings}>
|
||||
{this.props.children}
|
||||
<div className={css(styles.filmstripLayout, styles.filmStrip)} aria-label={props.ariaLabel} ref={topElem}>
|
||||
<Slider ref={_slider} {...settings}>
|
||||
{props.children}
|
||||
</Slider>
|
||||
<div
|
||||
className={css(styles.indexButtonContainer, styles.sliderButtons, styles.sliderButtonLeft)}
|
||||
onClick={() => this._slider.slickPrev()}
|
||||
className={css(styles.indexButtonContainer, styles.sliderButtons)}
|
||||
style={{ left: "10px" }}
|
||||
onClick={() => _slider.current.slickPrev()}
|
||||
>
|
||||
<IconButton
|
||||
className={css(styles.indexButton, styles.leftPositioned)}
|
||||
iconProps={{ iconName: "ChevronLeft" }}
|
||||
iconProps={{ iconName: "ChevronLeft", styles: { root: { fontSize: '28px', fontWeight: '400' } } }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={css(styles.indexButtonContainer, styles.sliderButtons, styles.sliderButtonRight)}
|
||||
onClick={() => this._slider.slickNext()}
|
||||
className={css(styles.indexButtonContainer, styles.sliderButtons)}
|
||||
style={{ right: "10px" }}
|
||||
onClick={() => _slider.current.slickNext()}
|
||||
>
|
||||
<IconButton
|
||||
className={css(styles.indexButton, styles.rightPositioned)}
|
||||
iconProps={{ iconName: "ChevronRight" }}
|
||||
iconProps={{ iconName: "ChevronRight", styles: { root: { fontSize: '28px', fontWeight: '400' } } }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +0,0 @@
|
||||
export interface IFilmstripLayoutProps {
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export interface IFilmstripLayoutState { }
|
@ -1,2 +1 @@
|
||||
export * from "./FilmstripLayout";
|
||||
export * from "./FilmstripLayout.types";
|
||||
|
@ -19,6 +19,16 @@ const sampleEvents: ICalendarEvent[] = [
|
||||
location: "Barrie, ON",
|
||||
description: "This is a description"
|
||||
},
|
||||
{
|
||||
title: "This is a UTC event",
|
||||
start: moment().add(1, "d").utc(false).toDate(),
|
||||
end: moment().add(1, "d").add(1, "h").utc(false).toDate(),
|
||||
url: "https://www.contoso.com/news-events/events/1UTC/",
|
||||
allDay: false,
|
||||
category: "Meeting",
|
||||
location: "Barrie, ON",
|
||||
description: "This is a description for a UTC event"
|
||||
},
|
||||
{
|
||||
title: "This event will be in one week",
|
||||
start: moment().add(1, "w").toDate(),
|
||||
@ -151,7 +161,7 @@ export class MockCalendarService extends BaseCalendarService implements ICalenda
|
||||
return new Promise<ICalendarEvent[]>((resolve: any) => {
|
||||
setTimeout(() => {
|
||||
resolve(this.filterEventRange(sampleEvents));
|
||||
}, 1000);
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
"id": "acef6c3f-852a-42e3-8a4b-7ea191ae9687",
|
||||
"alias": "CalendarFeedSummaryWebPart",
|
||||
"componentType": "WebPart",
|
||||
|
||||
"supportsThemeVariants": true,
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
"supportedHosts": ["SharePointWebPart"],
|
||||
|
@ -34,8 +34,8 @@ import { ICalendarFeedSummaryWebPartProps } from "./CalendarFeedSummaryWebPart.t
|
||||
import CalendarFeedSummary from "./components/CalendarFeedSummary";
|
||||
import { ICalendarFeedSummaryProps } from "./components/CalendarFeedSummary.types";
|
||||
|
||||
// this is the same width that the SharePoint events web parts use to render as narrow
|
||||
const MaxMobileWidth: number = 480;
|
||||
// Support for theme variants
|
||||
import { ThemeProvider, ThemeChangedEventArgs, IReadonlyTheme, ISemanticColors } from '@microsoft/sp-component-base';
|
||||
|
||||
/**
|
||||
* Calendar Feed Summary Web Part
|
||||
@ -46,6 +46,8 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
|
||||
// the list of proviers available
|
||||
private _providerList: any[];
|
||||
|
||||
private _themeProvider: ThemeProvider;
|
||||
private _themeVariant: IReadonlyTheme | undefined;
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@ -55,6 +57,14 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
|
||||
|
||||
protected onInit(): Promise<void> {
|
||||
return new Promise<void>((resolve, _reject) => {
|
||||
// Consume the new ThemeProvider service
|
||||
this._themeProvider = this.context.serviceScope.consume(ThemeProvider.serviceKey);
|
||||
|
||||
// If it exists, get the theme variant
|
||||
this._themeVariant = this._themeProvider.tryGetTheme();
|
||||
|
||||
// Register a handler to be notified if the theme variant changes
|
||||
this._themeProvider.themeChangedEvent.add(this, this._handleThemeChangedEvent);
|
||||
|
||||
let {
|
||||
cacheDuration,
|
||||
@ -90,8 +100,8 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
|
||||
* Renders the web part
|
||||
*/
|
||||
public render(): void {
|
||||
// see if we need to render a mobile view
|
||||
const isNarrow: boolean = this.domElement.clientWidth <= MaxMobileWidth;
|
||||
// We pass the width so that the components can resize
|
||||
const { clientWidth } = this.domElement;
|
||||
|
||||
// display the summary (or the configuration screen)
|
||||
const element: React.ReactElement<ICalendarFeedSummaryProps> = React.createElement(
|
||||
@ -101,12 +111,13 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
|
||||
displayMode: this.displayMode,
|
||||
context: this.context,
|
||||
isConfigured: this._isConfigured(),
|
||||
isNarrow: isNarrow,
|
||||
maxEvents: this.properties.maxEvents,
|
||||
provider: this._getDataProvider(),
|
||||
themeVariant: this._themeVariant,
|
||||
updateProperty: (value: string) => {
|
||||
this.properties.title = value;
|
||||
},
|
||||
clientWidth: clientWidth
|
||||
}
|
||||
);
|
||||
|
||||
@ -355,4 +366,14 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
|
||||
provider.MaxTotal = maxTotal;
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current theme variant reference and re-render.
|
||||
*
|
||||
* @param args The new theme
|
||||
*/
|
||||
private _handleThemeChangedEvent(args: ThemeChangedEventArgs): void {
|
||||
this._themeVariant = args.theme;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
@ -9,12 +9,13 @@
|
||||
}
|
||||
|
||||
.webPartChrome {
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-align: stretch;
|
||||
align-items: stretch;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
// display: -ms-flexbox;
|
||||
// display: flex;
|
||||
// -ms-flex-align: stretch;
|
||||
// align-items: stretch;
|
||||
// -ms-flex-direction: column;
|
||||
// flex-direction: column;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.headerSmMargin {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { DisplayMode } from "@microsoft/sp-core-library";
|
||||
import { SPComponentLoader } from "@microsoft/sp-loader";
|
||||
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
|
||||
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
|
||||
import * as strings from "CalendarFeedSummaryWebPartStrings";
|
||||
@ -7,15 +6,19 @@ import * as moment from "moment";
|
||||
import { FocusZone, FocusZoneDirection, List, Spinner, css } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { EventCard } from "../../../shared/components/EventCard";
|
||||
import { Paging } from "../../../shared/components/Paging";
|
||||
import { Pagination } from "../../../shared/components/Pagination";
|
||||
import { CalendarServiceProviderType, ICalendarEvent, ICalendarService } from "../../../shared/services/CalendarService";
|
||||
import styles from "./CalendarFeedSummary.module.scss";
|
||||
import { ICalendarFeedSummaryProps, ICalendarFeedSummaryState, IFeedCache } from "./CalendarFeedSummary.types";
|
||||
import { FilmstripLayout } from "../../../shared/components/filmstripLayout/index";
|
||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
|
||||
// the key used when caching events
|
||||
const CacheKey: string = "calendarFeedSummary";
|
||||
|
||||
// this is the same width that the SharePoint events web parts use to render as narrow
|
||||
const MaxMobileWidth: number = 480;
|
||||
|
||||
/**
|
||||
* Displays a feed summary from a given calendar feed provider. Renders a different view for mobile/narrow web parts.
|
||||
*/
|
||||
@ -90,6 +93,8 @@ export default class CalendarFeedSummary extends React.Component<ICalendarFeedSu
|
||||
isConfigured,
|
||||
} = this.props;
|
||||
|
||||
const { semanticColors }: IReadonlyTheme = this.props.themeVariant;
|
||||
|
||||
// if we're not configured, show the placeholder
|
||||
if (!isConfigured) {
|
||||
return <Placeholder
|
||||
@ -104,7 +109,7 @@ export default class CalendarFeedSummary extends React.Component<ICalendarFeedSu
|
||||
|
||||
// put everything together in a nice little calendar view
|
||||
return (
|
||||
<div className={css(styles.calendarFeedSummary, styles.webPartChrome)}>
|
||||
<div className={css(styles.calendarFeedSummary, styles.webPartChrome)} style={{backgroundColor: semanticColors.bodyBackground}}>
|
||||
<div className={css(styles.webPartHeader, styles.headerSmMargin)}>
|
||||
<WebPartTitle displayMode={this.props.displayMode}
|
||||
title={this.props.title}
|
||||
@ -122,8 +127,9 @@ export default class CalendarFeedSummary extends React.Component<ICalendarFeedSu
|
||||
* Render your web part content
|
||||
*/
|
||||
private _renderContent(): JSX.Element {
|
||||
const isNarrow: boolean = this.props.clientWidth < MaxMobileWidth;
|
||||
|
||||
const {
|
||||
isNarrow,
|
||||
displayMode
|
||||
} = this.props;
|
||||
const {
|
||||
@ -253,14 +259,16 @@ export default class CalendarFeedSummary extends React.Component<ICalendarFeedSu
|
||||
>
|
||||
<List
|
||||
items={pagedEvents}
|
||||
onRenderCell={(item, index) => (
|
||||
onRenderCell={(item, _index) => (
|
||||
<EventCard
|
||||
isEditMode={isEditMode}
|
||||
event={item}
|
||||
isNarrow={true} />
|
||||
isNarrow={true}
|
||||
themeVariant={this.props.themeVariant}
|
||||
/>
|
||||
)} />
|
||||
{usePaging &&
|
||||
<Paging
|
||||
<Pagination
|
||||
showPageNum={false}
|
||||
currentPage={currentPage}
|
||||
itemsCountPerPage={maxEvents}
|
||||
@ -289,13 +297,17 @@ export default class CalendarFeedSummary extends React.Component<ICalendarFeedSu
|
||||
<div role="application">
|
||||
<FilmstripLayout
|
||||
ariaLabel={strings.FilmStripAriaLabel}
|
||||
clientWidth={this.props.clientWidth}
|
||||
themeVariant={this.props.themeVariant}
|
||||
>
|
||||
{events.map((event: ICalendarEvent, index: number) => {
|
||||
return (<EventCard
|
||||
key={`eventCard${index}`}
|
||||
isEditMode={isEditMode}
|
||||
event={event}
|
||||
isNarrow={false} />);
|
||||
isNarrow={false}
|
||||
themeVariant={this.props.themeVariant} />
|
||||
);
|
||||
})}
|
||||
</FilmstripLayout>
|
||||
</div>
|
||||
|
@ -9,6 +9,7 @@ import { DisplayMode } from "@microsoft/sp-core-library";
|
||||
import { IWebPartContext } from "@microsoft/sp-webpart-base";
|
||||
import { Moment } from "moment";
|
||||
import { ICalendarEvent, ICalendarService } from "../../../shared/services/CalendarService";
|
||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
|
||||
/**
|
||||
* The props for the calendar feed summary component
|
||||
@ -19,9 +20,10 @@ export interface ICalendarFeedSummaryProps {
|
||||
context: IWebPartContext;
|
||||
updateProperty: (value: string) => void;
|
||||
isConfigured: boolean;
|
||||
isNarrow: boolean;
|
||||
provider: ICalendarService;
|
||||
maxEvents: number;
|
||||
themeVariant: IReadonlyTheme;
|
||||
clientWidth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json",
|
||||
"compilerOptions": {
|
||||
"inlineSources": false,
|
||||
"strictNullChecks": false,
|
||||
|
Loading…
x
Reference in New Issue
Block a user