Add Accordion web part implementation with dynamic SharePoint list integration
- Created Accordion web part with SPFx 1.4.1 framework. - Implemented dynamic loading of SharePoint lists and fields. - Added configuration options for title, content, and category fields. - Included support for multiple open sections and initial state settings. - Developed HTML sanitizer for rich text content. - Implemented accessible accordion UI with category grouping. - Added localization support for English and German languages. - Created necessary configuration files for deployment and asset management. - Added validation script to ensure project compliance with standards.
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
BaseClientSideWebPart,
|
||||
IPropertyPaneConfiguration,
|
||||
IPropertyPaneDropdownOption,
|
||||
PropertyPaneDropdown,
|
||||
PropertyPaneToggle
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
import * as strings from 'AccordionWebPartStrings';
|
||||
import Accordion from './components/Accordion';
|
||||
import { IAccordionProps } from './components/IAccordionProps';
|
||||
import { AccordionDataService } from './AccordionDataService';
|
||||
import {
|
||||
AccordionInitialState,
|
||||
IAccordionFieldOption,
|
||||
IAccordionItem,
|
||||
IAccordionListOption
|
||||
} from './AccordionTypes';
|
||||
import styles from './components/Accordion.module.scss';
|
||||
|
||||
export interface IAccordionWebPartProps {
|
||||
listId: string;
|
||||
titleField: string;
|
||||
contentField: string;
|
||||
categoryField: string;
|
||||
sortField: string;
|
||||
sortAscending: boolean;
|
||||
allowMultiple: boolean;
|
||||
initialState: AccordionInitialState;
|
||||
showCategories: boolean;
|
||||
}
|
||||
|
||||
export default class AccordionWebPart extends BaseClientSideWebPart<IAccordionWebPartProps> {
|
||||
private service: AccordionDataService;
|
||||
private lists: IAccordionListOption[] = [];
|
||||
private fields: IAccordionFieldOption[] = [];
|
||||
private loadingLists: boolean = false;
|
||||
private loadingFields: boolean = false;
|
||||
private requestVersion: number = 0;
|
||||
private disposed: boolean = false;
|
||||
|
||||
protected onInit(): Promise<void> {
|
||||
this.service = new AccordionDataService(this.context);
|
||||
return super.onInit().then((): Promise<void> => this.loadLists())
|
||||
.then((): Promise<void> => this.properties.listId ? this.loadFields(this.properties.listId) : Promise.resolve());
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
const version: number = ++this.requestVersion;
|
||||
if (!this.isConfigured()) {
|
||||
this.renderStatus(strings.ConfigureMessage, false);
|
||||
return;
|
||||
}
|
||||
this.renderStatus(strings.LoadingMessage, false);
|
||||
this.service.getItems(
|
||||
this.properties.listId,
|
||||
this.properties.titleField,
|
||||
this.properties.contentField,
|
||||
this.properties.categoryField || '',
|
||||
this.properties.sortField || this.properties.titleField,
|
||||
this.properties.sortAscending !== false
|
||||
).then((items: IAccordionItem[]): void => {
|
||||
if (this.disposed || version !== this.requestVersion) { return; }
|
||||
if (!items.length) { this.renderStatus(strings.EmptyMessage, false); return; }
|
||||
const element: React.ReactElement<IAccordionProps> = React.createElement(Accordion, {
|
||||
items: items,
|
||||
allowMultiple: this.properties.allowMultiple === true,
|
||||
initialState: this.normalizeInitialState(this.properties.initialState),
|
||||
showCategories: this.properties.showCategories !== false && !!this.properties.categoryField,
|
||||
expandLabel: strings.ExpandLabel,
|
||||
collapseLabel: strings.CollapseLabel,
|
||||
defaultCategory: strings.DefaultCategory
|
||||
});
|
||||
ReactDom.render(element, this.domElement);
|
||||
}).catch((error: Error): void => {
|
||||
if (this.disposed || version !== this.requestVersion) { return; }
|
||||
if (window.console) { console.error('[Accordion] ' + strings.LoadErrorMessage, error); }
|
||||
this.renderStatus(strings.LoadErrorMessage, true);
|
||||
});
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
this.disposed = true;
|
||||
this.requestVersion++;
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version { return Version.parse('1.0'); }
|
||||
|
||||
protected onPropertyPaneConfigurationStart(): void {
|
||||
if (!this.lists.length && !this.loadingLists) { this.loadLists(); }
|
||||
if (this.properties.listId && !this.fields.length && !this.loadingFields) {
|
||||
this.loadFields(this.properties.listId);
|
||||
}
|
||||
}
|
||||
|
||||
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void { // tslint:disable-line:no-any
|
||||
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
|
||||
if (propertyPath === 'listId' && newValue !== oldValue) {
|
||||
this.properties.titleField = '';
|
||||
this.properties.contentField = '';
|
||||
this.properties.categoryField = '';
|
||||
this.properties.sortField = '';
|
||||
this.fields = [];
|
||||
this.context.propertyPane.refresh();
|
||||
this.loadFields(String(newValue || '')).then((): void => this.render());
|
||||
}
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
const fieldOptions: IPropertyPaneDropdownOption[] = this.fields.map((field: IAccordionFieldOption): IPropertyPaneDropdownOption => ({
|
||||
key: field.internalName, text: field.title + ' (' + field.internalName + ')'
|
||||
}));
|
||||
const categoryOptions: IPropertyPaneDropdownOption[] = [{ key: '', text: strings.NoCategoryOption }];
|
||||
const listOptions: IPropertyPaneDropdownOption[] = [{ key: '', text: strings.SelectOption }];
|
||||
for (let i: number = 0; i < fieldOptions.length; i++) { categoryOptions.push(fieldOptions[i]); }
|
||||
for (let i: number = 0; i < this.lists.length; i++) {
|
||||
listOptions.push({ key: this.lists[i].id, text: this.lists[i].title });
|
||||
}
|
||||
return {
|
||||
pages: [{
|
||||
header: { description: strings.PropertyPaneDescription },
|
||||
groups: [{
|
||||
groupName: strings.DataGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneDropdown('listId', {
|
||||
label: strings.ListFieldLabel,
|
||||
options: listOptions,
|
||||
disabled: this.loadingLists
|
||||
}),
|
||||
PropertyPaneDropdown('titleField', {
|
||||
label: strings.TitleFieldLabel, options: fieldOptions,
|
||||
disabled: !this.properties.listId || this.loadingFields
|
||||
}),
|
||||
PropertyPaneDropdown('contentField', {
|
||||
label: strings.ContentFieldLabel, options: fieldOptions,
|
||||
disabled: !this.properties.listId || this.loadingFields
|
||||
}),
|
||||
PropertyPaneDropdown('categoryField', {
|
||||
label: strings.CategoryFieldLabel, options: categoryOptions,
|
||||
disabled: !this.properties.listId || this.loadingFields
|
||||
}),
|
||||
PropertyPaneDropdown('sortField', {
|
||||
label: strings.SortFieldLabel, options: fieldOptions,
|
||||
disabled: !this.properties.listId || this.loadingFields
|
||||
}),
|
||||
PropertyPaneToggle('sortAscending', { label: strings.SortAscendingLabel })
|
||||
]
|
||||
}, {
|
||||
groupName: strings.BehaviorGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneDropdown('initialState', {
|
||||
label: strings.InitialStateFieldLabel,
|
||||
options: [
|
||||
{ key: 'collapsed', text: strings.InitialCollapsed },
|
||||
{ key: 'first', text: strings.InitialFirst },
|
||||
{ key: 'all', text: strings.InitialAll }
|
||||
]
|
||||
}),
|
||||
PropertyPaneToggle('allowMultiple', { label: strings.AllowMultipleLabel }),
|
||||
PropertyPaneToggle('showCategories', {
|
||||
label: strings.ShowCategoriesLabel,
|
||||
disabled: !this.properties.categoryField
|
||||
})
|
||||
]
|
||||
}]
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
private loadLists(): Promise<void> {
|
||||
this.loadingLists = true;
|
||||
this.refreshPropertyPane();
|
||||
return this.service.getLists().then((lists: IAccordionListOption[]): void => {
|
||||
this.lists = lists;
|
||||
}).catch((error: Error): void => {
|
||||
this.lists = [];
|
||||
if (window.console) { console.error('[Accordion] Lists could not be loaded.', error); }
|
||||
}).then((): void => {
|
||||
this.loadingLists = false;
|
||||
this.refreshPropertyPane();
|
||||
});
|
||||
}
|
||||
|
||||
private loadFields(listId: string): Promise<void> {
|
||||
if (!listId) { this.fields = []; return Promise.resolve(); }
|
||||
this.loadingFields = true;
|
||||
this.refreshPropertyPane();
|
||||
return this.service.getFields(listId).then((fields: IAccordionFieldOption[]): void => {
|
||||
this.fields = fields;
|
||||
this.applyFieldDefaults();
|
||||
}).catch((error: Error): void => {
|
||||
this.fields = [];
|
||||
if (window.console) { console.error('[Accordion] Fields could not be loaded.', error); }
|
||||
}).then((): void => {
|
||||
this.loadingFields = false;
|
||||
this.refreshPropertyPane();
|
||||
});
|
||||
}
|
||||
|
||||
private applyFieldDefaults(): void {
|
||||
const names: string[] = this.fields.map((field: IAccordionFieldOption): string => field.internalName);
|
||||
if (names.indexOf(this.properties.titleField) < 0) {
|
||||
this.properties.titleField = names.indexOf('Title') >= 0 ? 'Title' : (names[0] || '');
|
||||
}
|
||||
if (names.indexOf(this.properties.contentField) < 0) {
|
||||
const note: IAccordionFieldOption = this.fields.filter((field: IAccordionFieldOption): boolean =>
|
||||
field.type === 'Note' && field.internalName !== this.properties.titleField)[0];
|
||||
this.properties.contentField = note ? note.internalName : this.firstDifferentField(this.properties.titleField);
|
||||
}
|
||||
if (names.indexOf(this.properties.categoryField) < 0) { this.properties.categoryField = ''; }
|
||||
if (names.indexOf(this.properties.sortField) < 0) { this.properties.sortField = this.properties.titleField; }
|
||||
}
|
||||
|
||||
private firstDifferentField(excluded: string): string {
|
||||
for (let i: number = 0; i < this.fields.length; i++) {
|
||||
if (this.fields[i].internalName !== excluded) { return this.fields[i].internalName; }
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private isConfigured(): boolean {
|
||||
return !!(this.service && this.properties.listId && this.properties.titleField && this.properties.contentField);
|
||||
}
|
||||
private normalizeInitialState(value: string): AccordionInitialState {
|
||||
return value === 'first' || value === 'all' ? value : 'collapsed';
|
||||
}
|
||||
private refreshPropertyPane(): void {
|
||||
if (this.context && this.context.propertyPane) { this.context.propertyPane.refresh(); }
|
||||
}
|
||||
private renderStatus(message: string, error: boolean): void {
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
const status: HTMLDivElement = document.createElement('div');
|
||||
status.className = error ? styles.error : styles.status;
|
||||
status.setAttribute('role', error ? 'alert' : 'status');
|
||||
status.textContent = message;
|
||||
this.domElement.innerHTML = '';
|
||||
this.domElement.appendChild(status);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user