Füge Unterstützung für die Anzeige mehrerer aktiver Umfragen als Accordion oder Slider hinzu und aktualisiere die Version auf 1.1.0
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
"pollListId": "",
|
||||
"answerListId": "",
|
||||
"showResultsBeforeVote": false,
|
||||
"displayMode": "slider",
|
||||
"refreshSeconds": 60
|
||||
}
|
||||
}]
|
||||
|
||||
@@ -6,17 +6,17 @@ import {
|
||||
PropertyPaneDropdown, PropertyPaneSlider, PropertyPaneToggle
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
import * as strings from 'DynamicPollWebPartStrings';
|
||||
import DynamicPoll from './components/DynamicPoll';
|
||||
import { IDynamicPollProps } from './components/IDynamicPollProps';
|
||||
import DynamicPollCollection, { IDynamicPollCollectionProps } from './components/DynamicPollCollection';
|
||||
import styles from './components/DynamicPoll.module.scss';
|
||||
import { PollDataService } from './PollDataService';
|
||||
import { IListOption, IPollSnapshot } from './PollTypes';
|
||||
import { IListOption, IPollSnapshot, PollDisplayMode } from './PollTypes';
|
||||
|
||||
export interface IDynamicPollWebPartProps {
|
||||
pollListId: string;
|
||||
answerListId: string;
|
||||
showResultsBeforeVote: boolean;
|
||||
refreshSeconds: number;
|
||||
displayMode: PollDisplayMode;
|
||||
}
|
||||
|
||||
export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPollWebPartProps> {
|
||||
@@ -68,6 +68,13 @@ export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPo
|
||||
groupName: strings.BehaviorGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneToggle('showResultsBeforeVote', { label: strings.ShowResultsBeforeVoteLabel }),
|
||||
PropertyPaneDropdown('displayMode', {
|
||||
label: strings.DisplayModeLabel,
|
||||
options: [
|
||||
{ key: 'accordion', text: strings.AccordionModeLabel },
|
||||
{ key: 'slider', text: strings.SliderModeLabel }
|
||||
]
|
||||
}),
|
||||
PropertyPaneSlider('refreshSeconds', {
|
||||
label: strings.RefreshSecondsLabel, min: 0, max: 300, step: 15, showValue: true
|
||||
})
|
||||
@@ -83,21 +90,25 @@ export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPo
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (showLoading) { this.renderStatus(strings.LoadingMessage, false); }
|
||||
return this.service.getSnapshot(this.properties.pollListId, this.properties.answerListId, this.userKey)
|
||||
.then((snapshot: IPollSnapshot): void => {
|
||||
return this.service.getSnapshots(this.properties.pollListId, this.properties.answerListId, this.userKey)
|
||||
.then((snapshots: IPollSnapshot[]): void => {
|
||||
if (this.disposed || version !== this.requestVersion) { return; }
|
||||
if (!snapshot) { this.renderStatus(strings.NoPollMessage, false); return; }
|
||||
const element: React.ReactElement<IDynamicPollProps> = React.createElement(DynamicPoll, {
|
||||
snapshot: snapshot,
|
||||
if (!snapshots.length) { this.renderStatus(strings.NoPollMessage, false); return; }
|
||||
const pollStrings = {
|
||||
voteButton: strings.VoteButton, votingButton: strings.VotingButton,
|
||||
thanksMessage: strings.ThanksMessage, resultsHeading: strings.ResultsHeading,
|
||||
votesLabel: strings.VotesLabel, oneVoteLabel: strings.OneVoteLabel,
|
||||
selectAnswerMessage: strings.SelectAnswerMessage, voteErrorMessage: strings.VoteErrorMessage,
|
||||
alreadyVotedMessage: strings.AlreadyVotedMessage,
|
||||
previousPollLabel: strings.PreviousPollLabel, nextPollLabel: strings.NextPollLabel,
|
||||
pollNavigationLabel: strings.PollNavigationLabel
|
||||
};
|
||||
const element: React.ReactElement<IDynamicPollCollectionProps> = React.createElement(DynamicPollCollection, {
|
||||
snapshots: snapshots,
|
||||
displayMode: this.properties.displayMode === 'accordion' ? 'accordion' : 'slider',
|
||||
showResultsBeforeVote: this.properties.showResultsBeforeVote === true,
|
||||
strings: {
|
||||
voteButton: strings.VoteButton, votingButton: strings.VotingButton,
|
||||
thanksMessage: strings.ThanksMessage, resultsHeading: strings.ResultsHeading,
|
||||
votesLabel: strings.VotesLabel, oneVoteLabel: strings.OneVoteLabel,
|
||||
selectAnswerMessage: strings.SelectAnswerMessage, voteErrorMessage: strings.VoteErrorMessage,
|
||||
alreadyVotedMessage: strings.AlreadyVotedMessage
|
||||
},
|
||||
onVote: (answer: string): Promise<void> => this.vote(snapshot.poll.id, answer)
|
||||
strings: pollStrings,
|
||||
onVote: (pollId: number, answer: string): Promise<void> => this.vote(pollId, answer)
|
||||
});
|
||||
ReactDom.render(element, this.domElement);
|
||||
}).catch((error: Error): void => {
|
||||
|
||||
@@ -15,6 +15,10 @@ export function parsePollOptionLines(values: string[]): string[] {
|
||||
}
|
||||
|
||||
export function selectActivePoll(polls: IPoll[], now: Date): IPoll | undefined {
|
||||
return selectActivePolls(polls, now)[0];
|
||||
}
|
||||
|
||||
export function selectActivePolls(polls: IPoll[], now: Date): IPoll[] {
|
||||
const timestamp: number = now.getTime();
|
||||
const active: IPoll[] = polls.filter((poll: IPoll): boolean =>
|
||||
poll.isActive && poll.options.length > 0 &&
|
||||
@@ -22,7 +26,7 @@ export function selectActivePoll(polls: IPoll[], now: Date): IPoll | undefined {
|
||||
(!poll.endDate || poll.endDate.getTime() >= timestamp));
|
||||
active.sort((left: IPoll, right: IPoll): number =>
|
||||
dateValue(right.startDate) - dateValue(left.startDate) || right.id - left.id);
|
||||
return active[0];
|
||||
return active;
|
||||
}
|
||||
|
||||
export function calculatePollResults(options: string[], answers: IPollAnswer[]): IPollResult[] {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { calculatePollResults, findUserAnswer, parsePollOptionLines, selectActivePoll } from './PollCore';
|
||||
import { calculatePollResults, findUserAnswer, parsePollOptionLines, selectActivePolls } from './PollCore';
|
||||
import { IListOption, IPoll, IPollAnswer, IPollSnapshot } from './PollTypes';
|
||||
|
||||
export class PollDataService {
|
||||
@@ -15,24 +15,26 @@ export class PollDataService {
|
||||
})));
|
||||
}
|
||||
|
||||
public getSnapshot(pollListId: string, answerListId: string, userKey: string): Promise<IPollSnapshot | undefined> {
|
||||
return this.getActivePoll(pollListId).then((poll: IPoll): Promise<IPollSnapshot | undefined> => {
|
||||
if (!poll) { return Promise.resolve(undefined); }
|
||||
return this.getAnswers(answerListId, poll.id).then((answers: IPollAnswer[]): IPollSnapshot => {
|
||||
const results = calculatePollResults(poll.options, answers);
|
||||
return {
|
||||
poll: poll,
|
||||
results: results,
|
||||
totalVotes: results.reduce((total, result): number => total + result.votes, 0),
|
||||
userAnswer: findUserAnswer(answers, userKey)
|
||||
};
|
||||
});
|
||||
public getSnapshots(pollListId: string, answerListId: string, userKey: string): Promise<IPollSnapshot[]> {
|
||||
return this.getActivePolls(pollListId).then((polls: IPoll[]): Promise<IPollSnapshot[]> => {
|
||||
const requests: Array<Promise<IPollSnapshot>> = polls.map((poll: IPoll): Promise<IPollSnapshot> =>
|
||||
this.getAnswers(answerListId, poll.id).then((answers: IPollAnswer[]): IPollSnapshot => {
|
||||
const results = calculatePollResults(poll.options, answers);
|
||||
return {
|
||||
poll: poll,
|
||||
results: results,
|
||||
totalVotes: results.reduce((total, result): number => total + result.votes, 0),
|
||||
userAnswer: findUserAnswer(answers, userKey)
|
||||
};
|
||||
}));
|
||||
return Promise.all(requests);
|
||||
});
|
||||
}
|
||||
|
||||
public vote(pollListId: string, answerListId: string, pollId: number, answer: string, userKey: string): Promise<void> {
|
||||
return this.getActivePoll(pollListId).then((poll: IPoll): Promise<void> => {
|
||||
if (!poll || poll.id !== pollId || poll.options.indexOf(answer) < 0) {
|
||||
return this.getActivePolls(pollListId).then((polls: IPoll[]): Promise<void> => {
|
||||
const poll: IPoll = polls.filter((candidate: IPoll): boolean => candidate.id === pollId)[0];
|
||||
if (!poll || poll.options.indexOf(answer) < 0) {
|
||||
return Promise.reject(new Error('POLL_NOT_ACTIVE'));
|
||||
}
|
||||
return this.saveVote(answerListId, pollId, answer, userKey);
|
||||
@@ -57,12 +59,12 @@ export class PollDataService {
|
||||
});
|
||||
}
|
||||
|
||||
private getActivePoll(listId: string): Promise<IPoll | undefined> {
|
||||
return this.getOptionsFieldName(listId).then((optionsField: string): Promise<IPoll | undefined> => {
|
||||
private getActivePolls(listId: string): Promise<IPoll[]> {
|
||||
return this.getOptionsFieldName(listId).then((optionsField: string): Promise<IPoll[]> => {
|
||||
const url: string = this.webUrl + "/_api/web/lists(guid'" + this.validateGuid(listId) + "')/items" +
|
||||
'?$select=Id,Title,Question,' + optionsField +
|
||||
',StartDate,EndDate,IsActive&$filter=IsActive eq 1&$orderby=StartDate desc&$top=100';
|
||||
return this.getJson(url).then((payload: any): IPoll | undefined => { // tslint:disable-line:no-any
|
||||
return this.getJson(url).then((payload: any): IPoll[] => { // tslint:disable-line:no-any
|
||||
const polls: IPoll[] = this.results(payload).map((item: any): IPoll => ({ // tslint:disable-line:no-any
|
||||
id: Number(item.Id),
|
||||
title: String(item.Title || ''),
|
||||
@@ -72,7 +74,7 @@ export class PollDataService {
|
||||
endDate: this.toDate(item.EndDate),
|
||||
isActive: item.IsActive === true || item.IsActive === 1
|
||||
}));
|
||||
return selectActivePoll(polls, new Date());
|
||||
return selectActivePolls(polls, new Date());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,4 +16,4 @@ export interface IPollSnapshot {
|
||||
totalVotes: number;
|
||||
userAnswer: string;
|
||||
}
|
||||
|
||||
export type PollDisplayMode = 'accordion' | 'slider';
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
.poll {
|
||||
background: "[theme: white, default: #ffffff]";
|
||||
border: 1px solid #edebe9;
|
||||
box-sizing: border-box;
|
||||
color: "[theme: neutralPrimary, default: #323130]";
|
||||
font-family: "Segoe UI", "Segoe UI Web (West European)", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
|
||||
@@ -67,4 +66,32 @@
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.collection { width: 100%; }
|
||||
.accordionItem { border-bottom: 1px solid "[theme: neutralLight, default: #edebe9]"; }
|
||||
.accordionHeader {
|
||||
align-items: center;
|
||||
background: "[theme: white, default: #ffffff]";
|
||||
border: 0;
|
||||
color: "[theme: neutralPrimary, default: #323130]";
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
justify-content: space-between;
|
||||
min-height: 44px;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
.accordionHeader:hover { background: "[theme: neutralLighter, default: #f3f2f1]"; }
|
||||
.accordionHeader:focus { outline: 2px solid "[theme: themePrimary, default: #0078d4]"; outline-offset: -2px; }
|
||||
.accordionChevron { font-family: 'FabricMDL2Icons'; font-size: 12px; margin-left: 12px; }
|
||||
.accordionPanel .poll { padding-top: 14px; }
|
||||
.sliderNavigation { align-items: center; display: flex; justify-content: center; margin-top: 8px; }
|
||||
.sliderArrow { background: transparent; border: 0; color: "[theme: themePrimary, default: #0078d4]"; cursor: pointer; font-size: 28px; height: 32px; line-height: 24px; padding: 0; width: 36px; }
|
||||
.sliderArrow:hover { background: "[theme: neutralLighter, default: #f3f2f1]"; }
|
||||
.sliderArrow:focus { outline: 2px solid "[theme: themePrimary, default: #0078d4]"; }
|
||||
.pagination { align-items: center; display: flex; gap: 8px; padding: 0 8px; }
|
||||
.paginationDot, .paginationDotActive { background: "[theme: neutralTertiary, default: #a19f9d]"; border: 0; border-radius: 50%; cursor: pointer; height: 8px; padding: 0; width: 8px; }
|
||||
.paginationDotActive { background: "[theme: themePrimary, default: #0078d4]"; height: 10px; width: 10px; }
|
||||
.paginationDot:focus, .paginationDotActive:focus { outline: 2px solid "[theme: themePrimary, default: #0078d4]"; outline-offset: 2px; }
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import * as React from 'react';
|
||||
import { IPollSnapshot, PollDisplayMode } from '../PollTypes';
|
||||
import DynamicPoll from './DynamicPoll';
|
||||
import { IDynamicPollStrings } from './IDynamicPollProps';
|
||||
import styles from './DynamicPoll.module.scss';
|
||||
|
||||
export interface IDynamicPollCollectionProps {
|
||||
displayMode: PollDisplayMode;
|
||||
snapshots: IPollSnapshot[];
|
||||
showResultsBeforeVote: boolean;
|
||||
strings: IDynamicPollStrings;
|
||||
onVote: (pollId: number, answer: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface IDynamicPollCollectionState {
|
||||
activeIndex: number;
|
||||
}
|
||||
|
||||
export default class DynamicPollCollection
|
||||
extends React.Component<IDynamicPollCollectionProps, IDynamicPollCollectionState> {
|
||||
|
||||
public constructor(props: IDynamicPollCollectionProps) {
|
||||
super(props);
|
||||
this.state = { activeIndex: 0 };
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: IDynamicPollCollectionProps): void {
|
||||
if (this.state.activeIndex >= nextProps.snapshots.length) { this.setState({ activeIndex: 0 }); }
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return this.props.displayMode === 'accordion' ? this.renderAccordion() : this.renderSlider();
|
||||
}
|
||||
|
||||
private renderAccordion(): JSX.Element {
|
||||
return <div className={styles.collection}>
|
||||
{this.props.snapshots.map((snapshot: IPollSnapshot, index: number): JSX.Element => {
|
||||
const expanded: boolean = index === this.state.activeIndex;
|
||||
const panelId: string = 'poll-panel-' + snapshot.poll.id;
|
||||
return <section className={styles.accordionItem} key={snapshot.poll.id}>
|
||||
<button type='button' className={styles.accordionHeader}
|
||||
aria-expanded={expanded} aria-controls={panelId}
|
||||
onClick={(): void => this.setState({ activeIndex: index })}>
|
||||
<span>{snapshot.poll.title || snapshot.poll.question}</span>
|
||||
<span className={styles.accordionChevron} aria-hidden='true'>{expanded ? '\uE70E' : '\uE70D'}</span>
|
||||
</button>
|
||||
{expanded && <div id={panelId} className={styles.accordionPanel}>
|
||||
{this.renderPoll(snapshot)}
|
||||
</div>}
|
||||
</section>;
|
||||
})}
|
||||
</div>;
|
||||
}
|
||||
|
||||
private renderSlider(): JSX.Element {
|
||||
const count: number = this.props.snapshots.length;
|
||||
const active: number = Math.min(this.state.activeIndex, Math.max(count - 1, 0));
|
||||
const snapshot: IPollSnapshot = this.props.snapshots[active];
|
||||
return <div className={styles.collection}>
|
||||
{snapshot && this.renderPoll(snapshot)}
|
||||
{count > 1 && <div className={styles.sliderNavigation}>
|
||||
<button type='button' className={styles.sliderArrow} aria-label={this.props.strings.previousPollLabel}
|
||||
onClick={(): void => this.move(-1)}>‹</button>
|
||||
<div className={styles.pagination} role='tablist' aria-label={this.props.strings.pollNavigationLabel}>
|
||||
{this.props.snapshots.map((item: IPollSnapshot, index: number): JSX.Element =>
|
||||
<button type='button' role='tab' key={item.poll.id}
|
||||
className={index === active ? styles.paginationDotActive : styles.paginationDot}
|
||||
aria-selected={index === active} aria-label={(index + 1) + ' / ' + count}
|
||||
onClick={(): void => this.setState({ activeIndex: index })} />)}
|
||||
</div>
|
||||
<button type='button' className={styles.sliderArrow} aria-label={this.props.strings.nextPollLabel}
|
||||
onClick={(): void => this.move(1)}>›</button>
|
||||
</div>}
|
||||
</div>;
|
||||
}
|
||||
|
||||
private renderPoll(snapshot: IPollSnapshot): JSX.Element {
|
||||
return <DynamicPoll snapshot={snapshot}
|
||||
showResultsBeforeVote={this.props.showResultsBeforeVote}
|
||||
strings={this.props.strings}
|
||||
onVote={(answer: string): Promise<void> => this.props.onVote(snapshot.poll.id, answer)} />;
|
||||
}
|
||||
|
||||
private move(offset: number): void {
|
||||
const count: number = this.props.snapshots.length;
|
||||
if (count) { this.setState({ activeIndex: (this.state.activeIndex + offset + count) % count }); }
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export interface IDynamicPollStrings {
|
||||
voteButton: string; votingButton: string; thanksMessage: string; resultsHeading: string;
|
||||
votesLabel: string; oneVoteLabel: string; selectAnswerMessage: string;
|
||||
voteErrorMessage: string; alreadyVotedMessage: string;
|
||||
previousPollLabel: string; nextPollLabel: string; pollNavigationLabel: string;
|
||||
}
|
||||
export interface IDynamicPollProps {
|
||||
snapshot: IPollSnapshot;
|
||||
@@ -11,4 +12,3 @@ export interface IDynamicPollProps {
|
||||
strings: IDynamicPollStrings;
|
||||
onVote: (answer: string) => Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ define([], function() { return {
|
||||
"PollListLabel": "Umfrageliste",
|
||||
"AnswerListLabel": "Antwortliste",
|
||||
"ShowResultsBeforeVoteLabel": "Ergebnisse vor der Abstimmung anzeigen",
|
||||
"DisplayModeLabel": "Mehrere aktive Umfragen anzeigen als",
|
||||
"AccordionModeLabel": "Accordion",
|
||||
"SliderModeLabel": "Slider mit Seitennavigation",
|
||||
"RefreshSecondsLabel": "Automatische Aktualisierung (Sekunden)",
|
||||
"SelectOption": "Auswählen...",
|
||||
"ConfigureMessage": "Wählen Sie in den WebPart-Einstellungen die Umfrage- und Antwortliste aus.",
|
||||
@@ -19,6 +22,8 @@ define([], function() { return {
|
||||
"ResultsHeading": "Ergebnis",
|
||||
"VotesLabel": "Stimmen",
|
||||
"OneVoteLabel": "Stimme",
|
||||
"SelectAnswerMessage": "Wählen Sie vor der Abstimmung eine Antwort aus."
|
||||
"SelectAnswerMessage": "Wählen Sie vor der Abstimmung eine Antwort aus.",
|
||||
"PreviousPollLabel": "Vorherige Umfrage",
|
||||
"NextPollLabel": "Nächste Umfrage",
|
||||
"PollNavigationLabel": "Zwischen aktiven Umfragen wechseln"
|
||||
}; });
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ define([], function() { return {
|
||||
"PollListLabel": "Poll list",
|
||||
"AnswerListLabel": "Answer list",
|
||||
"ShowResultsBeforeVoteLabel": "Show results before voting",
|
||||
"DisplayModeLabel": "Show multiple active polls as",
|
||||
"AccordionModeLabel": "Accordion",
|
||||
"SliderModeLabel": "Slider with pagination",
|
||||
"RefreshSecondsLabel": "Automatic refresh (seconds)",
|
||||
"SelectOption": "Select...",
|
||||
"ConfigureMessage": "Select the poll and answer lists in the web part settings.",
|
||||
@@ -19,6 +22,8 @@ define([], function() { return {
|
||||
"ResultsHeading": "Results",
|
||||
"VotesLabel": "votes",
|
||||
"OneVoteLabel": "vote",
|
||||
"SelectAnswerMessage": "Select an answer before voting."
|
||||
"SelectAnswerMessage": "Select an answer before voting.",
|
||||
"PreviousPollLabel": "Previous poll",
|
||||
"NextPollLabel": "Next poll",
|
||||
"PollNavigationLabel": "Switch between active polls"
|
||||
}; });
|
||||
|
||||
|
||||
+2
-1
@@ -5,9 +5,10 @@ declare interface IDynamicPollWebPartStrings {
|
||||
LoadingMessage: string; NoPollMessage: string; LoadErrorMessage: string; VoteErrorMessage: string;
|
||||
AlreadyVotedMessage: string; VoteButton: string; VotingButton: string; ThanksMessage: string;
|
||||
ResultsHeading: string; VotesLabel: string; OneVoteLabel: string; SelectAnswerMessage: string;
|
||||
DisplayModeLabel: string; AccordionModeLabel: string; SliderModeLabel: string;
|
||||
PreviousPollLabel: string; NextPollLabel: string; PollNavigationLabel: string;
|
||||
}
|
||||
declare module 'DynamicPollWebPartStrings' {
|
||||
const strings: IDynamicPollWebPartStrings;
|
||||
export = strings;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user