Added a new sample react-faq app

This commit is contained in:
Swain, Ashok 2020-04-25 23:46:39 -07:00
parent fb012d4212
commit 0f13b091ed
33 changed files with 19779 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

32
samples/react-faqapp/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.8.2",
"libraryName": "spfxwebparts",
"libraryId": "f3b9b2b7-533c-42db-9a81-d3ea68e1e8b6",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,76 @@
## SharePoint Framework Faq App
## Summary
- This Web Part allows users to create Frequently Asked Questions(Faq App) in modern and classic SharePoint pages.
- This webpart allows to search within questions and answers which are stored in a SharePoint FAQ list.
- "React-autosuggest and react-accessible-accordion" react packages are used for the search and accordion control.
<p align="center">
<img src="./assets/FAQWebpart.png"/>
</p>
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-1.8.2-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Solution
Solution|Author(s)
--------|---------
react-FAQApp | Ashok Swain - LinkedIn: https://www.linkedin.com/in/ashok-kumar-swain-2627a514
## Version history
Version|Date|Comments
-------|----|--------
1.0 | April 25, 2020 | 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
- git clone the repo
- npm i
- npm i -g gulp
- gulp serve
#### Local Mode
A browser in local mode (localhost) will be opened.
https://localhost:4321/temp/workbench.html
#### SharePoint Mode
If you want to try on a real environment, open:
https://your-domain.sharepoint.com/_layouts/15/workbench.aspx
## Usage
- Create a FAQ List in SharePoint.
- Create the below fields:
- Title field can be considered as "Question" field.
- Column Name Field Type
- Title Single line of text
- Answer Multiple lines of text
- Category Single line of text
- CategorySortOrder Number
- QuestionSortOrder Number
- Edit the webpart, set the the ListName in the property pane
## Features
- This Web Part allows users to create Frequently Asked Questions(Faq App) in modern and classic SharePoint pages.
- An accordion layout is used to make it easy to browse through different questions.
- Expand answers to your most frequent questions.
- Include text, links, images in your answers.
- A search bar to make your FAQ accordion searchable.
- This webpart allows to search within questions and answers which are stored in a SharePoint FAQ list.
- Sorting is enabled on both the category & Question
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-FAQApp" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"react-faq-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/reactFaq/ReactFaqWebPart.js",
"manifest": "./src/webparts/reactFaq/ReactFaqWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"ReactFaqWebPartStrings": "lib/webparts/reactFaq/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

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

View File

@ -0,0 +1,13 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "spfxwebparts-client-side-solution",
"id": "f3b9b2b7-533c-42db-9a81-d3ea68e1e8b6",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/spfxwebparts.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

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

7
samples/react-faqapp/gulpfile.js vendored Normal file
View File

@ -0,0 +1,7 @@
'use strict';
const gulp = require('gulp');
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.`);
build.initialize(gulp);

18129
samples/react-faqapp/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
{
"name": "spfxwebparts",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.27",
"@fortawesome/free-solid-svg-icons": "^5.12.1",
"@fortawesome/react-fontawesome": "^0.1.8",
"@microsoft/sp-core-library": "1.8.2",
"@microsoft/sp-lodash-subset": "1.8.2",
"@microsoft/sp-office-ui-fabric-core": "1.8.2",
"@microsoft/sp-property-pane": "1.8.2",
"@microsoft/sp-webpart-base": "1.8.2",
"@types/es6-promise": "0.0.33",
"@types/react": "16.7.22",
"@types/react-dom": "16.8.0",
"@types/webpack-env": "1.13.1",
"office-ui-fabric-react": "6.143.0",
"react": "16.7.0",
"react-accessible-accordion": "^3.0.1",
"react-autosuggest": "^9.4.3",
"react-dom": "16.7.0",
"react-html-parser": "^2.0.2"
},
"resolutions": {
"@types/react": "16.7.22"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.8.2",
"@microsoft/sp-tslint-rules": "1.8.2",
"@microsoft/sp-module-interfaces": "1.8.2",
"@microsoft/sp-webpart-workbench": "1.8.2",
"@microsoft/rush-stack-compiler-2.9": "0.7.7",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
}
}

View File

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

View File

@ -0,0 +1,12 @@
export interface IFaqProp {
Id?: Number;
Title?: string;
Answer?: string;
BusinessCategory?: string;
Category?: string;
CategorySortOrder?: Number;
QuestionSortOrder?: Number;
IsFullRow?: string;
expandRow?: Boolean;
Modified?: Date;
}

View File

@ -0,0 +1,4 @@
import {IFaqProp} from './IFaqProp';
export interface IFaqServices {
getFaq:(listName) => Promise<IFaqProp[]>;
}

View File

@ -0,0 +1,3 @@
export{IFaqProp} from './IFaqProp';
export{IFaqServices} from './IFaqServices';

View File

@ -0,0 +1,105 @@
import { ServiceScope, ServiceKey } from '@microsoft/sp-core-library';
import { PageContext } from '@microsoft/sp-page-context';
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import { IFaqServices, IFaqProp} from '../interface';
export class FaqServices implements IFaqServices {
public static readonly serviceKey: ServiceKey<IFaqServices> = ServiceKey.create<IFaqServices>('vrd:IFaqServices', FaqServices);
private _spHttpClient: SPHttpClient;
private _pageContext: PageContext;
private _currentWebUrl: string;
constructor(serviceScope: ServiceScope) {
serviceScope.whenFinished(() => {
this._spHttpClient = serviceScope.consume(SPHttpClient.serviceKey);
this._pageContext = serviceScope.consume(PageContext.serviceKey);
this._currentWebUrl = this._pageContext.web.absoluteUrl;
});
}
public getFaq(listName): Promise<IFaqProp[]> {
return new Promise<IFaqProp[]>((resolve: any) => {
var ParentDetails = this.getFaqs(listName);
resolve(ParentDetails);
});
}
public async getMockFaq(): Promise<any> {
var tempOrg = [{
Id: 1,
Title: "What is the HR Policy?",
Answer: "There is no change in HR Policy",
Category: "HR Policy",
CategorySortOrder: 3,
QuestionSortOrder: 3,
Modified: '2020-03-27T11:07:21Z'
},
{
Id: 2,
Title: "What changes should I expect (or not) as an employee?",
Answer: "For the immediate future, There is no change.",
Category: "Top Questions",
CategorySortOrder: 2,
QuestionSortOrder: 2,
Modified: '2020-03-27T11:07:21Z'
},
{
Id: 3,
Title: "What is the finance policy in the company?",
Answer: "There is change in Finance Policy. ",
Category: "Finance Policy",
CategorySortOrder: 3,
QuestionSortOrder: 1,
Modified: '2020-03-27T11:07:21Z'
}
];
return tempOrg;
}
public async getFaqs(listName: string): Promise<IFaqProp[]> {
try {
const FaqProp:IFaqProp[] = [];
let restUrl: string = this._currentWebUrl;
restUrl += "/_api/web/lists/getbytitle('" + listName + "')/items?$select=Id,Title,Answer,Category,CategorySortOrder,QuestionSortOrder,Modified";
return await this._spHttpClient.get(restUrl, SPHttpClient.configurations.v1,
{
headers: {
"Accept": "application/json;odata=nometadata",
"odata-version": "3.0"
}
})
.then((response: SPHttpClientResponse) => {
return response.json().then((responseFormatted: any) => {
if (response.ok) {
var collection = responseFormatted.value;
for (var i = 0; i < collection.length; i++) {
FaqProp.push({
Id: collection[i].Id,
Title: collection[i].Title,
Answer: collection[i].Answer,
Category: collection[i].Category,
CategorySortOrder: collection[i].CategorySortOrder,
QuestionSortOrder: collection[i].QuestionSortOrder,
Modified: collection[i].Modified
});
}
}
else {
throw new Error(response.text().toString());
}
return FaqProp;
});
});
}
catch (error) {
console.log("Service API Error - " + error);
}
}
}

View File

@ -0,0 +1,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "a3c5e704-6f57-4f19-86ea-3a462c18780e",
"alias": "ReactFaqWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "react-faq" },
"description": { "default": "react-faq description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "react-faq"
}
}]
}

View File

@ -0,0 +1,61 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import * as strings from 'ReactFaqWebPartStrings';
import ReactFaq from './components/ReactFaq';
import { IReactFaqProps } from './components/IReactFaqProps';
export interface IReactFaqWebPartProps {
listName:string;
}
export default class ReactFaqWebPart extends BaseClientSideWebPart<IReactFaqWebPartProps> {
public render(): void {
const element: React.ReactElement<IReactFaqProps > = React.createElement(
ReactFaq,
{
listName:this.properties.listName,
ServiceScope: this.context.serviceScope
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('listName', {
label: strings.ListNameFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,31 @@
import * as React from 'react';
class ErrorBoundary extends React.Component<any, any> {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
info: null
};
}
public componentDidCatch(error, info) {
this.setState({
hasError: true,
error: error,
info: info
});
}
public render() {
if (this.state.hasError) {
return (
<div>
<h1>Oops, something went wrong :(</h1>
<p>The error: {this.state.error.toString()}</p>
<p>Where it occurred: {this.state.info.componentStack}</p>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@ -0,0 +1,5 @@
import { ServiceScope } from '@microsoft/sp-core-library';
export interface IReactFaqProps {
listName: string;
ServiceScope: ServiceScope;
}

View File

@ -0,0 +1,74 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.reactFaq {
.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);
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,633 @@
import * as React from 'react';
import { IReactFaqProps } from './IReactFaqProps';
import { IFaqProp, IFaqServices } from '../../../interface';
import { ServiceScope, ServiceKey, Environment, EnvironmentType } from '@microsoft/sp-core-library';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as fontawesome from '@fortawesome/free-solid-svg-icons';
import Autosuggest from 'react-autosuggest';
import { FaqServices } from '../../../services/FaqServices';
import ReactHtmlParser from 'react-html-parser';
import './reactAccordion.css';
import {
Accordion,
AccordionItem,
AccordionItemHeading,
AccordionItemButton,
AccordionItemPanel,
} from 'react-accessible-accordion';
import './index.css';
import ErrorBoundary from './ErrorBoundary';
export interface IFaqState {
originalData: IFaqProp[];
actualData: IFaqProp[];
BusinessCategory: any;
isLoading: Boolean;
errorCause: String;
selectedEntity: any;
show: Boolean;
filterData: any;
searchValue: String;
filteredCategoryData: any;
filteredQuestion: String;
value: String;
suggestions: any;
actualCanvasContentHeight: number;
actualCanvasWrapperHeight: number;
actualAccordionHeight: number;
}
export default class ReactFaq extends React.Component<IReactFaqProps, IFaqState> {
private faqServicesInstance: IFaqServices;
constructor(props) {
super(props);
this.state = {
originalData: [],
actualData: [],
BusinessCategory: [],
isLoading: true,
errorCause: "No Data",
selectedEntity: [],
show: false,
filterData: [],
searchValue: "",
filteredCategoryData: [],
filteredQuestion: '',
value: '',
suggestions: [],
actualCanvasContentHeight: 0,
actualCanvasWrapperHeight: 0,
actualAccordionHeight: 0
};
try {
let serviceScope: ServiceScope;
serviceScope = this.props.ServiceScope;
if (Environment.type == EnvironmentType.SharePoint || Environment.type == EnvironmentType.ClassicSharePoint) {
// Mapping to be used when webpart runs in SharePoint.
this.faqServicesInstance = serviceScope.consume(FaqServices.serviceKey);
}
else {
}
} catch (error) {
}
}
public onHandleChange = (event, value, FaqData) => {
if (FaqData.length > 0 && event != undefined) {
if (value == "") {
const FaqFilteredData = this.filterByValue(FaqData, value);
this.setState({ originalData: FaqFilteredData });
}
else {
this.setState({ originalData: this.state.actualData });
}
}
}
public onChange = (event, { newValue }) => {
if (newValue != "") {
this.setState({
value: newValue,
});
}
else {
this.setState({
originalData: this.state.actualData,
});
}
}
public onSuggestionSelected = (FaqData, event) => {
const FaqFilteredData = this.filterByValue(FaqData, event.currentTarget.innerText);
if (FaqFilteredData) {
if (FaqFilteredData.length > 0) {
var autoSuggestTextbox = document.getElementById("txtSearchBox") as HTMLTextAreaElement;
autoSuggestTextbox.value = event.currentTarget.innerText;
autoSuggestTextbox.blur();
let FaqId = FaqFilteredData[0].Id;
let FaqCategory = FaqFilteredData[0].Category;
var catData = [];
catData.push(FaqCategory);
this.setState({ filteredCategoryData: catData });
var nodElem = 'acc-' + FaqCategory;
var node = document.getElementsByClassName(nodElem);
var chNode = node[0].children[0].children[0].children[0];
var newAttr = document.createAttribute('aria-expanded');
newAttr.value = 'true';
chNode.setAttributeNode(newAttr);
node[0].children[0].children[1].removeAttribute('hidden');
var FaqNode = this.getFaqElement(FaqId);
var txtNode = document.getElementById("txtSearchBox");
var FaqEle = FaqNode[0];
var newAttrII = document.createAttribute('aria-expanded');
newAttrII.value = 'true';
FaqEle.setAttributeNode(newAttrII);
FaqEle.nextSibling.style.display = 'block';
FaqEle.nextSibling.removeAttribute('class');
if (FaqEle.previousElementSibling.previousSibling.classList != undefined) {
FaqEle.previousElementSibling.previousSibling.classList.add("hideDiv");
}
else {
// IE11 does not implement classList on <svg>
let appliedClasses = FaqEle.previousElementSibling.previousSibling.getAttribute("class") || "";
appliedClasses = appliedClasses.split(" ").indexOf("hideDiv") == -1
? appliedClasses + " hideDiv"
: appliedClasses;
FaqEle.previousElementSibling.previousSibling.setAttribute('class', appliedClasses);
}
if (FaqEle.previousElementSibling.classList != undefined) {
FaqEle.previousElementSibling.classList.remove("hideDiv");
}
else {
// IE11 does not implement classList on <svg>
let appliedClassesII = FaqEle.previousElementSibling.getAttribute("class") || "";
appliedClassesII = appliedClassesII.split(" ").indexOf("hideDiv") != -1
? appliedClassesII.replace(" hideDiv", "")
: appliedClassesII + " hideDiv";
FaqEle.previousElementSibling.setAttribute('class', appliedClassesII);
}
var txtSibEle = txtNode.nextElementSibling;
txtSibEle.classList.remove("react-autosuggest__suggestions-container--open");
FaqEle.scrollIntoView({ behavior: 'smooth' });
if (document.getElementsByClassName("mainContent") != undefined && document.getElementsByClassName("mainContent").length > 0) {
this.setFaqWebPartHeightDynamic();
}
}
}
}
public onSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: this.getSuggestions(value)
});
}
public onSuggestionsClearRequested = () => {
var autoSuggestTextbox = document.getElementById("txtSearchBox") as HTMLTextAreaElement;
if(autoSuggestTextbox.value == ""){
autoSuggestTextbox.value = "";
this.setState({
suggestions: [],
value: ""
});
}
}
// When suggestion is clicked, Autosuggest needs to populate the input
// based on the clicked suggestion. Teach Autosuggest how to calculate the
// input value for every given suggestion.
public getSuggestionValue = (suggestion) => {
if (suggestion.length < 0) {
return "";
}
else {
return suggestion.Title;
}
}
public getSuggestions = (value) => {
const inputValue = value.trim().toLowerCase();
const inputLength = inputValue.length;
return inputLength === 0 ? [] : this.state.actualData.filter(lang =>
(lang.Title.toLowerCase().indexOf(inputValue) !== -1) ||
(lang.Answer.toLowerCase().indexOf(inputValue) !== -1)
);
}
public renderSuggestion = (suggestion) => {
return (
<div>
{suggestion.Title}
</div>
);
}
public setNodeValues = () => {
var SPCanvasFirstParent = (document.getElementsByClassName("mainContent") != undefined && document.getElementsByClassName("mainContent").length > 0) ? document.getElementsByClassName("SPCanvas")[0].parentElement.offsetHeight : 0;
var SPCanvasSecondParent = (document.getElementsByClassName("mainContent") != undefined && document.getElementsByClassName("mainContent").length > 0) ? document.getElementsByClassName("SPCanvas")[0].parentElement.parentElement.offsetHeight : 0;
this.setState({
actualCanvasContentHeight: SPCanvasFirstParent,
actualCanvasWrapperHeight: SPCanvasSecondParent
}, this.dynamicHeight);
}
public async componentDidMount() {
if (Environment.type == EnvironmentType.SharePoint || Environment.type == EnvironmentType.ClassicSharePoint) {
this.loadFaq();
}
else {
//await this.loadMockFaq();
}
this.setState({
actualAccordionHeight: (document.getElementsByClassName("accordion") != undefined && document.getElementsByClassName("accordion").length > 0) ? document.getElementsByClassName("accordion")[0].parentElement.offsetHeight : 0
});
var ua = window.navigator.userAgent;
var trident = ua.indexOf('Trident/');
if (trident > 0) {
// IE 11 => return version number
var rv = ua.indexOf('rv:');
if ((parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10)) < 12) {
document.getElementById("txtSearchBox").style.paddingTop = '3px';
}
}
}
public async loadFaq() {
await this.faqServicesInstance.getFaq(this.props.listName).then((FaqData: IFaqProp[]) => {
try {
this.setState(
{
actualData: FaqData,
originalData: FaqData
}
);
}
catch (error) {
console.log("Error Occurred :" + error);
}
});
}
public categoryAndQuestionSorting = (Data) => {
var result = [];
// Get Distinct category for sorting Category
var distCate = this.distinct(Data, "Category");
distCate.sort((c, d) => {
return c.CategorySortOrder - d.CategorySortOrder;
});
//Sorting the FQA as per CategorySortOrder
distCate.forEach((distCateItem) => {
Data.map((item) => {
if (distCateItem.Category.toLowerCase() == item.Category.toLowerCase()) {
result.push(item);
}
});
});
//Sorting the FQA as per QuestionSortOrder
result.sort((a, b) => {
return a.QuestionSortOrder - b.QuestionSortOrder;
});
return result;
}
public distinct(items, prop) {
var unique = [];
var distinctItems = [];
for (const item of items) {
if (unique[item[prop]] === undefined) {
distinctItems.push(item);
}
unique[item[prop]] = 0;
}
return distinctItems;
}
public filterByValue = (arrayData, value) => {
return arrayData.filter(o =>
this.includes(o["Title"].toLowerCase(), value.toLowerCase()) || this.includes(o["Answer"].toLowerCase(), value.toLowerCase())
);
}
public getFaqElement = (FaqId) => {
return Array.prototype.filter.call(
document.getElementsByTagName('span'),
(el) => el.getAttribute('data-id') == FaqId
);
}
public formatDate = (ModifiedDate) => {
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const dt = new Date(ModifiedDate);
var hours = dt.getHours();
var minutes = dt.getMinutes();
var secs = dt.getSeconds();
var ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12;
hours = hours ? hours : 12; // the hour '0' should be '12'
var strTime = hours + ':' + minutes + ':' + secs + ' ' + ampm;
return monthNames[dt.getMonth()] + " " + dt.getDate() + ", " + dt.getFullYear() + " " + strTime;
}
public loadMoreEvent(event: any): void {
var clickedId = event.target.getAttribute('data-id');
console.log('clicked - ' + clickedId + ' ' + event.target);
if (event.target.nodeName == "SPAN") {
if (event.target.nextElementSibling.classList.contains("hideDiv")) {
event.target.nextElementSibling.classList.remove("hideDiv");
try {
if (event.currentTarget.children[0].classList != undefined) {
event.currentTarget.children[0].classList.add("hideDiv");
}
else {
// IE11 does not implement classList on <svg>
let appliedClasses = event.currentTarget.children[0].getAttribute("class") || "";
appliedClasses = appliedClasses.split(" ").indexOf("hideDiv") == -1
? appliedClasses + " hideDiv"
: appliedClasses;
event.currentTarget.children[0].setAttribute('class', appliedClasses);
}
if (event.currentTarget.children[1].classList != undefined) {
event.currentTarget.children[1].classList.remove("hideDiv");
}
else {
// IE11 does not implement classList on <svg>
let appliedClassesII = event.currentTarget.children[1].getAttribute("class") || "";
appliedClassesII = appliedClassesII.split(" ").indexOf("hideDiv") != -1
? appliedClassesII.replace(" hideDiv", "")
: appliedClassesII + " hideDiv";
event.currentTarget.children[1].setAttribute('class', appliedClassesII);
}
event.currentTarget.children[3].removeAttribute("style");
}
catch (e) { }
}
else {
event.target.nextElementSibling.classList.add("hideDiv");
try {
if (event.currentTarget.children[1].classList != undefined) {
event.currentTarget.children[1].classList.add("hideDiv");
}
else {
// IE11 does not implement classList on <svg>
let appliedClasses = event.currentTarget.children[1].getAttribute("class") || "";
appliedClasses = appliedClasses.split(" ").indexOf("hideDiv") == -1
? appliedClasses + " hideDiv"
: appliedClasses;
event.currentTarget.children[1].setAttribute('class', appliedClasses);
}
if (event.currentTarget.children[0].classList != undefined) {
event.currentTarget.children[0].classList.remove("hideDiv");
}
else {
// IE11 does not implement classList on <svg>
let appliedClassesII = event.currentTarget.children[0].getAttribute("class") || "";
appliedClassesII = appliedClassesII.split(" ").indexOf("hideDiv") != -1
? appliedClassesII.replace(" hideDiv", "")
: appliedClassesII + " hideDiv";
event.currentTarget.children[0].setAttribute('class', appliedClassesII);
}
event.currentTarget.children[3].removeAttribute("style");
}
catch (e) { }
}
}
else {
if (event.target.nodeName == "path") {
if (event.currentTarget.children[1] != undefined) {
event.currentTarget.children[1].classList.add("hideDiv");
event.currentTarget.children[0].classList.add("hideDiv");
}
else {
// IE11 does not implement classList on <svg>
let appliedClasses = event.currentTarget.children[0].getAttribute("class") || "";
appliedClasses = appliedClasses + " hideDiv";
event.currentTarget.children[0].setAttribute('class', appliedClasses);
let appliedClassesII = event.currentTarget.children[1].getAttribute("class") || "";
appliedClassesII = appliedClassesII + " hideDiv";
event.currentTarget.children[1].setAttribute('class', appliedClassesII);
}
if (event.target.parentElement.getAttribute('data-icon') == "plus-square") {
event.target.parentElement.nextElementSibling.nextElementSibling.nextElementSibling.classList.remove("hideDiv");
if (event.currentTarget.children[1].classList != undefined) {
event.currentTarget.children[1].classList.remove("hideDiv");
}
else {
// IE11 does not implement classList on <svg>
let appliedClassesII = event.currentTarget.children[1].getAttribute("class") || "";
appliedClassesII = appliedClassesII.split(" ").indexOf("hideDiv") != -1
? appliedClassesII.replace(" hideDiv", "")
: appliedClassesII + " hideDiv";
event.currentTarget.children[1].setAttribute('class', appliedClassesII);
}
}
else {
event.target.parentElement.nextElementSibling.nextElementSibling.classList.add("hideDiv");
event.target.parentElement.nextElementSibling.nextElementSibling.removeAttribute("style");
if (event.currentTarget.children[0].classList != undefined) {
event.currentTarget.children[0].classList.remove("hideDiv");
}
else {
// IE11 does not implement classList on <svg>
let appliedClassesII = event.currentTarget.children[0].getAttribute("class") || "";
appliedClassesII = appliedClassesII.split(" ").indexOf("hideDiv") != -1
? appliedClassesII.replace(" hideDiv", "")
: appliedClassesII + " hideDiv";
event.currentTarget.children[0].setAttribute('class', appliedClassesII);
}
}
}
else if (event.target.nodeName == "svg") {
if (event.target.classList != undefined) {
event.target.classList.add("hideDiv");
}
else {
// IE11 does not implement classList on <svg>
let appliedClasses = event.target.getAttribute("class") || "";
appliedClasses = appliedClasses + " hideDiv";
event.target.setAttribute('class', appliedClasses);
}
//alert('path');
if (event.target.getAttribute('data-icon') == "plus-square") {
event.target.nextElementSibling.nextElementSibling.nextElementSibling.classList.remove("hideDiv");
//event.target.nextElementSibling.classList.remove("hideDiv");
if (event.target.nextElementSibling.classList != undefined) {
event.target.nextElementSibling.classList.remove("hideDiv");
}
else {
// IE11 does not implement classList on <svg>
let appliedClassesII = event.target.nextElementSibling.getAttribute("class") || "";
appliedClassesII = appliedClassesII.split(" ").indexOf("hideDiv") != -1
? appliedClassesII.replace(" hideDiv", "")
: appliedClassesII + " hideDiv";
event.target.nextElementSibling.setAttribute('class', appliedClassesII);
}
}
else {
event.target.nextElementSibling.nextElementSibling.classList.add("hideDiv");
event.target.nextElementSibling.nextElementSibling.removeAttribute("style");
if (event.target.previousElementSibling.classList != undefined) {
event.target.previousElementSibling.classList.remove("hideDiv");
}
else {
// IE11 does not implement classList on <svg>
let appliedClassesII = event.target.previousElementSibling.getAttribute("class") || "";
appliedClassesII = appliedClassesII.split(" ").indexOf("hideDiv") != -1
? appliedClassesII.replace(" hideDiv", "")
: appliedClassesII + " hideDiv";
event.target.previousElementSibling.setAttribute('class', appliedClassesII);
}
}
}
else {
if (event.target.getAttribute('data-icon') == "plus-square") {
event.target.nextElementSibling.nextElementSibling.nextElementSibling.classList.remove("hideDiv");
event.target.nextElementSibling.classList.remove("hideDiv");
event.target.classList.add("hideDiv");
}
else {
event.target.nextElementSibling.nextElementSibling.classList.add("hideDiv");
event.target.previousElementSibling.classList.add("hideDiv");
event.target.classList.add("hideDiv");
event.target.removeAttribute("style");
}
}
}
if (document.getElementsByClassName("mainContent") != undefined && document.getElementsByClassName("mainContent").length > 0) {
this.setFaqWebPartHeightDynamic();
}
}
public dynamicHeight = () => {
var SPCanvasNode = document.getElementsByClassName("SPCanvas");
var accordionNode = document.getElementsByClassName("accordion");
if (SPCanvasNode.length > 0 && accordionNode.length > 0) {
SPCanvasNode[0].parentElement.style.height = (this.state.actualCanvasContentHeight + (accordionNode[0].parentElement.offsetHeight - this.state.actualAccordionHeight)) + "px";
SPCanvasNode[0].parentElement.parentElement.style.height = (this.state.actualCanvasWrapperHeight + (accordionNode[0].parentElement.offsetHeight - this.state.actualAccordionHeight)) + "px";
}
}
public setFaqWebPartHeightDynamic = () => {
if (this.state.actualCanvasContentHeight == 0) {
this.setNodeValues();
}
else {
this.dynamicHeight();
}
}
public accordionOnchange = () => {
if (document.getElementsByClassName("mainContent") != undefined && document.getElementsByClassName("mainContent").length > 0) {
this.setFaqWebPartHeightDynamic();
}
}
public includes = (container, value) => {
var returnValue = false;
var pos = container.indexOf(value);
if (pos >= 0) {
returnValue = true;
}
return returnValue;
}
public render(): React.ReactElement<IReactFaqProps> {
var uniqueBC = [];
var FaqData = [];
if (this.state.originalData.length > 0) {
FaqData = this.categoryAndQuestionSorting(this.state.originalData);
uniqueBC = this.distinct(FaqData, "BusinessCategory");
}
const { value, suggestions } = this.state;
// Autosuggest will pass through all these props to the input.
const inputProps = {
placeholder: 'Search existing Faq',
value,
onChange: this.onChange,
id: 'txtSearchBox'
};
return (
<div className={`container`}>
<div className="FaqSearchBox" accept-charset="UTF-8">
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={this.renderSuggestion}
onSuggestionSelected={this.onSuggestionSelected.bind(this, this.state.actualData)}
inputProps={inputProps}
focusInputOnSuggestionClick={false}
/>
</div>
<ErrorBoundary>
<div className="clearBody">
<Accordion allowMultipleExpanded={true} allowZeroExpanded={true} onChange={this.accordionOnchange.bind(this)} preExpanded={this.state.filteredCategoryData}
>
{uniqueBC.map((item) => (
<div>
{this.distinct(FaqData, "Category").map((allCat) => (
<div className={`acc-${allCat.Category}`}>
<AccordionItem uuid={allCat.Category}>
<AccordionItemHeading>
<AccordionItemButton >
{allCat.Category}
</AccordionItemButton>
</AccordionItemHeading>
<AccordionItemPanel>
<div className="acc-item-panel">
{FaqData.filter(it => it.Category == allCat.Category).map((allFaq) => (
<div
className="acc-item"
data-id={allFaq.Id}
onClick={
event => this.loadMoreEvent(event)
}>
<FontAwesomeIcon icon={fontawesome.faPlusSquare} size="1x" data-id={allFaq.Id} className={"plusminusImg"} />
<FontAwesomeIcon icon={fontawesome.faMinusSquare} size="1x" data-id={allFaq.Id} className={"plusminusImg hideDiv"} />
<span className="acc-span-text" data-id={allFaq.Id}>{allFaq.Title}</span>
<div className="hideDiv">
<span className="acc-modified-text">Last Modified : {this.formatDate(allFaq.Modified)}</span>
<div className="acc-answer">
{ReactHtmlParser(allFaq.Answer)}
</div>
</div>
</div>
))}
</div>
</AccordionItemPanel>
</AccordionItem>
</div>
))}
</div>
))}
</Accordion>
</div>
</ErrorBoundary>
</div>
);
}
}

View File

@ -0,0 +1,284 @@
.headerSection {
background-color: #E20074;
color: #fff;
padding: 10px 10px 20px 10px;
font-size: 20px;
}
.fullrow {
padding: 10px;
border-bottom: 1px solid #ccc;
}
.faqTitle {
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
}
.faqQuestionsList span {
color: #E20074;
display: block;
padding-bottom: 5px;
}
.col3row {
width: 33.33%;
float: left;
box-sizing: border-box;
padding: 10px;
}
.bodySection {
float: left;
width: 100%;
border: 1px solid #ccc;
box-sizing: border-box;
}
.clearBody{
float: left;
width: 100%;
padding-top: 40px;
}
.imgSec{
float: left;
padding-right: 10px;
}
.acc-item{
float: left;
color: #e20074;
cursor: pointer;
width: 100%;
margin: 5px 0;
}
.plusminusImg{
padding-right: 10px;
font-size: 28px;
position: relative;
top: 4px;
}
.hideDiv{
display: none;
}
.acc-answer{
color: #999;
padding-top: 10px;
}
.accordion__panel{
width: 100%;
border: 1px solid #e9e9e9;
margin-top: -15px;
box-sizing: border-box;
margin-bottom: 20px;
}
.titlefullrow{
margin: 10px 20px;
border: 1px solid #e20074;
border-radius: 5px;
}
.titlefullrow:hover{
box-shadow: inset 0 0 5px #e20074;
transition: box-shadow 0.3s ease-in-out;
}
.titlecol3row {
width: calc(33.33% - 40px);
float: left;
border: 1px solid #e20074;
box-sizing: border-box;
margin: 10px 20px;
border-radius: 5px;
}
.titlecol3row:hover{
box-shadow: inset 0 0 5px #e20074;
transition: box-shadow 0.3s ease-in-out;
}
.titlefaqTitle{
text-align: center;
padding: 15px;
color: #5c5c5c;
font-size: 18px;
font-weight: 600;
}
.titlecol3row:hover .titlefaqTitle, .titlefullrow:hover .titlefaqTitle{
color: #E20074;
}
.acc-item-panel{
display: inline-block;
}
.accordion__button {
background-color: #fff;
color: #5c5c5c !important;
padding: 10px;
text-align: left;
border: none;
font-size: 18px;
margin-bottom: 15px;
font-weight: 600;
border: 1px solid #e20074;
border-radius: 4px;
}
.accordion{
margin-top: 15px;
}
.titleView{
width: 100%;
}
.viewAllQuestions{
font-size: 12px;
text-align: right;
text-decoration: underline;
color: #e20074;
}
.acc-span-text{
color: #5c5c5c!important;
cursor: pointer;
}
.acc-modified-text{
font-family: 'Tele-GroteskFet';
font-size: 14px;
color: rgb(102, 102, 102) !important;
}
.accordion__button{
font-family: 'Tele-GroteskFet';
font-size: 20px;
color: #000;
/* text-transform: capitalize; */
}
.acc-span-text{
font-family: 'Tele-GroteskHal';
font-size: 20px;
color: #000;
/* text-transform: capitalize; */
cursor: pointer !important;
}
.acc-answer{
font-family: 'Tele-GroteskHal' !important;
font-size: 18px !important;
color: #000 !important;
}
.d-none{
display: none;
}
.react-autosuggest__container {
position: relative;
}
.react-autosuggest__input {
width: 80%;
height: 40px;
padding: 10px 20px;
font-family: 'Tele-GroteskHal' !important;
font-weight: 300;
font-size: 20px;
border: 1px solid #e20074;
border-radius: 30px;
box-sizing: border-box;
}
.react-autosuggest__input::before{
position: relative;
top: 50%;
transform: translateY(-50%);
background-color: #fff;
color: #ddd;
content: "\1F50D";
font-size: 18px;
position: absolute;
left: 15px;
}
.react-autosuggest__input--focused {
outline: none;
}
.react-autosuggest__suggestions-container {
display: none;
}
.react-autosuggest__suggestions-container--open {
display: block;
position: absolute;
top: 51px;
width: 80%;
border: 1px solid #aaa;
background-color: #fff;
font-family: Helvetica, sans-serif;
font-weight: 300;
font-size: 16px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
z-index: 2;
box-shadow: 10px 0 5px 0px #888;
left: 50%;
transform: translate(-50%, 0%);
}
.react-autosuggest__suggestions-list {
margin: 0;
padding: 0;
list-style-type: none;
}
.react-autosuggest__suggestion {
cursor: pointer;
padding: 10px 20px;
}
.react-autosuggest__suggestion--highlighted {
background-color: #ddd;
}
.react-autosuggest__section-container {
border-top: 1px dashed #ccc;
}
.react-autosuggest__section-container--first {
border-top: 0;
}
.react-autosuggest__section-title {
padding: 10px 0 0 10px;
font-size: 12px;
color: #777;
}
.faqSearchBox{
margin: 20px 0;
width: 100%;
text-align: center;
}
.acc-answer a {
color: #e20074 !important;
}
.acc-answer a span{
color: #e20074 !important;
}

View File

@ -0,0 +1,67 @@
:focus {
outline: none;
}
.accordion {
border-radius: 2px;
}
.accordion__item + .accordion__item {
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.accordion__button {
background-color: #fff !important;
color: #5c5c5c !important;
padding: 10px;
text-align: left;
border: none;
font-size: 18px;
margin-bottom: 15px;
font-weight: 600;
border: 1px solid #e20074 !important;
border-radius: 4px;
cursor: pointer !important;
}
.accordion__button:hover {
background-color: #ddd;
border-color: #e20074 !important;
border-width: 2px !important;
}
.accordion__button:before {
display: inline-block;
content: '';
height: 10px;
width: 10px;
margin-right: 12px;
border-bottom: 2px solid #e20074;
border-right: 2px solid #e20074;
transform: rotate(-45deg);
}
.accordion__button[aria-expanded='true']::before,
.accordion__button[aria-selected='true']::before {
transform: rotate(45deg);
}
.accordion__panel {
padding: 20px;
animation: fadein 0.35s ease-in;
}
h2 {
text-align: center;
color: #444;
}
@keyframes fadein {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}

View File

@ -0,0 +1,8 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field",
"ListNameFieldLabel": "List Name"
}
});

View File

@ -0,0 +1,12 @@
declare interface IReactFaqWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
ListNameFieldLabel: string;
}
declare module 'ReactFaqWebPartStrings' {
const strings: IReactFaqWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,38 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-2.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": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "@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-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}