initial release
This commit is contained in:
parent
51eebc82de
commit
246f63aa9c
|
@ -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
|
|
@ -0,0 +1,16 @@
|
|||
!dist
|
||||
config
|
||||
|
||||
gulpfile.js
|
||||
|
||||
release
|
||||
src
|
||||
temp
|
||||
|
||||
tsconfig.json
|
||||
tslint.json
|
||||
|
||||
*.log
|
||||
|
||||
.yo-rc.json
|
||||
.vscode
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 -->"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -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'));
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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",
|
||||
}
|
||||
});
|
|
@ -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 |
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue