initial release

This commit is contained in:
Arun Kumar Perumal 2022-03-07 07:49:49 +05:30
parent 51eebc82de
commit 246f63aa9c
37 changed files with 25655 additions and 0 deletions

33
samples/react-faqs/.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
release
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,16 @@
!dist
config
gulpfile.js
release
src
temp
tsconfig.json
tslint.json
*.log
.yo-rc.json
.vscode

View File

@ -0,0 +1,13 @@
{
"@microsoft/generator-sharepoint": {
"plusBeta": false,
"isCreatingSolution": true,
"environment": "spo",
"version": "1.13.0",
"libraryName": "react-faqs",
"libraryId": "ad8bfeaa-1ae0-4bf1-b395-b9d863d62d7c",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,94 @@
---
page_type: sample
products:
- office-sp
languages:
- javascript
- typescript
extensions:
contentType: samples
technologies:
- SharePoint Framework
platforms:
- react
createdDate: 03/07/2022 12:00:00 AM
---
# Frequently Asked Questions with Property Colection Data
## Summary
- This Web Part allows users to create Frequently Asked Questions using Property Field Collection Data for SharePoint Online.
- This web part allows to search within questions and answers which are stored in a Property Field Collection Data and can be easily edited inline within the webpart using Property Pane. Search text is highligted in the search results.
- Provides options to view as an Accordion or Tab.
- In mobile browser defaults to Accordion view.
![Web part preview](assets/FAQWebpart.png)
## Used SharePoint Framework Version
![version](https://img.shields.io/badge/version-1.13-green.svg)
## Applies to
- [SharePoint Framework](https://aka.ms/spfx)
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
## Prerequisites
There are no pre-requisites to use these samples.
## Solution
Solution|Author(s)
--------|---------
react-Faqs | Arun Kumar Perumal - LinkedIn: https://www.linkedin.com/in/arunkumarperumal/
## Version history
Version|Date|Comments
-------|----|--------
1.0|March 07, 2022|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- Ensure that you are at the solution folder
- in the command-line run:
- **npm install**
- **gulp serve**
## Features
This Web Part allows users to create Frequently Asked Questions using Property Field Collection Data for SharePoint Online.
Has the following features:
- Ability to create FAQ Categories
- Ability to create FAQs with Rich Text Editor for Answer using PnP Rich Text Control
- Ability to sort FAQs with capability from PnP Property Pane PropertyFieldCollectionData
- Ability to view the FAQs as an Accordion or Tab
- Ability to search based on FAQ Question and Answer and highlights the search term in the results
- Defaults to Accordion in Mobile displays
- Uses Custom Accordion components included in the code.
- Use the site Primary colors and themes for display-
- Uses Office UI Fabric Search Box for the search functionality
> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp.
## References
- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,49 @@
[
{
"name": "pnp-sp-dev-spfx-web-parts-react-faqs",
"source": "pnp",
"title": "Frequently Asked Questions",
"shortDescription": "Allows users to create Frequently Asked Questions using Property Field Collection Data",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-faqs",
"longDescription": [
"Allows users to create Frequently Asked Questions using Property Field Collection Data, with options to view as an Accordion or Tab and also ability to search within the FAQs"
],
"creationDateTime": "2022-03-07",
"updateDateTime": "2022-03-07",
"products": [
"SharePoint"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.13"
}
],
"thumbnails": [
{
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-faqs/assets/FAQWebpart.png",
"alt": "Web Part Preview"
}
],
"authors": [
{
"gitHubAccount": "arunkumarperumal",
"pictureUrl": "https://avatars.githubusercontent.com/u/39132298?v=4",
"name": "Arun Kumar Perumal"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
}
]

View File

@ -0,0 +1,20 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"fa-qs-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/faQs/FaQsWebPart.js",
"manifest": "./src/webparts/faQs/FaQsWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"FaqsWebPartStrings": "lib/webparts/faQs/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js",
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js"
}
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./release/assets/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-faqs",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,21 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-faqs-client-side-solution",
"id": "ad8bfeaa-1ae0-4bf1-b395-b9d863d62d7c",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": "Undefined-1.13.0"
}
},
"paths": {
"zippedPackage": "solution/react-faqs.sppkg"
}
}

View File

@ -0,0 +1,6 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://enter-your-SharePoint-site/_layouts/workbench.aspx"
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

16
samples/react-faqs/gulpfile.js vendored Normal file
View File

@ -0,0 +1,16 @@
'use strict';
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
var getTasks = build.rig.getTasks;
build.rig.getTasks = function () {
var result = getTasks.call(build.rig);
result.set('serve', result.get('serve-deprecated'));
return result;
};
build.initialize(require('gulp'));

23492
samples/react-faqs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
{
"name": "react-faqs",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.13.0",
"@microsoft/sp-lodash-subset": "1.13.0",
"@microsoft/sp-office-ui-fabric-core": "1.13.0",
"@microsoft/sp-property-pane": "1.13.0",
"@microsoft/sp-webpart-base": "1.13.0",
"@pnp/spfx-controls-react": "^3.5.0",
"@pnp/spfx-property-controls": "^3.3.0",
"office-ui-fabric-react": "7.174.1",
"react": "16.13.1",
"react-dom": "16.13.1"
},
"devDependencies": {
"@types/react": "16.9.51",
"@types/react-dom": "16.9.8",
"@microsoft/sp-build-web": "1.13.0",
"@microsoft/sp-tslint-rules": "1.13.0",
"@microsoft/sp-module-interfaces": "1.13.0",
"@microsoft/rush-stack-compiler-3.9": "0.4.47",
"gulp": "~4.0.2",
"ajv": "~5.2.2",
"@types/webpack-env": "1.13.1"
}
}

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,256 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
WebPartContext
} from '@microsoft/sp-webpart-base';
import {
IPropertyPaneConfiguration,
PropertyPaneDropdown,
} from '@microsoft/sp-property-pane';
import { RichText } from '@pnp/spfx-controls-react/lib/RichText';
import * as strings from 'FaqsWebPartStrings';
import Faqs from './components/Faqs';
import { IFaqsProps } from './components/IFaqsProps';
import Accordions from './components/Accordions';
import { IAccordionsProps } from './components/IAccordionsProps';
import { IFaq, FaqTarget } from './components/IFaq';
import styles from './components/Faqs.module.scss';
export interface IFaqsWebPartProps {
collectionData: IFaq[];
categoryData: any[];
title: string;
type: string;
}
export default class FaqsWebPart extends BaseClientSideWebPart<IFaqsWebPartProps> {
private propertyFieldCollectionData;
private customCollectionFieldType;
private guid: string;
private isMobile: boolean;
/**
* @function
* Web part contructor.
*/
public constructor(context?: WebPartContext) {
super();
//Initialize unique GUID
this.guid = this.getGuid();
this.isMobile = this.detectmob();
//Hack: to invoke correctly the onPropertyChange function outside this class
//we need to bind this object on it first
this.onPropertyPaneFieldChanged = this.onPropertyPaneFieldChanged.bind(this);
}
public render(): void {
const element: React.ReactElement<IFaqsProps > = React.createElement(
Faqs,
{
collectionData: this.properties.collectionData,
title: this.properties.title,
categoryData: this.properties.categoryData,
displayMode: this.displayMode,
fUpdateProperty: (value: string) => {
this.properties.title = value;
},
fPropertyPaneOpen: this.context.propertyPane.open
}
);
const elementAccordion: React.ReactElement<IAccordionsProps > = React.createElement(
Accordions,
{
collectionData: this.properties.collectionData,
displayMode: this.displayMode,
guid: this.guid,
title: this.properties.title,
accordion:true,
fUpdateProperty: (value: string) => {
this.properties.title = value;
},
fPropertyPaneOpen: this.context.propertyPane.open
}
);
if(this.isMobile)
{
ReactDom.render(elementAccordion, this.domElement);
}
else
{
if(this.properties.type == "Accordion")
{
ReactDom.render(elementAccordion, this.domElement);
}
else
{
ReactDom.render(element, this.domElement);
}
}
}
private detectmob(): boolean {
if(window.innerWidth <= 480) {
return true;
} else {
return false;
}
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
/**
* @function
* Generates a GUID
*/
private getGuid(): string {
return this.s4() + this.s4() + '-' + this.s4() + '-' + this.s4() + '-' +
this.s4() + '-' + this.s4() + this.s4() + this.s4();
}
/**
* @function
* Generates a GUID part
*/
private s4(): string {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
//executes only before property pane is loaded.
protected async loadPropertyPaneResources(): Promise<void> {
// import additional controls/components
const { PropertyFieldCollectionData, CustomCollectionFieldType } = await import (
/* webpackChunkName: 'pnp-propcontrols-colldata' */
'@pnp/spfx-property-controls/lib/PropertyFieldCollectionData'
);
this.propertyFieldCollectionData = PropertyFieldCollectionData;
this.customCollectionFieldType = CustomCollectionFieldType;
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
let groups = [];
if (this.properties.categoryData && this.properties.categoryData.length > 0) {
groups = this.properties.categoryData.map((category: any) => ({ key: category.title, text: category.title }));
}
return {
pages: [
{
groups: [
{
groupFields: [
this.propertyFieldCollectionData("categoryData", {
key: "categoryData",
label: strings.categoryDataLabel,
panelHeader: strings.categoryPanelHeader,
manageBtnLabel: strings.manageCategoryBtn,
value: this.properties.categoryData,
enableSorting: true,
fields: [
{
id: "title",
title: strings.questionTitleField,
type: this.customCollectionFieldType.string,
required: true
}
]
}),
this.propertyFieldCollectionData("collectionData", {
key: "collectionData",
label: strings.FaqDataLabel,
panelHeader: strings.FaqPanelHeader,
manageBtnLabel: strings.manageFaqsBtn,
value: this.properties.collectionData,
tableClassName: 'tableSpan',
panelClassName: 'propertyPanel',
enableSorting: true,
fields: [
{
id: "questionTitle",
title: strings.questionTitleField,
type: this.customCollectionFieldType.string,
required: true,
placeholder: 'Question Title'
},
{
id: "answerText",
title: strings.answerTextField,
type: this.customCollectionFieldType.custom,
required: true,
defaultValue: '',
onCustomRender: (field, value, onUpdate, item, itemId) => {
return (
React.createElement("div", {style: {width: "250px"}},
React.createElement(RichText, {
key: itemId,
value: value,
onChange : (newText: string) => {
onUpdate(field.id, newText);
return newText;
}
}),
React.createElement("span", {style:{color:'#a80000', top:'-5px', position: 'relative', float: 'right', left: '-5px'}, value: ' *'},'*'),
)
);
}
},
{
id: "answerLinkTitle",
title: strings.answerLinkTitleField,
type: this.customCollectionFieldType.string,
placeholder: 'Question Link Url Title'
},
{
id: "answerLink",
title: strings.answerLinkField,
type: this.customCollectionFieldType.url
},
{
id: "category",
title: strings.categoryField,
type: this.customCollectionFieldType.dropdown,
options: [
{
key: null,
text: ""
},
...groups
]
}
]
}),
PropertyPaneDropdown('type', {
label: strings.Type,
disabled: false,
options: [
{key: 'Accordion', text: 'Accordion'},
{key: 'Tab', text: 'Tab'}
]
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,366 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
/*
* ----------------------------------------------
* Demo styles
* ----------------------------------------------
**/
:global {
.ql-toolbar {
top: 28px !important;
}
}
.webparttitle {
font-size: 24px;
font-weight: 100;
display: inline-block;
padding-bottom: 20px;
}
.faqQuestionBlock {
padding-left:10px;
margin-bottom: 30px;
border-bottom: 1px solid ;
border-color: $ms-color-themePrimary;
}
.faqAnswerLink {
//padding-left:25px;
position: relative;
max-width: 650px;
font-size: 15px;
line-height: 19px;
}
.faqAnswerSvgLink {
fill: $ms-color-themePrimary;
position: relative;
top: 1px;
left: 0;
height: 21px;
float: left;
}
.faqAnswerSvgLink svg {
width: 17px;
}
.faqAnswerLink a {
margin-left:10px;
text-decoration: none;
color: $ms-color-themePrimary;
}
.faqHeader {
float:left;
width: 100%;
}
.faqContent {
float:left;
width: 100%;
}
.faqSearchBox {
//margin-top: -40px;
float: right;
margin-bottom: 10px;
@media (max-width: 480px) {
margin-top: 10px !important;
float: left;
width:80%;
}
@media (max-width: 320px) {
margin-top: 10px !important;
float: left;
width: 80%;
}
}
.faqWebPartTitle {
width:80%;
float: left;
@media (max-width: 480px) {
width: 100%;
}
@media (max-width: 320px) {
width: 100%;
}
}
.noResultsWrapper {
display: block;
margin-left: 35px;
float: left;
width: 100%;
}
.backToCategories {
color: $ms-color-white;
background-color: $ms-color-themePrimary;
border-bottom: 2px solid $ms-color-themeDarker;
position: relative;
display: inline-block;
height: 40px;
margin: 15px 0;
padding-left: 24px;
padding-right: 10px;
font-weight: 300;
font-size: 16px;
line-height: 32px;
text-transform: uppercase;
}
.noSearchResults {
color: $ms-color-black;
font-size: 18px;
font-weight: 600;
line-height: 20px;
text-transform: uppercase;
margin-left: 10px;
}
.iconNavigateBack {
margin-right: 10px;
vertical-align: middle;
}
.searchResultsCategoryName {
color: $ms-color-themePrimary;
font-size: 18px;
font-weight: 600;
line-height: 20px;
text-transform: uppercase;
margin-left: 10px;
}
input[class^="react-search-field-input"] {
padding-left: 10px;
}
.webpartheader>span {
float: right;
position:relative;
display:inline-block;
}
.positionAbsolute {
position: absolute;
}
.positionRelative {
position: relative;
display: inline;
padding-left: 20px;
font-family: "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
font-size: 21px;
top: 15%;
font-weight: 600;
}
.accordion {
// border-bottom: 1px solid;
//border-radius: 2px;
// border-bottom-color: $ms-color-themePrimary;
float: left;
width: 100%;
@media (max-width: 480px) {
margin-top: 10px !important;
}
@media (max-width: 320px) {
margin-top: 10px !important;
}
}
.accordion__item{
margin-bottom: 15px;
}
.accordion__item:focus{
outline:none;
}
.accordionItemHasIcon {
position: relative;
}
.accordion__title {
background-color: $ms-color-neutralLighter;
color: $ms-color-themePrimary;
cursor: pointer;
padding: 8px 0px 10px 0px;
text-align: left;
border: none;
vertical-align: top;
&:hover {
background-color: $ms-color-neutralLight;
}
}
.accordion__item [aria-expanded='true'], .accordion__item [aria-selected='true'] {
background-color: $ms-color-themePrimary;
color:$ms-color-white;
&:hover {
background-color: $ms-color-themeDarker;
}
}
.accordion__title:focus {
outline:none;
border:none;
}
.accordion__body {
padding: 20px;
display: block;
animation: fadein 0.35s ease-in;
}
.accordionBodyHidden {
display: none;
opacity: 0;
animation: fadein 0.35s ease-in;
}
.accordion__title > *:last-child, .accordion__body > *:last-child {
margin-bottom: 0;
}
.accordion__arrow {
display: inline-block;
position: relative;
width: 20px;
height: 20px;
top: 20%;
border-radius: 20px;
background-color: $ms-color-themePrimary;
color:$ms-color-white;
margin-left: 20px;
&::after {
display: block;
position: absolute;
top: 50%;
width: 10px;
height: 2px;
content: '';
background-color: $ms-color-white;
}
&::before {
display: block;
position: absolute;
top: 50%;
width: 10px;
height: 2px;
content: '';
transform: rotate(45deg);
background-color: $ms-color-white;
}
}
[aria-expanded='true'] .accordion__arrow, [aria-selected='true'] .accordion__arrow{
background-color: $ms-color-white;
}
[aria-expanded='true'] .accordion__arrow::before, [aria-selected='true'] .accordion__arrow::before {
transform: rotate(-45deg);
background-color: $ms-color-themePrimary;
color:$ms-color-themePrimary;
}
.accordion__arrow::before {
left: 2px;
}
.accordion__arrow::after {
right: 2px;
transform: rotate(-45deg);
}
[aria-expanded='true'] .accordion__arrow::after, [aria-selected='true'] .accordion__arrow::after {
transform: rotate(45deg);
background-color: $ms-color-themePrimary;
}
.accordion__arrow {
&::before, &::after {
transition: transform 0.25s ease, -webkit-transform 0.25s ease;
}
}
/* -------------------------------------------------- */
/* ---------------- Animation part ------------------ */
/* -------------------------------------------------- */
@keyframes fadein {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes moveDown {
0% {
transform: translateY(0);
}
10% {
transform: translateY(0);
}
20% {
transform: translateY(5px);
}
30% {
transform: translateY(0);
}
100% {
transform: translateY(0);
}
}
@keyframes moveUp {
0% {
transform: translateY(0);
}
10% {
transform: translateY(0);
}
20% {
transform: translateY(-5px);
}
30% {
transform: translateY(0);
}
100% {
transform: translateY(0);
}
}
.accordionTitleAnimated {
&:hover .accordion__arrow {
animation-name: moveDown;
animation-duration: 1.5s;
}
&[aria-expanded='true']:hover .accordion__arrow {
animation-name: moveUp;
animation-duration: 1.5s;
}
}

View File

@ -0,0 +1,231 @@
import * as React from 'react';
import styles from './Accordions.module.scss';
import * as strings from 'FaqsWebPartStrings';
import { IAccordionsProps } from './IAccordionsProps';
import { SPComponentLoader } from '@microsoft/sp-loader';
import { escape, groupBy, toPairs, sortBy, fromPairs } from '@microsoft/sp-lodash-subset';
import { DisplayMode, Version } from '@microsoft/sp-core-library';
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
import { SearchBox } from 'office-ui-fabric-react/lib/SearchBox';
import { Link } from 'office-ui-fabric-react/lib/components/Link';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import {
Accordion,
AccordionItem,
AccordionItemTitle,
AccordionItemBody,
} from './utilities/Accordion/index';
import { IAccordionsState } from './IAccordionsState';
//import 'react-accessible-accordion/dist/main.css';
const NO_CATEGORY_NAME = "..NOCATEGORYNAME..";
export default class Accordions extends React.Component<IAccordionsProps, IAccordionsState> {
constructor(props: IAccordionsProps) {
super(props);
this.state = {
categories: null,
searchCategories: null,
searchValue: '',
};
}
/**
* Process all links from the collection data
*/
private _processFaqs(): void {
const {collectionData} = this.props;
if (collectionData && collectionData.length > 0) {
// Group by the group name
let categories = groupBy(collectionData, Faq => Faq.category ? Faq.category : NO_CATEGORY_NAME);
// Sort the group by the property name
categories = fromPairs(sortBy(toPairs(categories), 0));
this.setState({
categories
});
} else {
this.setState({
categories: null
});
}
}
/**
* componentWillMount lifecycle hook
*/
public componentWillMount(): void {
this._processFaqs();
}
/**
* componentDidUpdate lifecycle hook
* @param prevProps
* @param prevState
*/
public componentDidUpdate(prevProps: IAccordionsProps, prevState: IAccordionsState): void {
if (prevProps.collectionData !== this.props.collectionData) {
this._processFaqs();
}
}
private onChange = (event: any, newValue: string): void => {
const {collectionData } = this.props;
if (collectionData && collectionData.length > 0) {
let filteredCategories;
filteredCategories = collectionData.filter(item => item.answerText.toLowerCase().indexOf(newValue.toLowerCase()) != -1 || item.questionTitle.toLowerCase().indexOf(newValue.toLowerCase()) != -1);
let categories = groupBy(filteredCategories, Faq => Faq.category ? Faq.category : NO_CATEGORY_NAME);
// Sort the group by the property name
categories = fromPairs(sortBy(toPairs(categories), 0));
this.setState({
searchCategories: categories,
searchValue : newValue
});
} else {
this.setState({
searchCategories: null,
searchValue : newValue
});
}
}
private onClear() {
this.setState({
searchCategories: null,
searchValue : ''
});
}
private backToCategories() {
this.setState({
searchCategories: null,
searchValue : ''
});
}
public render(): React.ReactElement<IAccordionsProps> {
const categoryNames = this.state.categories ? Object.keys(this.state.categories) : null;
const searchCategoryNames = this.state.searchCategories ? Object.keys(this.state.searchCategories) : null;
const searchValue = this.state.searchValue;
let categories;
var regEx = new RegExp(searchValue, "ig");
if(searchCategoryNames && searchCategoryNames.length > 0 && searchValue != ''){
categories = <div className={styles.noResultsWrapper}><Link className={styles.backToCategories} onClick={this.backToCategories.bind(this)}>
<Icon iconName="NavigateBack" className={styles.iconNavigateBack} />
Back to Categories</Link><div>
{
searchCategoryNames.map(categoryName => (
<div key={categoryName}>
<h2 className={styles.searchResultsCategoryName}>{categoryName}</h2>
{
// Loop over all links per group
this.state.searchCategories[categoryName].map(faq => (
<div className={styles.faqQuestionBlock}>
<h2 dangerouslySetInnerHTML={{__html : faq.questionTitle.replace(regEx, str => '<mark>' + str + '</mark>')}}></h2>
<p dangerouslySetInnerHTML={{__html : faq.answerText.replace(regEx, str => '<mark>' + str + '</mark>')}}></p>
{ faq.answerLink && <p className={styles.faqAnswerLink}>
<div className={styles.faqAnswerSvgLink}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 34">
<path d="M12.88,21.52h0a1.16,1.16,0,0,1-.85.36,1.2,1.2,0,0,1-1.2-1.2,1.21,1.21,0,0,1,.38-.88l7.94-8a1.19,1.19,0,0,1,.88-.39,1.21,1.21,0,0,1,.86,2.05ZM26.62,12.7l-3.34,3.36a4.72,4.72,0,0,1-4.59,1.22l2.79-2.8a.58.58,0,0,0,.12-.1L25,11A2.37,2.37,0,0,0,21.6,7.67L18.26,11l-2.89,2.9a4.74,4.74,0,0,1,1.22-4.57L19.93,6a4.71,4.71,0,0,1,6.69,0A4.77,4.77,0,0,1,26.62,12.7ZM17,0A17,17,0,1,0,34,17,17,17,0,0,0,17,0ZM15.41,23.93l-3.34,3.36a4.7,4.7,0,0,1-6.68,0,4.75,4.75,0,0,1,0-6.71l3.34-3.36A4.7,4.7,0,0,1,13.3,16l-2.88,2.88s0,0,0,0L7.06,22.26a2.38,2.38,0,0,0,0,3.35,2.35,2.35,0,0,0,3.34,0l3.34-3.35.06-.08,2.83-2.83A4.74,4.74,0,0,1,15.41,23.93Z"></path></svg>
</div>
<a href={faq.answerLink}>{faq.answerLinkTitle}</a>
</p> }
</div>
))
}
</div>
))
}
</div></div>;
}
else if(searchCategoryNames && searchCategoryNames.length == 0 && searchValue != '' )
{
categories = <div className={styles.noResultsWrapper}><Link className={styles.backToCategories} onClick={this.backToCategories.bind(this)}>
<Icon iconName="NavigateBack" className={styles.iconNavigateBack} />
Back to Categories</Link><div><h2 className={styles.noSearchResults}>0 result found</h2></div></div>;
}
else if(categoryNames && categoryNames.length >0 && searchValue == '')
{
categories = <Accordion className={styles.accordion} aria-live="polite" accordion={this.props.accordion}>
{
categoryNames.map((categoryName,index: number) => (
<AccordionItem key={"tab" + index} className={styles.accordion__item} aria-controls={this.props.guid + '-title-' + index} id={"tab" + index} >
<AccordionItemTitle className={styles.accordion__title} id={this.props.guid + '-title-' + index}>
<div className={styles.accordion__arrow} role="presentation" />
<div className={styles["positionRelative"]} >
{categoryName}
</div>
</AccordionItemTitle>
<AccordionItemBody className={styles.accordion__body} hideBodyClassName={styles["accordionBodyHidden"]}>
{
// Loop over all links per group
this.state.categories[categoryName].map(faq => (
<div className={styles.faqQuestionBlock}>
<h2>{faq.questionTitle}</h2>
<p dangerouslySetInnerHTML={{__html: faq.answerText}}></p>
{ faq.answerLink && <p className={styles.faqAnswerLink}>
<div className={styles.faqAnswerSvgLink}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 34">
<path d="M12.88,21.52h0a1.16,1.16,0,0,1-.85.36,1.2,1.2,0,0,1-1.2-1.2,1.21,1.21,0,0,1,.38-.88l7.94-8a1.19,1.19,0,0,1,.88-.39,1.21,1.21,0,0,1,.86,2.05ZM26.62,12.7l-3.34,3.36a4.72,4.72,0,0,1-4.59,1.22l2.79-2.8a.58.58,0,0,0,.12-.1L25,11A2.37,2.37,0,0,0,21.6,7.67L18.26,11l-2.89,2.9a4.74,4.74,0,0,1,1.22-4.57L19.93,6a4.71,4.71,0,0,1,6.69,0A4.77,4.77,0,0,1,26.62,12.7ZM17,0A17,17,0,1,0,34,17,17,17,0,0,0,17,0ZM15.41,23.93l-3.34,3.36a4.7,4.7,0,0,1-6.68,0,4.75,4.75,0,0,1,0-6.71l3.34-3.36A4.7,4.7,0,0,1,13.3,16l-2.88,2.88s0,0,0,0L7.06,22.26a2.38,2.38,0,0,0,0,3.35,2.35,2.35,0,0,0,3.34,0l3.34-3.35.06-.08,2.83-2.83A4.74,4.74,0,0,1,15.41,23.93Z"></path></svg>
</div>
<a href={faq.answerLink} >{faq.answerLinkTitle}</a>
</p> }
</div>
))
}
</AccordionItemBody>
</AccordionItem>
))
// }
}
</Accordion>;
}
else if(categoryNames && categoryNames.length == 0 && searchValue == '') {
categories = <Placeholder
iconName='Edit'
iconText={strings.noFaqsIconText}
description={strings.noFaqsConfigured}
buttonLabel={strings.noFaqsBtn}
onConfigure={this.props.fPropertyPaneOpen} />;
}
return (
<div>
<WebPartTitle displayMode={this.props.displayMode}
title={this.props.title}
updateProperty={this.props.fUpdateProperty}
className={styles.faqWebPartTitle}/>
<SearchBox
styles={{ root: { width: 200 } }}
className={styles.faqSearchBox}
placeholder='Search'
onChange={this.onChange.bind(this)}
value={this.state.searchValue}
/>
{categories}
</div>
);
}
}

View File

@ -0,0 +1,242 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
:global {
.ql-toolbar {
top: 28px !important;
}
.propertyPanel {
left: -350px;
}
.tableSpan{
vertical-align: top;
}
}
.faq {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.faqHeader {
float:left;
width: 100%;
}
.faqContent {
float:left;
width: 100%;
}
.faqSearchBox {
//margin-top: -40px;
float: right;
margin-top: 40px;
@media (max-width: 480px) {
margin-top: 10px !important;
float: left;
}
@media (max-width: 320px) {
margin-top: 10px !important;
float: left;
}
}
input[class^="react-search-field-input"] {
padding-left: 10px;
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
div[class^="ms-Pivot"] {
background-color: $ms-color-neutralLighter;
height: 58px;
margin-top: 40px;
}
button[class*="ms-Pivot-link"] {
height: 58px;
padding-top: 12px;
padding-bottom: 10px;
margin-left: 40px;
color: $ms-color-neutralTertiary;
}
button[class*="linkIsSelected"] {
color: $ms-color-themePrimary;
margin-left: 40px;
//border-bottom: 6px solid;
//border-color: $ms-color-themeDark;
}
span[class^="ms-Pivot-text"] {
font-size: 21px;
}
.faqQuestionBlock {
padding-left:10px;
margin-bottom: 30px;
border-bottom: 1px solid ;
border-color: $ms-color-themePrimary;
margin-left: 40px;
width: 90%;
}
.faqAnswerLink {
//padding-left:25px;
position: relative;
max-width: 650px;
font-size: 15px;
line-height: 19px;
}
.faqAnswerSvgLink {
fill: $ms-color-themePrimary;
position: relative;
top: 1px;
left: 0;
height: 21px;
float: left;
}
.faqAnswerSvgLink svg {
width: 17px;
}
.faqAnswerLink a {
margin-left:10px;
text-decoration: none;
color: $ms-color-themePrimary;
}
.faqWebPartTitle {
width:80%;
float: left;
@media (max-width: 480px) {
width: 100%;
}
@media (max-width: 320px) {
width: 100%;
}
}
.faqPlaceholder {
width: 100%;
float: left;
}
.noResultsWrapper {
display: block;
margin-left: 35px;
}
.backToCategories {
color: $ms-color-white;
background-color: $ms-color-themePrimary;
border-bottom: 2px solid $ms-color-themeDarker;
position: relative;
display: inline-block;
height: 40px;
margin: 15px 0;
padding-left: 24px;
padding-right: 10px;
font-weight: 300;
font-size: 16px;
line-height: 32px;
text-transform: uppercase;
}
.noSearchResults {
color: $ms-color-black;
font-size: 18px;
font-weight: 600;
line-height: 20px;
text-transform: uppercase;
margin-left: 10px;
}
.iconNavigateBack {
margin-right: 10px;
vertical-align: middle;
}
.searchResultsCategoryName {
color: $ms-color-themePrimary;
font-size: 18px;
font-weight: 600;
line-height: 20px;
text-transform: uppercase;
margin-left: 10px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,220 @@
import * as React from 'react';
import styles from './Faqs.module.scss';
import * as strings from 'FaqsWebPartStrings';
import { IFaqsProps } from './IFaqsProps';
import { IFaqsState } from './IFaqsState';
import { escape, groupBy, toPairs, sortBy, fromPairs } from '@microsoft/sp-lodash-subset';
import { Link } from 'office-ui-fabric-react/lib/components/Link';
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { IFaq } from './IFaq';
import { Pivot, PivotItem, PivotLinkFormat, PivotLinkSize } from 'office-ui-fabric-react/lib/Pivot';
import { Label } from 'office-ui-fabric-react/lib/Label';
import { SearchBox } from 'office-ui-fabric-react/lib/SearchBox';
const NO_CATEGORY_NAME = "..NOCATEGORYNAME..";
export default class Faqs extends React.Component<IFaqsProps, IFaqsState> {
constructor(props: IFaqsProps) {
super(props);
this.state = {
categories: null,
searchCategories: null,
searchValue: '',
};
}
/**
* Process all links from the collection data
*/
private _processFaqs(): void {
const {collectionData} = this.props;
if (collectionData && collectionData.length > 0) {
// Group by the group name
let categories = groupBy(collectionData, Faq => Faq.category ? Faq.category : NO_CATEGORY_NAME);
// Sort the group by the property name
categories = fromPairs(sortBy(toPairs(categories), 0));
this.setState({
categories
});
} else {
this.setState({
categories: null
});
}
}
/**
* componentWillMount lifecycle hook
*/
public componentWillMount(): void {
this._processFaqs();
}
/**
* componentDidUpdate lifecycle hook
* @param prevProps
* @param prevState
*/
public componentDidUpdate(prevProps: IFaqsProps, prevState: IFaqsState): void {
if (prevProps.collectionData !== this.props.collectionData) {
this._processFaqs();
}
}
private onChange = (event : any, newValue: string): void => {
const {collectionData } = this.props;
if (collectionData && collectionData.length > 0) {
let filteredCategories;
filteredCategories = collectionData.filter(item => item.answerText.toLowerCase().indexOf(newValue.toLowerCase()) != -1 || item.questionTitle.toLowerCase().indexOf(newValue.toLowerCase()) != -1);
let categories = groupBy(filteredCategories, Faq => Faq.category ? Faq.category : NO_CATEGORY_NAME);
// Sort the group by the property name
categories = fromPairs(sortBy(toPairs(categories), 0));
this.setState({
searchCategories : categories,
searchValue : newValue
});
} else {
this.setState({
searchCategories: null,
searchValue : newValue
});
}
}
private onClear() {
this.setState({
searchCategories: null,
searchValue : ''
});
}
private backToCategories() {
this.setState({
searchCategories: null,
searchValue : ''
});
}
public render(): React.ReactElement<IFaqsProps> {
// Get all group names
const categoryNames = this.state.categories ? Object.keys(this.state.categories) : null;
const searchCategoryNames = this.state.searchCategories ? Object.keys(this.state.searchCategories) : null;
const searchValue = this.state.searchValue;
let replaceSearchValue = "<mark>" + searchValue + "</mark>";
var regEx = new RegExp(searchValue, "ig");
//let searchValueString = '/' + searchValue + '/ig';
let categories;
if(searchCategoryNames && searchCategoryNames.length > 0 && searchValue != ''){
categories = <div className={styles.noResultsWrapper}><Link className={styles.backToCategories} onClick={this.backToCategories.bind(this)}>
<Icon iconName="NavigateBack" className={styles.iconNavigateBack} />
Back to Categories</Link><div>
{
searchCategoryNames.map(categoryName => (
<div key={categoryName}>
<h2 className={styles.searchResultsCategoryName}>{categoryName}</h2>
{
// Loop over all links per group
this.state.searchCategories[categoryName].map(faq => (
<div className={styles.faqQuestionBlock}>
<h2 dangerouslySetInnerHTML={{__html : faq.questionTitle.replace(regEx, str => '<mark>' + str + '</mark>')}}></h2>
<p dangerouslySetInnerHTML={{__html : faq.answerText.replace(regEx, str => '<mark>' + str + '</mark>')}}></p>
{faq.answerLink && <p className={styles.faqAnswerLink}>
<div className={styles.faqAnswerSvgLink}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 34">
<path d="M12.88,21.52h0a1.16,1.16,0,0,1-.85.36,1.2,1.2,0,0,1-1.2-1.2,1.21,1.21,0,0,1,.38-.88l7.94-8a1.19,1.19,0,0,1,.88-.39,1.21,1.21,0,0,1,.86,2.05ZM26.62,12.7l-3.34,3.36a4.72,4.72,0,0,1-4.59,1.22l2.79-2.8a.58.58,0,0,0,.12-.1L25,11A2.37,2.37,0,0,0,21.6,7.67L18.26,11l-2.89,2.9a4.74,4.74,0,0,1,1.22-4.57L19.93,6a4.71,4.71,0,0,1,6.69,0A4.77,4.77,0,0,1,26.62,12.7ZM17,0A17,17,0,1,0,34,17,17,17,0,0,0,17,0ZM15.41,23.93l-3.34,3.36a4.7,4.7,0,0,1-6.68,0,4.75,4.75,0,0,1,0-6.71l3.34-3.36A4.7,4.7,0,0,1,13.3,16l-2.88,2.88s0,0,0,0L7.06,22.26a2.38,2.38,0,0,0,0,3.35,2.35,2.35,0,0,0,3.34,0l3.34-3.35.06-.08,2.83-2.83A4.74,4.74,0,0,1,15.41,23.93Z"></path></svg>
</div>
<a href={faq.answerLink}>{faq.answerLinkTitle}</a>
</p> }
</div>
))
}
</div>
))
}
</div></div>;
}
else if(searchCategoryNames && searchCategoryNames.length == 0 && searchValue != '' )
{
categories = <div className={styles.noResultsWrapper}><Link className={styles.backToCategories} onClick={this.backToCategories.bind(this)}>
<Icon iconName="NavigateBack" className={styles.iconNavigateBack} />
Back to Categories</Link><div><h2 className={styles.noSearchResults}>0 result found</h2></div></div>;
}
else if(categoryNames && categoryNames.length >0 && searchValue == '')
{
categories = <Pivot linkSize={PivotLinkSize.large} className={styles.faqContent}>
{
categoryNames.map(categoryName => (
<PivotItem headerText={categoryName} key={categoryName}>
{
// Loop over all links per group
this.state.categories[categoryName].map(faq => (
<div className={styles.faqQuestionBlock}>
<h2>{faq.questionTitle}</h2>
<p dangerouslySetInnerHTML={{__html: faq.answerText}}></p>
{faq.answerLink &&<p className={styles.faqAnswerLink}>
<div className={styles.faqAnswerSvgLink}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 34">
<path d="M12.88,21.52h0a1.16,1.16,0,0,1-.85.36,1.2,1.2,0,0,1-1.2-1.2,1.21,1.21,0,0,1,.38-.88l7.94-8a1.19,1.19,0,0,1,.88-.39,1.21,1.21,0,0,1,.86,2.05ZM26.62,12.7l-3.34,3.36a4.72,4.72,0,0,1-4.59,1.22l2.79-2.8a.58.58,0,0,0,.12-.1L25,11A2.37,2.37,0,0,0,21.6,7.67L18.26,11l-2.89,2.9a4.74,4.74,0,0,1,1.22-4.57L19.93,6a4.71,4.71,0,0,1,6.69,0A4.77,4.77,0,0,1,26.62,12.7ZM17,0A17,17,0,1,0,34,17,17,17,0,0,0,17,0ZM15.41,23.93l-3.34,3.36a4.7,4.7,0,0,1-6.68,0,4.75,4.75,0,0,1,0-6.71l3.34-3.36A4.7,4.7,0,0,1,13.3,16l-2.88,2.88s0,0,0,0L7.06,22.26a2.38,2.38,0,0,0,0,3.35,2.35,2.35,0,0,0,3.34,0l3.34-3.35.06-.08,2.83-2.83A4.74,4.74,0,0,1,15.41,23.93Z"></path></svg>
</div>
<a href={faq.answerLink}>{faq.answerLinkTitle}</a>
</p> }
</div>
))
}
</PivotItem>
))
}
</Pivot>;
}
else if(categoryNames && categoryNames.length == 0 && searchValue == '') {
categories = <Placeholder
iconName='Edit'
iconText={strings.noFaqsIconText}
description={strings.noFaqsConfigured}
buttonLabel={strings.noFaqsBtn}
onConfigure={this.props.fPropertyPaneOpen}
contentClassName={styles.faqPlaceholder}
/>;
}
return (
<div className={styles.faq}>
<div className={styles.faqHeader}>
<WebPartTitle displayMode={this.props.displayMode}
title={this.props.title}
updateProperty={this.props.fUpdateProperty}
className={styles.faqWebPartTitle} />
<SearchBox
styles={{ root: { width: 200 } }}
className={styles.faqSearchBox}
placeholder='Search'
onChange={this.onChange.bind(this)}
onClear={this.onClear.bind(this)}
value={this.state.searchValue}
/>
</div>
{categories}
</div>
);
}
}

View File

@ -0,0 +1,13 @@
import { DisplayMode } from "@microsoft/sp-core-library";
import { IFaq } from "./IFaq";
export interface IAccordionsProps {
collectionData: IFaq[];
displayMode: DisplayMode;
title: string;
accordion: boolean;
guid : string;
fUpdateProperty: (value: string) => void;
fPropertyPaneOpen: () => void;
}

View File

@ -0,0 +1,7 @@
import { IFaq } from "./IFaq";
export interface IAccordionsState {
categories: { [categories: string]: IFaq[]; };
searchCategories : { [category: string]: IFaq[]; };
searchValue : string;
}

View File

@ -0,0 +1,13 @@
export interface IFaq {
questionTitle: string;
answerText: string;
answerLink: string;
answerLinkTitle: string;
category: string;
}
export enum FaqTarget {
parent = "",
blank = "_blank"
}

View File

@ -0,0 +1,11 @@
import { DisplayMode } from "@microsoft/sp-core-library";
import { IFaq } from "./IFaq";
export interface IFaqsProps {
collectionData: IFaq[];
displayMode: DisplayMode;
title: string;
categoryData : any;
fUpdateProperty: (value: string) => void;
fPropertyPaneOpen: () => void;
}

View File

@ -0,0 +1,7 @@
import { IFaq } from "./IFaq";
export interface IFaqsState {
categories: { [category: string]: IFaq[]; };
searchCategories : { [category: string]: IFaq[]; };
searchValue : string;
}

View File

@ -0,0 +1,92 @@
import * as React from 'react';
export interface IAccordionProps {
accordion?: boolean;
children?: JSX.Element[]|object;
className?: string;
onChange?: (any) => void;
}
export interface IAccordionState {
activeItems: any [];
accordion: boolean;
}
export default class Accordion extends React.Component<IAccordionProps, IAccordionState> {
public static defaultProps = {
accordion: true,
onChange: (any) => {},
className: 'accordion',
};
constructor(props) {
super(props);
const activeItems = this.preExpandedItems();
this.state = {
activeItems: activeItems,
accordion: true,
};
this.renderItems = this.renderItems.bind(this);
}
public preExpandedItems() {
const activeItems = [];
React.Children.map(this.props.children, (item, index) => {
let child = item as React.ReactElement<any>;
if (child.props.expanded) {
if (this.props.accordion) {
if (activeItems.length === 0) activeItems.push(index);
} else {
activeItems.push(index);
}
}
});
return activeItems;
}
public handleClick(key) {
let activeItems = this.state.activeItems;
if (this.props.accordion) {
activeItems = activeItems[0] === key ? [] : [key];
} else {
activeItems = [...activeItems];
const index = activeItems.indexOf(key);
const isActive = index > -1;
if (isActive) {
// remove active state
activeItems.splice(index, 1);
} else {
activeItems.push(key);
}
}
this.setState({
activeItems: activeItems,
});
this.props.onChange(this.props.accordion ? activeItems[0] : activeItems);
}
public renderItems() {
const { accordion, children } = this.props;
return React.Children.map(children, (item, index) => {
let child = item as React.ReactElement<any>;
const key = index;
const expanded = (this.state.activeItems.indexOf(key) !== -1) && (!child.props.disabled);
return React.cloneElement(child, {
disabled: child.props.disabled,
accordion: accordion,
expanded: expanded,
key: `accordion__item-${key}`,
onClick: this.handleClick.bind(this, key),
});
});
}
public render() {
const { className } = this.props;
return (
<div className={className}>
{this.renderItems()}
</div>
);
}
}

View File

@ -0,0 +1,70 @@
import * as React from 'react';
import { IAccordionItemTitleProps } from './AccordionItemTitle';
import { IAccordionItemBodyProps } from './AccordionItemBody';
export interface IAccordionItemProps {
accordion?: boolean;
onClick?: () => void;
expanded?: boolean;
children?: JSX.Element[];
className?: string;
hideBodyClassName?: string;
id?:string;
}
export default class AccordionItem extends React.Component<IAccordionItemProps, {}> {
public static defaultProps = {
accordion: true,
expanded: false,
onClick: () => {},
className: 'accordion__item',
hideBodyClassName: null,
};
constructor(props) {
super(props);
this.renderChildren = this.renderChildren.bind(this);
}
public renderChildren() {
const { accordion, expanded, onClick, children } = this.props;
const itemUuid = this.props.id;
return React.Children.map(children, (item) => {
var child = item as React.ReactElement<any>;
if (child.props.accordionElementName === 'AccordionItemTitle') {
const itemProps : IAccordionItemTitleProps = {};
itemProps.expanded = expanded;
itemProps.key = 'title';
itemProps.id = `accordion__title-${itemUuid}`;
itemProps.ariaControls = `accordion__body-${itemUuid}`;
itemProps.onClick = onClick;
itemProps.role = accordion ? 'tab' : 'button';
return React.cloneElement(child, itemProps);
} else if (child.props.accordionElementName === 'AccordionItemBody') {
const itemProps : IAccordionItemBodyProps = {};
itemProps.expanded = expanded;
itemProps.key = 'body';
itemProps.id = `accordion__body-${itemUuid}`;
itemProps.role = accordion ? 'tabpanel' : '';
return React.cloneElement(child, itemProps);
}
return item;
});
}
public render() {
const { expanded, className } = this.props;
return (
<div className={className}>
{this.renderChildren()}
</div>
);
}
}

View File

@ -0,0 +1,59 @@
import * as React from 'react';
export interface IAccordionItemBodyProps {
id?: string;
expanded?: boolean;
ariaControls?: string;
children?: JSX.Element|JSX.Element[];
className?: string;
hideBodyClassName?: string;
role?: string;
key?: string;
}
export default class AccordionItemBody extends React.Component<IAccordionItemBodyProps, {}> {
public static defaultProps = {
id: '',
expanded: false,
className: 'accordion__body',
hideBodyClassName: 'accordion__body--hidden',
role: '',
accordionElementName: 'AccordionItemBody',
};
constructor(props) {
super(props);
}
public render() {
const { id, expanded, children, className, hideBodyClassName, role } = this.props;
const ariaHidden = !expanded;
if(expanded)
{
return (
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
id={id}
className={className}
aria-hidden={ariaHidden}
role={role}
>
{children}
</div>
);
}
else
{
return (
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
id={id}
className={hideBodyClassName}
aria-hidden={ariaHidden}
role={role}
>
{children}
</div>
);
}
}
}

View File

@ -0,0 +1,56 @@
import * as React from 'react';
export interface IAccordionItemTitleProps {
id?: string;
expanded?: boolean;
onClick?: () => void;
ariaControls?: string;
children?: JSX.Element|JSX.Element[];
className?: string;
hideBodyClassName?: string;
role?: string;
key?: string;
accordionElementName?: string;
}
export default class AccordionItemTitle extends React.Component<IAccordionItemTitleProps, {}> {
public static defaultProps = {
id: '',
expanded: false,
onClick: () => {},
ariaControls: '',
className: 'accordion__title',
hideBodyClassName: null,
role: '',
accordionElementName: 'AccordionItemTitle',
};
constructor(props) {
super(props);
this.handleKeyPress = this.handleKeyPress.bind(this);
}
public handleKeyPress(evt) {
const { onClick } = this.props;
if (evt.charCode === 13 || evt.charCode === 32) {
onClick();
}
}
public render() {
const { id, expanded, ariaControls, onClick, children, className, role, hideBodyClassName } = this.props;
return (
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
id={id}
aria-expanded={expanded}
aria-controls={ariaControls}
className={className}
onClick={onClick}
role={role}
tabIndex={0}
onKeyPress={this.handleKeyPress}
>
{children}
</div>
);
}
}

View File

@ -0,0 +1,11 @@
import Accordion from './Accordion';
import AccordionItem from './AccordionItem';
import AccordionItemTitle from './AccordionItemTitle';
import AccordionItemBody from './AccordionItemBody';
export {
Accordion,
AccordionItem,
AccordionItemTitle,
AccordionItemBody,
};

View File

@ -0,0 +1,27 @@
define([], function() {
return {
"categoryDataLabel": "Category names for the Faqs",
"themePanelHeader": "Configure your Category names",
"manageCategoryBtn": "Configure Categories",
"FaqDataLabel": "Faq data",
"FaqPanelHeader": "Configure your Faqs",
"manageFaqsBtn": "Configure Faqs",
"questionTitleField": "Title",
"answerTextField": "Answer Text",
"answerLinkTitleField": "Answer Text Url Title",
"answerLinkField": "Answer Text Url",
"categoryField": "Category name",
"targetField": "Target",
"targetCurrent": "Current window",
"targetNew": "New tab",
"noFaqsIconText": "Configure your links",
"noFaqsConfigured": "Please configure the web part in order to show links",
"noFaqsBtn": "Configure",
"Type": "Type",
}
});

View File

@ -0,0 +1,30 @@
declare interface IFaqsWebPartStrings {
categoryDataLabel: string;
categoryPanelHeader: string;
manageCategoryBtn: string;
FaqDataLabel: string;
FaqPanelHeader: string;
manageFaqsBtn: string;
questionTitleField: string;
answerTextField: string;
answerLinkTitleField: string;
answerLinkField: string;
categoryField: string;
targetField: string;
targetCurrent: string;
targetNew: string;
noFaqsIconText: string;
noFaqsConfigured: string;
noFaqsBtn: string;
Type: string;
}
declare module 'FaqsWebPartStrings' {
const strings: IFaqsWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

View File

@ -0,0 +1,35 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.9/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection",
"es2015.promise"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
]
}

View File

@ -0,0 +1,29 @@
{
"extends": "./node_modules/@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}