mirror of
https://github.com/pnp/sp-dev-fx-webparts.git
synced 2025-02-18 19:07:12 +00:00
Merge pull request #2407 from arunkumarperumal/react-faqs
This commit is contained in:
commit
2ec3ae44db
39
samples/react-faqs/.devcontainer/devcontainer.json
Normal file
39
samples/react-faqs/.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,39 @@
|
||||
// For more information on how to run this SPFx project in a VS Code Remote Container, please visit https://aka.ms/spfx-devcontainer
|
||||
{
|
||||
"name": "SPFx 1.13.1",
|
||||
"image": "docker.io/m365pnp/spfx:1.13.1",
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {},
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"editorconfig.editorconfig",
|
||||
"dbaeumer.vscode-eslint"
|
||||
],
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [
|
||||
4321,
|
||||
35729
|
||||
],
|
||||
"portsAttributes": {
|
||||
"4321": {
|
||||
"protocol": "https",
|
||||
"label": "Manifest",
|
||||
"onAutoForward": "silent",
|
||||
"requireLocalPort": true
|
||||
},
|
||||
// Not needed for SPFx>= 1.12.1
|
||||
// "5432": {
|
||||
// "protocol": "https",
|
||||
// "label": "Workbench",
|
||||
// "onAutoForward": "silent"
|
||||
// },
|
||||
"35729": {
|
||||
"protocol": "https",
|
||||
"label": "LiveReload",
|
||||
"onAutoForward": "silent",
|
||||
"requireLocalPort": true
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "bash .devcontainer/spfx-startup.sh",
|
||||
"remoteUser": "node"
|
||||
}
|
33
samples/react-faqs/.devcontainer/spfx-startup.sh
Normal file
33
samples/react-faqs/.devcontainer/spfx-startup.sh
Normal file
@ -0,0 +1,33 @@
|
||||
echo
|
||||
echo -e "\e[1;94mInstalling Node dependencies\e[0m"
|
||||
npm install
|
||||
|
||||
## commands to create dev certificate and copy it to the root folder of the project
|
||||
echo
|
||||
echo -e "\e[1;94mGenerating dev certificate\e[0m"
|
||||
gulp trust-dev-cert
|
||||
|
||||
# Convert the generated PEM certificate to a CER certificate
|
||||
openssl x509 -inform PEM -in ~/.rushstack/rushstack-serve.pem -outform DER -out ./spfx-dev-cert.cer
|
||||
|
||||
# Copy the PEM ecrtificate for non-Windows hosts
|
||||
cp ~/.rushstack/rushstack-serve.pem ./spfx-dev-cert.pem
|
||||
|
||||
## add *.cer to .gitignore to prevent certificates from being saved in repo
|
||||
if ! grep -Fxq '*.cer' ./.gitignore
|
||||
then
|
||||
echo "# .CER Certificates" >> .gitignore
|
||||
echo "*.cer" >> .gitignore
|
||||
fi
|
||||
|
||||
## add *.pem to .gitignore to prevent certificates from being saved in repo
|
||||
if ! grep -Fxq '*.pem' ./.gitignore
|
||||
then
|
||||
echo "# .PEM Certificates" >> .gitignore
|
||||
echo "*.pem" >> .gitignore
|
||||
fi
|
||||
|
||||
echo
|
||||
echo -e "\e[1;92mReady!\e[0m"
|
||||
|
||||
echo -e "\n\e[1;94m**********\nOptional: if you plan on using gulp serve, don't forget to add the container certificate to your local machine. Please visit https://aka.ms/spfx-devcontainer for more information\n**********"
|
37
samples/react-faqs/.gitignore
vendored
Normal file
37
samples/react-faqs/.gitignore
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
# 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
|
||||
# .CER Certificates
|
||||
*.cer
|
||||
# .PEM Certificates
|
||||
*.pem
|
16
samples/react-faqs/.npmignore
Normal file
16
samples/react-faqs/.npmignore
Normal file
@ -0,0 +1,16 @@
|
||||
!dist
|
||||
config
|
||||
|
||||
gulpfile.js
|
||||
|
||||
release
|
||||
src
|
||||
temp
|
||||
|
||||
tsconfig.json
|
||||
tslint.json
|
||||
|
||||
*.log
|
||||
|
||||
.yo-rc.json
|
||||
.vscode
|
13
samples/react-faqs/.yo-rc.json
Normal file
13
samples/react-faqs/.yo-rc.json
Normal 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"
|
||||
}
|
||||
}
|
121
samples/react-faqs/README.md
Normal file
121
samples/react-faqs/README.md
Normal file
@ -0,0 +1,121 @@
|
||||
---
|
||||
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 Field Collection 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 web part using Property Pane. Search text is highlighted 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)
|
||||
|
||||
## Compatibility
|
||||
|
||||
![SPFx 1.13.1](https://img.shields.io/badge/SPFx-1.13.1-green.svg)
|
||||
![Node.js v14 | v12](https://img.shields.io/badge/Node.js-v14%20%7C%20v12-green.svg)
|
||||
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
|
||||
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
|
||||
![Does not work with SharePoint 2016 (Feature Pack 2)](https://img.shields.io/badge/SharePoint%20Server%202016%20(Feature%20Pack%202)-Incompatible-red.svg "SharePoint Server 2016 Feature Pack 2 requires SPFx 1.1")
|
||||
![Local Workbench Unsupported](https://img.shields.io/badge/Local%20Workbench-Unsupported-red.svg "Local workbench is no longer available as of SPFx 1.13 and above")
|
||||
![Hosted Workbench Compatible](https://img.shields.io/badge/Hosted%20Workbench-Compatible-green.svg)
|
||||
![Compatible with Remote Containers](https://img.shields.io/badge/Remote%20Containers-Compatible-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](https://github.com/arunkumarperumal) - LinkedIn: <https://www.linkedin.com/in/arunkumarperumal/>
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0|March 07, 2022|Initial release
|
||||
|
||||
|
||||
## Minimal path to awesome
|
||||
|
||||
- Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-faqs) then unzip it)
|
||||
- Ensure that you are at the solution folder
|
||||
- in the command-line run:
|
||||
- `npm install`
|
||||
- `gulp serve`
|
||||
|
||||
> This sample can also be opened with [VS Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview). Visit <https://aka.ms/spfx-devcontainer> for further instructions.
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
## Help
|
||||
|
||||
|
||||
We do not support samples, but this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.
|
||||
|
||||
If you're having issues building the solution, please run [spfx doctor](https://pnp.github.io/cli-microsoft365/cmd/spfx/spfx-doctor/) from within the solution folder to diagnose incompatibility issues with your environment.
|
||||
|
||||
You can try looking at [issues related to this sample](https://github.com/pnp/sp-dev-fx-webparts/issues?q=label%3A%22sample%3A%20react-faqs%22) to see if anybody else is having the same issues.
|
||||
|
||||
You can also try looking at [discussions related to this sample](https://github.com/pnp/sp-dev-fx-webparts/discussions?discussions_q=react-faqs) and see what the community is saying.
|
||||
|
||||
If you encounter any issues while using this sample, [create a new issue](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected%2Csample%3A%20react-faqs&template=bug-report.yml&sample=react-faqs&authors=@arunkumarperumal&title=react-faqs%20-%20).
|
||||
|
||||
For questions regarding this sample, [create a new question](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aquestion%2Csample%3A%20react-faqs&template=question.yml&sample=react-faqs&authors=@arunkumarperumal&title=react-faqs%20-%20).
|
||||
|
||||
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aenhancement%2Csample%3A%20react-faqs&template=suggestion.yml&sample=react-faqs&authors=@arunkumarperumal&title=react-faqs%20-%20).
|
||||
|
||||
## 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.**
|
||||
|
||||
<img src="https://pnptelemetry.azurewebsites.net/sp-dev-fx-webparts/samples/react-faqs" />
|
BIN
samples/react-faqs/assets/FAQWebpart.png
Normal file
BIN
samples/react-faqs/assets/FAQWebpart.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
50
samples/react-faqs/assets/sample.json
Normal file
50
samples/react-faqs/assets/sample.json
Normal file
@ -0,0 +1,50 @@
|
||||
[
|
||||
{
|
||||
"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",
|
||||
"downloadUrl": "https://pnp.github.io/download-partial/?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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
20
samples/react-faqs/config/config.json
Normal file
20
samples/react-faqs/config/config.json
Normal 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"
|
||||
}
|
||||
}
|
7
samples/react-faqs/config/deploy-azure-storage.json
Normal file
7
samples/react-faqs/config/deploy-azure-storage.json
Normal 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 -->"
|
||||
}
|
21
samples/react-faqs/config/package-solution.json
Normal file
21
samples/react-faqs/config/package-solution.json
Normal 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"
|
||||
}
|
||||
}
|
6
samples/react-faqs/config/serve.json
Normal file
6
samples/react-faqs/config/serve.json
Normal 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"
|
||||
}
|
4
samples/react-faqs/config/write-manifests.json
Normal file
4
samples/react-faqs/config/write-manifests.json
Normal 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
16
samples/react-faqs/gulpfile.js
vendored
Normal 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
23492
samples/react-faqs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
samples/react-faqs/package.json
Normal file
34
samples/react-faqs/package.json
Normal 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"
|
||||
}
|
||||
}
|
1
samples/react-faqs/src/index.ts
Normal file
1
samples/react-faqs/src/index.ts
Normal 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
256
samples/react-faqs/src/webparts/faQs/FaQsWebPart.ts
Normal file
256
samples/react-faqs/src/webparts/faQs/FaQsWebPart.ts
Normal 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'}
|
||||
]
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
231
samples/react-faqs/src/webparts/faQs/components/Accordions.tsx
Normal file
231
samples/react-faqs/src/webparts/faQs/components/Accordions.tsx
Normal 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>
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
}
|
242
samples/react-faqs/src/webparts/faQs/components/Faqs.module.scss
Normal file
242
samples/react-faqs/src/webparts/faQs/components/Faqs.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
220
samples/react-faqs/src/webparts/faQs/components/Faqs.tsx
Normal file
220
samples/react-faqs/src/webparts/faQs/components/Faqs.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import { IFaq } from "./IFaq";
|
||||
|
||||
export interface IAccordionsState {
|
||||
categories: { [categories: string]: IFaq[]; };
|
||||
searchCategories : { [category: string]: IFaq[]; };
|
||||
searchValue : string;
|
||||
}
|
13
samples/react-faqs/src/webparts/faQs/components/IFaq.ts
Normal file
13
samples/react-faqs/src/webparts/faQs/components/IFaq.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export interface IFaq {
|
||||
questionTitle: string;
|
||||
answerText: string;
|
||||
answerLink: string;
|
||||
answerLinkTitle: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
export enum FaqTarget {
|
||||
parent = "",
|
||||
blank = "_blank"
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import { IFaq } from "./IFaq";
|
||||
|
||||
export interface IFaqsState {
|
||||
categories: { [category: string]: IFaq[]; };
|
||||
searchCategories : { [category: string]: IFaq[]; };
|
||||
searchValue : string;
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
27
samples/react-faqs/src/webparts/faQs/loc/en-us.js
vendored
Normal file
27
samples/react-faqs/src/webparts/faQs/loc/en-us.js
vendored
Normal 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",
|
||||
}
|
||||
});
|
30
samples/react-faqs/src/webparts/faQs/loc/mystrings.d.ts
vendored
Normal file
30
samples/react-faqs/src/webparts/faQs/loc/mystrings.d.ts
vendored
Normal 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 |
35
samples/react-faqs/tsconfig.json
Normal file
35
samples/react-faqs/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
29
samples/react-faqs/tslint.json
Normal file
29
samples/react-faqs/tslint.json
Normal 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
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
"title": "YOUR-SAMPLE-TITLE",
|
||||
"shortDescription": "YOUR-SHORT-DESCRIPTION",
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/YOUR-SAMPLE-NAME",
|
||||
"downloadUrl": "https://download.url/file.zip",
|
||||
"downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/YOUR-SAMPLE-NAME",
|
||||
"longDescription": [
|
||||
"YOUR-SHORT-DESCRIPTION"
|
||||
],
|
||||
|
Loading…
x
Reference in New Issue
Block a user