Merge branch 'pnp:main' into Birthdays
|
@ -27364,9 +27364,9 @@
|
|||
}
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",
|
||||
"integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ=="
|
||||
"version": "0.7.33",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
|
||||
"integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.4.10",
|
||||
|
|
|
@ -23527,9 +23527,9 @@
|
|||
}
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.19",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz",
|
||||
"integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ=="
|
||||
"version": "0.7.33",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
|
||||
"integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.4.10",
|
||||
|
|
|
@ -8407,8 +8407,8 @@ typescript@~2.4.1:
|
|||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.2.tgz#f8395f85d459276067c988aa41837a8f82870844"
|
||||
|
||||
ua-parser-js@^0.7.18, ua-parser-js@^0.7.9:
|
||||
version "0.7.28"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
|
||||
version "0.7.33"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532"
|
||||
|
||||
uglify-js@3.0.x:
|
||||
version "3.0.24"
|
||||
|
|
|
@ -25584,9 +25584,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.19",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz",
|
||||
"integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ=="
|
||||
"version": "0.7.33",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
|
||||
"integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.4.10",
|
||||
|
|
|
@ -25759,9 +25759,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.28",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz",
|
||||
"integrity": "sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g=="
|
||||
"version": "0.7.33",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
|
||||
"integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.4.10",
|
||||
|
|
|
@ -17029,9 +17029,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.32",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.32.tgz",
|
||||
"integrity": "sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==",
|
||||
"version": "0.7.33",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
|
||||
"integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==",
|
||||
"dev": true
|
||||
},
|
||||
"uglify-js": {
|
||||
|
|
|
@ -24166,9 +24166,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.21",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz",
|
||||
"integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ=="
|
||||
"version": "0.7.33",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
|
||||
"integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.4.10",
|
||||
|
|
|
@ -23934,9 +23934,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.20",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.20.tgz",
|
||||
"integrity": "sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw=="
|
||||
"version": "0.7.33",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
|
||||
"integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.4.10",
|
||||
|
|
|
@ -5706,9 +5706,9 @@
|
|||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"version": "2.6.8",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz",
|
||||
"integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==",
|
||||
"requires": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
}
|
||||
|
@ -5958,9 +5958,9 @@
|
|||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"version": "2.6.8",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz",
|
||||
"integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==",
|
||||
"requires": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
}
|
||||
|
@ -29161,9 +29161,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",
|
||||
"integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ=="
|
||||
"version": "0.7.33",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
|
||||
"integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.4.10",
|
||||
|
|
|
@ -22488,9 +22488,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",
|
||||
"integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ=="
|
||||
"version": "0.7.33",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
|
||||
"integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.4.10",
|
||||
|
|
|
@ -26509,9 +26509,9 @@
|
|||
}
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.28",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz",
|
||||
"integrity": "sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g=="
|
||||
"version": "0.7.33",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
|
||||
"integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.4.10",
|
||||
|
|
|
@ -9,7 +9,7 @@ This demos the use of the new [Fluent UI version 9](https://github.com/microsoft
|
|||
|
||||
## Compatibility
|
||||
|
||||
![SPFx 1.16.0](https://img.shields.io/badge/SPFx-1.16-green.svg)
|
||||
![SPFx 1.16.1](https://img.shields.io/badge/SPFx-1.16.1-green.svg)
|
||||
![Node.js v16](https://img.shields.io/badge/Node.js-v16-green.svg)
|
||||
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
|
||||
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
|
||||
|
@ -46,7 +46,7 @@ Version|Date|Comments
|
|||
-------|----|--------
|
||||
1.0|April 20, 2022|Initial release
|
||||
1.0.1|November 14, 2022|Updated to SPFx 15, latest Fluent UI 9, shim based theme mapping
|
||||
1.0.2|November 16, 2022|
|
||||
1.0.2|January 18, 2023|Updated SPFx 16.1
|
||||
|
||||
- Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-fluentui-9) then unzip it)
|
||||
- From your command-line, change your current directory to the directory containing this sample (`react-fluentui-9`, located under `samples`)
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"description"
|
||||
],
|
||||
"creationDateTime": "2022-04-20",
|
||||
"updateDateTime": "2022-11-16",
|
||||
"updateDateTime": "2023-01-18",
|
||||
"products": [
|
||||
"SharePoint"
|
||||
],
|
||||
|
@ -21,7 +21,7 @@
|
|||
},
|
||||
{
|
||||
"key": "SPFX-VERSION",
|
||||
"value": "1.16"
|
||||
"value": "1.16.1"
|
||||
}
|
||||
],
|
||||
"thumbnails": [
|
||||
|
|
|
@ -12,22 +12,22 @@
|
|||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluentui/react-components": "^9.7.0",
|
||||
"@microsoft/sp-core-library": "1.16.0",
|
||||
"@microsoft/sp-lodash-subset": "1.16.0",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.16.0",
|
||||
"@microsoft/sp-property-pane": "1.16.0",
|
||||
"@microsoft/sp-webpart-base": "1.16.0",
|
||||
"@fluentui/react-components": "^9.10.1",
|
||||
"@microsoft/sp-core-library": "1.16.1",
|
||||
"@microsoft/sp-lodash-subset": "1.16.1",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.16.1",
|
||||
"@microsoft/sp-property-pane": "1.16.1",
|
||||
"@microsoft/sp-webpart-base": "1.16.1",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"tslib": "2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/eslint-config-spfx": "1.16.0",
|
||||
"@microsoft/eslint-plugin-spfx": "1.16.0",
|
||||
"@microsoft/eslint-config-spfx": "1.16.1",
|
||||
"@microsoft/eslint-plugin-spfx": "1.16.1",
|
||||
"@microsoft/rush-stack-compiler-4.5": "0.2.2",
|
||||
"@microsoft/sp-build-web": "1.16.0",
|
||||
"@microsoft/sp-module-interfaces": "1.16.0",
|
||||
"@microsoft/sp-build-web": "1.16.1",
|
||||
"@microsoft/sp-module-interfaces": "1.16.1",
|
||||
"@rushstack/eslint-config": "2.5.1",
|
||||
"@types/react": "17.0.45",
|
||||
"@types/react-dom": "17.0.17",
|
||||
|
|
|
@ -154,6 +154,8 @@ const mapAliasColors = (palette: Partial<IPalette>, inverted: boolean): ColorTok
|
|||
colorNeutralShadowKeyDarker: 'rgba(0,0,0,0.24)',
|
||||
colorBrandShadowAmbient: 'rgba(0,0,0,0.30)',
|
||||
colorBrandShadowKey: 'rgba(0,0,0,0.25)',
|
||||
colorNeutralStencil1Alpha: webLightTheme.colorNeutralStencil1Alpha,
|
||||
colorNeutralStencil2Alpha: webLightTheme.colorNeutralStencil2Alpha,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"@microsoft/generator-sharepoint": {
|
||||
"plusBeta": false,
|
||||
"isCreatingSolution": true,
|
||||
"version": "1.15.2",
|
||||
"version": "1.16.1",
|
||||
"libraryName": "group-membership-manager",
|
||||
"libraryId": "2a487e9a-b62a-484a-9bc0-e78b65550b61",
|
||||
"environment": "spo",
|
||||
|
@ -11,6 +11,9 @@
|
|||
"solutionShortDescription": "Group Membership Manager description",
|
||||
"skipFeatureDeployment": true,
|
||||
"isDomainIsolated": false,
|
||||
"componentType": "webpart"
|
||||
"componentType": "webpart",
|
||||
"sdkVersions": {
|
||||
"@microsoft/teams-js": "2.4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ This app is an example of managing the membership of a group you own including t
|
|||
|
||||
## Compatibility
|
||||
|
||||
![SPFx 1.15.2](https://img.shields.io/badge/SPFx-1.15.2-green.svg)
|
||||
![SPFx 1.16.1](https://img.shields.io/badge/SPFx-1.16.1-green.svg)
|
||||
![Node.js v16](https://img.shields.io/badge/Node.js-v16-green.svg)
|
||||
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
|
||||
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
|
||||
|
@ -35,6 +35,7 @@ This app is an example of managing the membership of a group you own including t
|
|||
| Version | Date | Comments |
|
||||
| ------- | ---------------- | --------------- |
|
||||
| 1.0 | August 25, 2022 | Initial release |
|
||||
| 1.1| | Jan 18, 2022 | Updated to SPFx 1.16.1 |
|
||||
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"This app is an example of managing the membership of a group you own including the owners of the group as well as using FluentUI v9"
|
||||
],
|
||||
"creationDateTime": "2022-08-25",
|
||||
"updateDateTime": "2022-08-25",
|
||||
"updateDateTime": "2023-01-18",
|
||||
"products": [
|
||||
"SharePoint"
|
||||
],
|
||||
|
@ -21,7 +21,7 @@
|
|||
},
|
||||
{
|
||||
"key": "SPFX-VERSION",
|
||||
"value": "1.14"
|
||||
"value": "1.16.1"
|
||||
}
|
||||
],
|
||||
"thumbnails": [
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"solution": {
|
||||
"name": "group-membership-manager-client-side-solution",
|
||||
"id": "2a487e9a-b62a-484a-9bc0-e78b65550b61",
|
||||
"version": "1.0.0.0",
|
||||
"version": "1.1.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"title": "Group Membership Manager",
|
||||
"skipFeatureDeployment": true,
|
||||
|
@ -13,7 +13,7 @@
|
|||
"websiteUrl": "https://nbdev.uk",
|
||||
"privacyUrl": "",
|
||||
"termsOfUseUrl": "",
|
||||
"mpnId": "Undefined-1.15.2"
|
||||
"mpnId": "Undefined-1.16.1"
|
||||
},
|
||||
"metadata": {
|
||||
"shortDescription": {
|
||||
|
|
|
@ -3,32 +3,36 @@
|
|||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"engines": {
|
||||
"node": ">=16.13.0 <17.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluentui/react-components": "^9.3.1",
|
||||
"@microsoft/sp-core-library": "1.15.2",
|
||||
"@microsoft/sp-lodash-subset": "1.15.2",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.15.2",
|
||||
"@microsoft/sp-property-pane": "1.15.2",
|
||||
"@microsoft/sp-webpart-base": "1.15.2",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"@fluentui/react-components": "^9.10.1",
|
||||
"@microsoft/sp-component-base": "^1.16.1",
|
||||
"@microsoft/sp-core-library": "1.16.1",
|
||||
"@microsoft/sp-lodash-subset": "1.16.1",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.16.1",
|
||||
"@microsoft/sp-property-pane": "1.16.1",
|
||||
"@microsoft/sp-webpart-base": "1.16.1",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"tslib": "2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/eslint-config-spfx": "1.15.2",
|
||||
"@microsoft/eslint-plugin-spfx": "1.15.2",
|
||||
"@microsoft/microsoft-graph-types": "^2.24.0",
|
||||
"@microsoft/eslint-config-spfx": "1.16.1",
|
||||
"@microsoft/eslint-plugin-spfx": "1.16.1",
|
||||
"@microsoft/microsoft-graph-types": "^2.25.0",
|
||||
"@microsoft/rush-stack-compiler-4.5": "0.2.2",
|
||||
"@microsoft/sp-build-web": "1.15.2",
|
||||
"@microsoft/sp-module-interfaces": "1.15.2",
|
||||
"@microsoft/sp-build-web": "1.16.1",
|
||||
"@microsoft/sp-module-interfaces": "1.16.1",
|
||||
"@rushstack/eslint-config": "2.5.1",
|
||||
"@types/react": "16.9.51",
|
||||
"@types/react-dom": "16.9.8",
|
||||
"@types/react": "17.0.45",
|
||||
"@types/react-dom": "17.0.17",
|
||||
"@types/webpack-env": "~1.15.2",
|
||||
"ajv": "^6.12.5",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
import { IPalette, ITheme as ThemeV8 } from '@microsoft/sp-component-base';
|
||||
|
||||
import { ColorTokens, Theme as ThemeV9, webLightTheme } from '@fluentui/react-components';
|
||||
import { blackAlpha, whiteAlpha, grey } from './themeDuplicates';
|
||||
|
||||
/**
|
||||
* Creates v9 color tokens from a v8 palette.
|
||||
* https://github.com/microsoft/fluentui/blob/master/apps/public-docsite-v9/src/shims/ThemeShim/v9ThemeShim.ts
|
||||
*/
|
||||
const mapAliasColors = (palette: Partial<IPalette>, inverted: boolean): ColorTokens => {
|
||||
return {
|
||||
colorNeutralForeground1: palette.neutralPrimary || webLightTheme.colorNeutralForeground1,
|
||||
colorNeutralForeground1Hover: palette.neutralPrimary || webLightTheme.colorNeutralForeground1Hover,
|
||||
colorNeutralForeground1Pressed: palette.neutralPrimary || webLightTheme.colorNeutralForeground1Pressed,
|
||||
colorNeutralForeground1Selected: palette.neutralPrimary || webLightTheme.colorNeutralForeground1Selected,
|
||||
colorNeutralForeground2: palette.neutralSecondary || webLightTheme.colorNeutralForeground2,
|
||||
colorNeutralForeground2Hover: palette.neutralPrimary || webLightTheme.colorNeutralForeground2Hover,
|
||||
colorNeutralForeground2Pressed: palette.neutralPrimary || webLightTheme.colorNeutralForeground2Pressed,
|
||||
colorNeutralForeground2Selected: palette.neutralPrimary || webLightTheme.colorNeutralForeground2Selected,
|
||||
colorNeutralForeground2BrandHover: palette.themePrimary || webLightTheme.colorNeutralForeground2BrandHover,
|
||||
colorNeutralForeground2BrandPressed: palette.themeDarkAlt || webLightTheme.colorNeutralForeground2BrandPressed,
|
||||
colorNeutralForeground2BrandSelected: palette.themePrimary || webLightTheme.colorNeutralForeground2BrandSelected,
|
||||
colorNeutralForeground3: palette.neutralTertiary || webLightTheme.colorNeutralForeground3,
|
||||
colorNeutralForeground3Hover: palette.neutralSecondary || webLightTheme.colorNeutralForeground3Hover,
|
||||
colorNeutralForeground3Pressed: palette.neutralSecondary || webLightTheme.colorNeutralForeground3Pressed,
|
||||
colorNeutralForeground3Selected: palette.neutralSecondary || webLightTheme.colorNeutralForeground3Selected,
|
||||
colorNeutralForeground3BrandHover: palette.themePrimary || webLightTheme.colorNeutralForeground3BrandHover,
|
||||
colorNeutralForeground3BrandPressed: palette.themeDarkAlt || webLightTheme.colorNeutralForeground3BrandPressed,
|
||||
colorNeutralForeground3BrandSelected: palette.themePrimary || webLightTheme.colorNeutralForeground3BrandSelected,
|
||||
colorNeutralForeground4: palette.neutralQuaternary || webLightTheme.colorNeutralForeground4,
|
||||
colorNeutralForegroundDisabled: palette.neutralTertiaryAlt || webLightTheme.colorNeutralForegroundDisabled,
|
||||
colorNeutralForegroundInvertedDisabled: whiteAlpha[40],
|
||||
colorBrandForegroundLink: palette.themeDarkAlt || webLightTheme.colorBrandForegroundLink,
|
||||
colorBrandForegroundLinkHover: palette.themeDark || webLightTheme.colorBrandForegroundLinkHover,
|
||||
colorBrandForegroundLinkPressed: palette.themeDarker || webLightTheme.colorBrandForegroundLinkPressed,
|
||||
colorBrandForegroundLinkSelected: palette.themeDarkAlt || webLightTheme.colorBrandForegroundLinkSelected,
|
||||
colorNeutralForeground2Link: palette.neutralSecondary || webLightTheme.colorNeutralForeground2Link,
|
||||
colorNeutralForeground2LinkHover: palette.neutralPrimary || webLightTheme.colorNeutralForeground2LinkHover,
|
||||
colorNeutralForeground2LinkPressed: palette.neutralPrimary || webLightTheme.colorNeutralForeground2LinkPressed,
|
||||
colorNeutralForeground2LinkSelected: palette.neutralPrimary || webLightTheme.colorNeutralForeground2LinkSelected,
|
||||
colorCompoundBrandForeground1: palette.themePrimary || webLightTheme.colorCompoundBrandForeground1,
|
||||
colorCompoundBrandForeground1Hover: palette.themeDarkAlt || webLightTheme.colorCompoundBrandForeground1Hover,
|
||||
colorCompoundBrandForeground1Pressed: palette.themeDark || webLightTheme.colorCompoundBrandForeground1Pressed,
|
||||
colorBrandForeground1: palette.themePrimary || webLightTheme.colorBrandForeground1,
|
||||
colorBrandForeground2: palette.themeDarkAlt || webLightTheme.colorBrandForeground2,
|
||||
colorNeutralForeground1Static: palette.neutralPrimary || webLightTheme.colorNeutralForeground1Static,
|
||||
colorNeutralForegroundInverted: palette.white || webLightTheme.colorNeutralForegroundInverted,
|
||||
colorNeutralForegroundInvertedHover: palette.white || webLightTheme.colorNeutralForegroundInvertedHover,
|
||||
colorNeutralForegroundInvertedPressed: palette.white || webLightTheme.colorNeutralForegroundInvertedPressed,
|
||||
colorNeutralForegroundInvertedSelected: palette.white || webLightTheme.colorNeutralForegroundInvertedSelected,
|
||||
colorNeutralForegroundOnBrand: palette.white || webLightTheme.colorNeutralForegroundOnBrand,
|
||||
colorNeutralForegroundStaticInverted: palette.white || webLightTheme.colorNeutralForegroundStaticInverted,
|
||||
colorNeutralForegroundInvertedLink: palette.white || webLightTheme.colorNeutralForegroundInvertedLink,
|
||||
colorNeutralForegroundInvertedLinkHover: palette.white || webLightTheme.colorNeutralForegroundInvertedLinkHover,
|
||||
colorNeutralForegroundInvertedLinkPressed: palette.white || webLightTheme.colorNeutralForegroundInvertedLinkPressed,
|
||||
colorNeutralForegroundInvertedLinkSelected: palette.white || webLightTheme.colorNeutralForegroundInvertedLinkSelected,
|
||||
colorNeutralForegroundInverted2: palette.white || webLightTheme.colorNeutralForegroundInverted2,
|
||||
colorBrandForegroundInverted: palette.themeSecondary || webLightTheme.colorBrandForegroundInverted,
|
||||
colorBrandForegroundInvertedHover: palette.themeTertiary || webLightTheme.colorBrandForegroundInvertedHover,
|
||||
colorBrandForegroundInvertedPressed: palette.themeSecondary || webLightTheme.colorBrandForegroundInvertedPressed,
|
||||
colorBrandForegroundOnLight: palette.themePrimary || webLightTheme.colorBrandForegroundOnLight,
|
||||
colorBrandForegroundOnLightHover: palette.themeDarkAlt || webLightTheme.colorBrandForegroundOnLightHover,
|
||||
colorBrandForegroundOnLightPressed: palette.themeDark || webLightTheme.colorBrandForegroundOnLightPressed,
|
||||
colorBrandForegroundOnLightSelected: palette.themeDark || webLightTheme.colorBrandForegroundOnLightSelected,
|
||||
colorNeutralBackground1: palette.white || webLightTheme.colorNeutralBackground1,
|
||||
colorNeutralBackground1Hover: palette.neutralLighter || webLightTheme.colorNeutralBackground1Hover,
|
||||
colorNeutralBackground1Pressed: palette.neutralQuaternaryAlt || webLightTheme.colorNeutralBackground1Pressed,
|
||||
colorNeutralBackground1Selected: palette.neutralLight || webLightTheme.colorNeutralBackground1Selected,
|
||||
colorNeutralBackground2: palette.neutralLighterAlt || webLightTheme.colorNeutralBackground2,
|
||||
colorNeutralBackground2Hover: palette.neutralLighter || webLightTheme.colorNeutralBackground2Hover,
|
||||
colorNeutralBackground2Pressed: palette.neutralQuaternaryAlt || webLightTheme.colorNeutralBackground2Pressed,
|
||||
colorNeutralBackground2Selected: palette.neutralLight || webLightTheme.colorNeutralBackground2Selected,
|
||||
colorNeutralBackground3: palette.neutralLighter || webLightTheme.colorNeutralBackground3,
|
||||
colorNeutralBackground3Hover: palette.neutralLight || webLightTheme.colorNeutralBackground3Hover,
|
||||
colorNeutralBackground3Pressed: palette.neutralQuaternary || webLightTheme.colorNeutralBackground3Pressed,
|
||||
colorNeutralBackground3Selected: palette.neutralQuaternaryAlt || webLightTheme.colorNeutralBackground3Selected,
|
||||
colorNeutralBackground4: palette.neutralLighter || webLightTheme.colorNeutralBackground4,
|
||||
colorNeutralBackground4Hover: palette.neutralLighterAlt || webLightTheme.colorNeutralBackground4Hover,
|
||||
colorNeutralBackground4Pressed: palette.neutralLighter || webLightTheme.colorNeutralBackground4Pressed,
|
||||
colorNeutralBackground4Selected: palette.white || webLightTheme.colorNeutralBackground4Selected,
|
||||
colorNeutralBackground5: palette.neutralLight || webLightTheme.colorNeutralBackground5,
|
||||
colorNeutralBackground5Hover: palette.neutralLighter || webLightTheme.colorNeutralBackground5Hover,
|
||||
colorNeutralBackground5Pressed: palette.neutralLighter || webLightTheme.colorNeutralBackground5Pressed,
|
||||
colorNeutralBackground5Selected: palette.neutralLighterAlt || webLightTheme.colorNeutralBackground5Selected,
|
||||
colorNeutralBackground6: palette.neutralLight || webLightTheme.colorNeutralBackground6,
|
||||
colorNeutralBackgroundStatic: grey[20],
|
||||
colorNeutralBackgroundInverted: palette.neutralSecondary || webLightTheme.colorNeutralBackgroundInverted,
|
||||
colorSubtleBackground: 'transparent',
|
||||
colorSubtleBackgroundHover: palette.neutralLighter || webLightTheme.colorSubtleBackgroundHover,
|
||||
colorSubtleBackgroundPressed: palette.neutralQuaternaryAlt || webLightTheme.colorSubtleBackgroundPressed,
|
||||
colorSubtleBackgroundSelected: palette.neutralLight || webLightTheme.colorSubtleBackgroundSelected,
|
||||
colorSubtleBackgroundLightAlphaHover: inverted ? whiteAlpha[10] : whiteAlpha[80],
|
||||
colorSubtleBackgroundLightAlphaPressed: inverted ? whiteAlpha[5] : whiteAlpha[50],
|
||||
colorSubtleBackgroundLightAlphaSelected: 'transparent',
|
||||
colorSubtleBackgroundInverted: 'transparent',
|
||||
colorSubtleBackgroundInvertedHover: blackAlpha[10],
|
||||
colorSubtleBackgroundInvertedPressed: blackAlpha[30],
|
||||
colorSubtleBackgroundInvertedSelected: blackAlpha[20],
|
||||
colorTransparentBackground: 'transparent',
|
||||
colorTransparentBackgroundHover: 'transparent',
|
||||
colorTransparentBackgroundPressed: 'transparent',
|
||||
colorTransparentBackgroundSelected: 'transparent',
|
||||
colorNeutralBackgroundDisabled: palette.neutralLighter || webLightTheme.colorNeutralBackgroundDisabled,
|
||||
colorNeutralBackgroundInvertedDisabled: whiteAlpha[10],
|
||||
colorNeutralStencil1: palette.neutralLight || webLightTheme.colorNeutralStencil1,
|
||||
colorNeutralStencil2: palette.neutralLighterAlt || webLightTheme.colorNeutralStencil2,
|
||||
colorBackgroundOverlay: blackAlpha[10],
|
||||
colorScrollbarOverlay: blackAlpha[50],
|
||||
colorBrandBackground: palette.themePrimary || webLightTheme.colorBrandBackground,
|
||||
colorBrandBackgroundHover: palette.themeDarkAlt || webLightTheme.colorBrandBackgroundHover,
|
||||
colorBrandBackgroundPressed: palette.themeDarker || webLightTheme.colorBrandBackgroundPressed,
|
||||
colorBrandBackgroundSelected: palette.themeDark || webLightTheme.colorBrandBackgroundSelected,
|
||||
colorCompoundBrandBackground: palette.themePrimary || webLightTheme.colorCompoundBrandBackground,
|
||||
colorCompoundBrandBackgroundHover: palette.themeDarkAlt || webLightTheme.colorCompoundBrandBackgroundHover,
|
||||
colorCompoundBrandBackgroundPressed: palette.themeDark || webLightTheme.colorCompoundBrandBackgroundPressed,
|
||||
colorBrandBackgroundStatic: palette.themePrimary || webLightTheme.colorBrandBackgroundStatic,
|
||||
colorBrandBackground2: palette.themeLighterAlt || webLightTheme.colorBrandBackground2,
|
||||
colorBrandBackgroundInverted: palette.white || webLightTheme.colorBrandBackgroundInverted,
|
||||
colorBrandBackgroundInvertedHover: palette.themeLighterAlt || webLightTheme.colorBrandBackgroundInvertedHover,
|
||||
colorBrandBackgroundInvertedPressed: palette.themeLight || webLightTheme.colorBrandBackgroundInvertedPressed,
|
||||
colorBrandBackgroundInvertedSelected: palette.themeLighter || webLightTheme.colorBrandBackgroundInvertedSelected,
|
||||
colorNeutralStrokeAccessible: palette.neutralSecondary || webLightTheme.colorNeutralStrokeAccessible,
|
||||
colorNeutralStrokeAccessibleHover: palette.neutralSecondary || webLightTheme.colorNeutralStrokeAccessibleHover,
|
||||
colorNeutralStrokeAccessiblePressed: palette.neutralSecondary || webLightTheme.colorNeutralStrokeAccessiblePressed,
|
||||
colorNeutralStrokeAccessibleSelected: palette.themePrimary || webLightTheme.colorNeutralStrokeAccessibleSelected,
|
||||
colorNeutralStroke1: palette.neutralQuaternary || webLightTheme.colorNeutralStroke1,
|
||||
colorNeutralStroke1Hover: palette.neutralTertiaryAlt || webLightTheme.colorNeutralStroke1Hover,
|
||||
colorNeutralStroke1Pressed: palette.neutralTertiaryAlt || webLightTheme.colorNeutralStroke1Pressed,
|
||||
colorNeutralStroke1Selected: palette.neutralTertiaryAlt || webLightTheme.colorNeutralStroke1Selected,
|
||||
colorNeutralStroke2: palette.neutralQuaternaryAlt || webLightTheme.colorNeutralStroke2,
|
||||
colorNeutralStroke3: palette.neutralLighter || webLightTheme.colorNeutralStroke3,
|
||||
colorNeutralStrokeOnBrand: palette.white || webLightTheme.colorNeutralStrokeOnBrand,
|
||||
colorNeutralStrokeOnBrand2: palette.white || webLightTheme.colorNeutralStrokeOnBrand2,
|
||||
colorNeutralStrokeOnBrand2Hover: palette.white || webLightTheme.colorNeutralStrokeOnBrand2Hover,
|
||||
colorNeutralStrokeOnBrand2Pressed: palette.white || webLightTheme.colorNeutralStrokeOnBrand2Pressed,
|
||||
colorNeutralStrokeOnBrand2Selected: palette.white || webLightTheme.colorNeutralStrokeOnBrand2Selected,
|
||||
colorBrandStroke1: palette.themePrimary || webLightTheme.colorBrandStroke1,
|
||||
colorBrandStroke2: palette.themeLight || webLightTheme.colorBrandStroke2,
|
||||
colorCompoundBrandStroke: palette.themePrimary || webLightTheme.colorCompoundBrandStroke,
|
||||
colorCompoundBrandStrokeHover: palette.themeDarkAlt || webLightTheme.colorCompoundBrandStrokeHover,
|
||||
colorCompoundBrandStrokePressed: palette.themeDark || webLightTheme.colorCompoundBrandStrokePressed,
|
||||
colorNeutralStrokeDisabled: palette.neutralQuaternaryAlt || webLightTheme.colorNeutralStrokeDisabled,
|
||||
colorNeutralStrokeInvertedDisabled: whiteAlpha[40],
|
||||
colorTransparentStroke: 'transparent',
|
||||
colorTransparentStrokeInteractive: 'transparent',
|
||||
colorTransparentStrokeDisabled: 'transparent',
|
||||
colorStrokeFocus1: palette.white || webLightTheme.colorStrokeFocus1,
|
||||
colorStrokeFocus2: palette.black || webLightTheme.colorStrokeFocus2,
|
||||
colorNeutralShadowAmbient: 'rgba(0,0,0,0.12)',
|
||||
colorNeutralShadowKey: 'rgba(0,0,0,0.14)',
|
||||
colorNeutralShadowAmbientLighter: 'rgba(0,0,0,0.06)',
|
||||
colorNeutralShadowKeyLighter: 'rgba(0,0,0,0.07)',
|
||||
colorNeutralShadowAmbientDarker: 'rgba(0,0,0,0.20)',
|
||||
colorNeutralShadowKeyDarker: 'rgba(0,0,0,0.24)',
|
||||
colorBrandShadowAmbient: 'rgba(0,0,0,0.30)',
|
||||
colorBrandShadowKey: 'rgba(0,0,0,0.25)',
|
||||
colorNeutralStencil1Alpha: webLightTheme.colorNeutralStencil1Alpha,
|
||||
colorNeutralStencil2Alpha: webLightTheme.colorNeutralStencil2Alpha,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a v9 theme from a v8 theme.
|
||||
* You can optional pass a base v9 theme; otherwise webLightTheme is used.
|
||||
*/
|
||||
export const createv9Theme = (themeV8: ThemeV8, baseThemeV9?: ThemeV9): ThemeV9 => {
|
||||
const baseTheme = baseThemeV9 ?? webLightTheme;
|
||||
|
||||
return {
|
||||
...baseTheme,
|
||||
...mapAliasColors(themeV8.palette, themeV8.isInverted)
|
||||
};
|
||||
};
|
|
@ -1,15 +1,15 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import { IPropertyPaneConfiguration } from '@microsoft/sp-property-pane';
|
||||
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
import GroupMembershipManager from './components/GroupMembershipManager';
|
||||
import { IGroupMembershipManagerProps } from './components/IGroupMembershipManagerProps';
|
||||
import { FluentProvider, FluentProviderProps, teamsDarkTheme, teamsLightTheme, webLightTheme, webDarkTheme, Theme } from '@fluentui/react-components';
|
||||
import { createv9Theme } from '../../shims/v9ThemeShim';
|
||||
|
||||
export enum AppMode {
|
||||
SharePoint, SharePointLocal, Teams, TeamsLocal
|
||||
SharePoint, Teams, Office, Outlook
|
||||
}
|
||||
|
||||
export default class GroupMembershipManagerWebPart extends BaseClientSideWebPart<{}> {
|
||||
|
@ -17,9 +17,16 @@ export default class GroupMembershipManagerWebPart extends BaseClientSideWebPart
|
|||
private _appMode: AppMode = AppMode.SharePoint;
|
||||
private _theme: Theme = webLightTheme;
|
||||
|
||||
protected onInit(): Promise<void> {
|
||||
//on initalizational set the App Mode
|
||||
this._appMode = !!this.context.sdks.microsoftTeams ? (this.context.isServedFromLocalhost ? AppMode.TeamsLocal : AppMode.Teams) : (this.context.isServedFromLocalhost) ? AppMode.SharePointLocal : AppMode.SharePoint;
|
||||
protected async onInit(): Promise<void> {
|
||||
if (!!this.context.sdks.microsoftTeams) {
|
||||
const teamsContext = await this.context.sdks.microsoftTeams.teamsJs.app.getContext();
|
||||
switch (teamsContext.app.host.name.toLowerCase()) {
|
||||
case 'teams': this._appMode = AppMode.Teams; break;
|
||||
case 'office': this._appMode = AppMode.Office; break;
|
||||
case 'outlook': this._appMode = AppMode.Outlook; break;
|
||||
default: throw new Error('Unknown host');
|
||||
}
|
||||
} else this._appMode = AppMode.SharePoint;
|
||||
return super.onInit();
|
||||
}
|
||||
|
||||
|
@ -37,9 +44,11 @@ export default class GroupMembershipManagerWebPart extends BaseClientSideWebPart
|
|||
const fluentElement: React.ReactElement<FluentProviderProps> = React.createElement(
|
||||
FluentProvider,
|
||||
{
|
||||
theme : this._appMode === AppMode.Teams || this._appMode === AppMode.TeamsLocal ?
|
||||
theme: this._appMode === AppMode.Teams ?
|
||||
this._isDarkTheme ? teamsDarkTheme : teamsLightTheme :
|
||||
this._isDarkTheme ? webDarkTheme : this._theme
|
||||
this._appMode === AppMode.SharePoint ?
|
||||
this._isDarkTheme ? webDarkTheme : this._theme :
|
||||
this._isDarkTheme ? webDarkTheme : webLightTheme
|
||||
},
|
||||
element
|
||||
);
|
||||
|
@ -50,30 +59,9 @@ export default class GroupMembershipManagerWebPart extends BaseClientSideWebPart
|
|||
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
|
||||
if (!currentTheme) return;
|
||||
this._isDarkTheme = !!currentTheme.isInverted;
|
||||
|
||||
//if the app mode is sharepoint, adjust the fluent ui 9 web light theme to use the sharepoint theme color, teams/dark mode should be fine on default
|
||||
if (this._appMode === AppMode.SharePoint || this._appMode === AppMode.SharePointLocal) {
|
||||
this._theme = {...webLightTheme,
|
||||
colorBrandBackground: currentTheme.palette.themePrimary,
|
||||
colorBrandBackgroundHover: currentTheme.palette.themeDark,
|
||||
colorBrandBackgroundPressed: currentTheme.palette.themeDarker,
|
||||
colorCompoundBrandForeground1: currentTheme.palette.themePrimary,
|
||||
colorCompoundBrandForeground1Hover: currentTheme.palette.themeDark,
|
||||
colorCompoundBrandForeground1Pressed: currentTheme.palette.themeDarker,
|
||||
colorNeutralForeground2BrandHover: currentTheme.palette.themeSecondary,
|
||||
colorNeutralForeground2BrandPressed: currentTheme.palette.themeDarkAlt,
|
||||
colorNeutralForeground2BrandSelected: currentTheme.palette.themeDarkAlt,
|
||||
colorBrandForeground1: currentTheme.palette.themeSecondary,
|
||||
colorBrandStroke1: currentTheme.palette.themePrimary,
|
||||
colorBrandStroke2: currentTheme.palette.themeSecondary,
|
||||
colorCompoundBrandStroke: currentTheme.palette.themePrimary,
|
||||
colorCompoundBrandStrokeHover: currentTheme.palette.themeSecondary,
|
||||
colorCompoundBrandStrokePressed: currentTheme.palette.themeDarkAlt,
|
||||
colorCompoundBrandBackground: currentTheme.palette.themePrimary,
|
||||
colorCompoundBrandBackgroundHover: currentTheme.palette.themeDark,
|
||||
colorCompoundBrandBackgroundPressed: currentTheme.palette.themeDarker,
|
||||
|
||||
};
|
||||
if (this._appMode === AppMode.SharePoint) {
|
||||
this._theme = createv9Theme(currentTheme, webLightTheme);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,11 +72,4 @@ export default class GroupMembershipManagerWebPart extends BaseClientSideWebPart
|
|||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
return {
|
||||
pages: [
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,14 @@ import * as React from 'react';
|
|||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import styles from './GroupMembershipManager.module.scss';
|
||||
import * as strings from 'GroupMembershipManagerWebPartStrings'
|
||||
import { Dialog, DialogTrigger, DialogSurface, DialogTitle, DialogBody, DialogActions, Alert } from "@fluentui/react-components/unstable";
|
||||
import { Button, Checkbox, Divider, Input, Label, Spinner, useId } from "@fluentui/react-components";
|
||||
import { Alert } from "@fluentui/react-components/unstable";
|
||||
import { Dialog, DialogTrigger, DialogSurface, DialogTitle, DialogBody, DialogActions, Button, Checkbox, Divider, Input, Label, Spinner, useId, DialogContent } from "@fluentui/react-components";
|
||||
import { PersonAddRegular } from '@fluentui/react-icons';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { MSGraphClientV3 } from '@microsoft/sp-http';
|
||||
import { PersonaSize } from 'office-ui-fabric-react/lib/Persona';
|
||||
import SPFxPeopleCard from './SPFxPeopleCard';
|
||||
import { GraphError } from '@microsoft/microsoft-graph-client';
|
||||
|
||||
import { GraphError } from '@microsoft/microsoft-graph-clientv1';
|
||||
|
||||
export enum AddUserMode { Member, Owner }
|
||||
enum rState { Idle, Running, Error, Completed }
|
||||
|
@ -44,14 +43,13 @@ export default function AddUser({ Group, Mode, context, onCompleted }: Props): R
|
|||
"members@odata.bind": users.map(m => `https://graph.microsoft.com/v1.0/directoryObjects/${m.id}`)
|
||||
} : {
|
||||
"owners@odata.bind": users.map(m => `https://graph.microsoft.com/v1.0/directoryObjects/${m.id}`)
|
||||
}, (e: GraphError, response ) => {
|
||||
}, (e: GraphError) => {
|
||||
if (e) throw e.message;
|
||||
else setRunning(rState.Completed)
|
||||
}).catch(handleError);
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
handleError(error);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -76,14 +74,13 @@ export default function AddUser({ Group, Mode, context, onCompleted }: Props): R
|
|||
|
||||
const inputId = useId('input-with-placeholder');
|
||||
|
||||
return (
|
||||
return (<>
|
||||
<Button appearance='primary' icon={<PersonAddRegular />} onClick={() => setOpen(true)}>{strings.Add}</Button>
|
||||
<Dialog open={open} onOpenChange={(event, data) => setOpen(data.open)}>
|
||||
<DialogTrigger>
|
||||
<Button appearance='primary' icon={<PersonAddRegular />}>{strings.Add}</Button>
|
||||
</DialogTrigger>
|
||||
<DialogSurface aria-label="label">
|
||||
<DialogTitle>{Mode === AddUserMode.Owner ? strings.AddDialogTitleOwner : strings.AddDialogTitle}{Group.displayName}</DialogTitle>
|
||||
<DialogSurface>
|
||||
<DialogBody>
|
||||
<DialogTitle>{Mode === AddUserMode.Owner ? strings.AddDialogTitleOwner : strings.AddDialogTitle}{Group.displayName}</DialogTitle>
|
||||
<DialogContent>
|
||||
<div className={styles.stack}>
|
||||
{running !== rState.Error && _error && <Alert intent="error">{_error}</Alert>}
|
||||
{running === rState.Error && <Alert intent="error" action={{ children: 'Retry', onClick: () => setRunning(rState.Running) }}>{_error}</Alert>}
|
||||
|
@ -105,14 +102,15 @@ export default function AddUser({ Group, Mode, context, onCompleted }: Props): R
|
|||
</>}
|
||||
{running === rState.Running && <Spinner labelPosition='below' label={strings.Adding} />}
|
||||
</div>
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<DialogTrigger>
|
||||
<Button appearance="secondary" disabled={running === rState.Running}>{strings.Close}</Button>
|
||||
</DialogTrigger>
|
||||
{users.length > 0 && <Button appearance="primary" disabled={running === rState.Completed || running === rState.Running} onClick={add}>{strings.Add}</Button>}
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
)
|
||||
</>)
|
||||
}
|
|
@ -2,15 +2,15 @@ import * as React from 'react';
|
|||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import styles from './GroupMembershipManager.module.scss';
|
||||
import * as strings from 'GroupMembershipManagerWebPartStrings'
|
||||
import { Dialog, DialogTrigger, DialogSurface, DialogTitle, DialogBody, DialogActions, Alert } from "@fluentui/react-components/unstable";
|
||||
import { Button, Spinner } from "@fluentui/react-components";
|
||||
import { Alert } from "@fluentui/react-components/unstable";
|
||||
import { Dialog, DialogTrigger, DialogSurface, DialogTitle, DialogBody, DialogActions, Button, Spinner, DialogContent } from "@fluentui/react-components";
|
||||
import { AddUserMode } from './AddUser';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { PersonDeleteRegular } from '@fluentui/react-icons';
|
||||
import { MSGraphClientV3 } from '@microsoft/sp-http';
|
||||
import { PersonaSize } from 'office-ui-fabric-react/lib/Persona';
|
||||
import SPFxPeopleCard from './SPFxPeopleCard';
|
||||
import { BatchRequestStep, BatchRequestContent, BatchRequestBody, BatchResponseContent } from '@microsoft/microsoft-graph-client';
|
||||
import { BatchRequestStep, BatchRequestContent, BatchRequestBody, BatchResponseContent } from '@microsoft/microsoft-graph-clientv1';
|
||||
|
||||
enum rState { Idle, Running, Error, Completed }
|
||||
|
||||
|
@ -74,14 +74,14 @@ export default function RemoveUser({ Group, Users, Mode, context, onCompleted }:
|
|||
}, [running]);
|
||||
|
||||
|
||||
return (
|
||||
return (<>
|
||||
<Button appearance='primary' icon={<PersonDeleteRegular />} disabled={Users.length === 0} onClick={() => setOpen(true)}>{strings.Remove}</Button>
|
||||
<Dialog open={open} onOpenChange={(event, data) => setOpen(data.open)}>
|
||||
<DialogTrigger>
|
||||
<Button appearance='primary' icon={<PersonDeleteRegular />} disabled={Users.length === 0}>{strings.Remove}</Button>
|
||||
</DialogTrigger>
|
||||
<DialogSurface aria-label="label">
|
||||
<DialogTitle>{Mode === AddUserMode.Owner ? strings.RemoveDialogTitleOwner : strings.RemoveDialogTitle}{Group.displayName}</DialogTitle>
|
||||
<DialogBody>
|
||||
<DialogTitle>{Mode === AddUserMode.Owner ? strings.RemoveDialogTitleOwner : strings.RemoveDialogTitle}{Group.displayName}</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<div className={styles.stack}>
|
||||
{running !== rState.Error && _error && <Alert intent="error">{_error.split('||').map((v, i) => (<div key={i}>{v}</div>))}</Alert>}
|
||||
{running === rState.Error && _error && <Alert intent="error" action={{ children: 'Retry', onClick: () => setRunning(rState.Running) }}>{_error.split('||').map((v, i) => (<div key={i}>{v}</div>))}</Alert>}
|
||||
|
@ -93,14 +93,15 @@ export default function RemoveUser({ Group, Users, Mode, context, onCompleted }:
|
|||
</div>}
|
||||
{running === rState.Running && <Spinner labelPosition='below' label={strings.Removing} />}
|
||||
</div>
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<DialogTrigger>
|
||||
<Button appearance="secondary" disabled={running === rState.Running}>{strings.Close}</Button>
|
||||
</DialogTrigger>
|
||||
<Button appearance="primary" disabled={running === rState.Completed || running === rState.Running} onClick={remove}>{strings.Remove}</Button>
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
)
|
||||
</>)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
@import '~@fluentui/react/dist/sass/References.scss';
|
||||
|
||||
.groupMembershipManager {
|
||||
overflow: hidden;
|
||||
|
|
|
@ -4,15 +4,13 @@ import { IGroupMembershipManagerProps } from './IGroupMembershipManagerProps';
|
|||
import * as strings from 'GroupMembershipManagerWebPartStrings';
|
||||
import { MSGraphClientV3 } from '@microsoft/sp-http';
|
||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import { Checkbox, CheckboxOnChangeData, Spinner, Subtitle1, Textarea, Tooltip } from '@fluentui/react-components';
|
||||
import { Spinner, Subtitle1, Textarea, Tooltip } from '@fluentui/react-components';
|
||||
import { Dropdown, Option, Toolbar } from "@fluentui/react-components/unstable";
|
||||
import { TableBody, TableCell, TableRow, Table } from '@fluentui/react-table';
|
||||
import AddUser, { AddUserMode } from './AddUser';
|
||||
import RemoveUser from './DeleteUser';
|
||||
import { GraphError } from '@microsoft/microsoft-graph-client';
|
||||
import SPFxPeopleCard from './SPFxPeopleCard';
|
||||
import { PersonaSize } from 'office-ui-fabric-react/lib/Persona';
|
||||
import { GraphError } from '@microsoft/microsoft-graph-clientv1';
|
||||
import { PeopleTeam24Regular, MailLink24Regular, ShieldLock24Regular, Eye24Regular, EyeOff24Regular } from '@fluentui/react-icons'
|
||||
import TableGrid from './Table';
|
||||
|
||||
type OnSelectData = {
|
||||
optionValue: string;
|
||||
|
@ -34,7 +32,7 @@ export default function GroupMembershipManager(props: IGroupMembershipManagerPro
|
|||
|
||||
React.useEffect(() => {
|
||||
context.msGraphClientFactory.getClient("3").then((client: MSGraphClientV3) => {
|
||||
client.api('/me/ownedObjects').select("id,displayName,groupTypes,visibility,mailEnabled,resourceProvisioningOptions,securityEnabled,description").get((error, response: { value: MicrosoftGraph.Group[] }) => {
|
||||
client.api('/me/ownedObjects').select("id,displayName,groupTypes,visibility,mailEnabled,resourceProvisioningOptions,securityEnabled,description").get((error: undefined, response: { value: MicrosoftGraph.Group[] }) => {
|
||||
if (error) throw error;
|
||||
setGroups(response.value.filter(g => g.groupTypes).sort((a, b) => a.displayName.localeCompare(b.displayName)));
|
||||
}).catch(console.error);
|
||||
|
@ -61,22 +59,14 @@ export default function GroupMembershipManager(props: IGroupMembershipManagerPro
|
|||
if (group) loadGroup().catch(console.error);
|
||||
}, [group]);
|
||||
|
||||
const checkUser = (data: CheckboxOnChangeData, user: MicrosoftGraph.User): void => {
|
||||
setToRemove(data.checked ? toRemove.concat([user]) : toRemove.filter(u => u.id !== user.id));
|
||||
};
|
||||
|
||||
const checkOwner = (data: CheckboxOnChangeData, user: MicrosoftGraph.User): void => {
|
||||
setRemoveOwner(data.checked ? removeOwner.concat([user]) : removeOwner.filter(u => u.id !== user.id));
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={`${styles.groupMembershipManager} ${hasTeamsContext ? styles.teams : ''}`}>
|
||||
{!groups && <Spinner labelPosition='below' label={strings.LoadingGroups} />}
|
||||
{groups && <section>
|
||||
<Subtitle1 block>{strings.PickGroup}</Subtitle1>
|
||||
<div className={styles.stackHoz} style={{ alignItems: 'center' }}>
|
||||
<Dropdown placeholder={strings.PickGroup} size="large" onSelect={(e, d?: OnSelectData) => setGroup(d ? d.optionValue : null)}>
|
||||
{groups.map(group => <Option id={group.id} key={group.id}>{group.displayName}</Option>)}
|
||||
<Dropdown placeholder={strings.PickGroup} size="large" onOptionSelect={(e, d?: OnSelectData) => { setGroup(d?.optionValue); }}>
|
||||
{groups.map(group => <Option key={group.mailNickname}>{group.displayName}</Option>)}
|
||||
</Dropdown>
|
||||
{group && groups.filter(g => g.displayName === group)[0].resourceProvisioningOptions?.filter(g => g === "Team").length === 1 &&
|
||||
<Tooltip relationship='label' content="Team"><span><PeopleTeam24Regular /></span></Tooltip>
|
||||
|
@ -108,19 +98,7 @@ export default function GroupMembershipManager(props: IGroupMembershipManagerPro
|
|||
<AddUser context={context} Group={groups.filter(g => g.displayName === group)[0]} Mode={AddUserMode.Member} onCompleted={loadGroup} />
|
||||
<RemoveUser Users={toRemove} context={context} Group={groups.filter(g => g.displayName === group)[0]} Mode={AddUserMode.Member} onCompleted={loadGroup} />
|
||||
</Toolbar>}
|
||||
<Table>
|
||||
<TableBody>
|
||||
{members.map(user => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
{!(groups.filter(g => g.displayName === group)[0].groupTypes?.filter(g => g === "DynamicMembership").length > 0) &&
|
||||
<Checkbox onChange={(ev, data) => checkUser(data, user)} />}
|
||||
<SPFxPeopleCard primaryText={user.displayName} serviceScope={context.serviceScope} email={user.userPrincipalName} size={PersonaSize.size24} secondaryText={user.mail} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TableGrid Users={members} ServiceScope={context.serviceScope} onSelectionChange={setToRemove} />
|
||||
</>}
|
||||
</div>
|
||||
<div style={{ width: '49%' }}>
|
||||
|
@ -131,19 +109,7 @@ export default function GroupMembershipManager(props: IGroupMembershipManagerPro
|
|||
<AddUser context={context} Group={groups.filter(g => g.displayName === group)[0]} Mode={AddUserMode.Owner} onCompleted={loadGroup} />
|
||||
<RemoveUser Users={removeOwner} context={context} Group={groups.filter(g => g.displayName === group)[0]} Mode={AddUserMode.Owner} onCompleted={loadGroup} />
|
||||
</Toolbar>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{owners.map(user => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
{!(groups.filter(g => g.displayName === group)[0].groupTypes?.filter(g => g === "DynamicMembership").length > 0) &&
|
||||
<Checkbox onChange={(ev, data) => checkOwner(data, user)} />}
|
||||
<SPFxPeopleCard primaryText={user.displayName} serviceScope={context.serviceScope} email={user.userPrincipalName} size={PersonaSize.size24} secondaryText={user.mail} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TableGrid Users={owners} ServiceScope={context.serviceScope} onSelectionChange={setRemoveOwner} ReadOnly={groups.filter(g => g.displayName === group)[0].groupTypes?.filter(g => g === "DynamicMembership").length > 0} />
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import { TableColumnDefinition, createTableColumn, TableRowId, useTableFeatures, useTableSelection, TableHeader, TableRow, Table, TableSelectionCell, TableHeaderCell, TableBody, TableCell, TableCellLayout } from "@fluentui/react-components/unstable";
|
||||
import * as React from 'react';
|
||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import SPFxPeopleCard from "./SPFxPeopleCard";
|
||||
import { ServiceScope } from "@microsoft/sp-core-library";
|
||||
import { PersonaSize } from "office-ui-fabric-react/lib/Persona";
|
||||
|
||||
export interface TableGridProps {
|
||||
Users: MicrosoftGraph.User[];
|
||||
ServiceScope: ServiceScope;
|
||||
ReadOnly?: boolean;
|
||||
onSelectionChange?: (SelectedUsers: MicrosoftGraph.User[]) => void;
|
||||
}
|
||||
|
||||
export default function TableGrid(props: TableGridProps): React.ReactElement<TableGridProps> {
|
||||
const columns: TableColumnDefinition<MicrosoftGraph.User>[] = React.useMemo(
|
||||
() => [
|
||||
createTableColumn<MicrosoftGraph.User>({
|
||||
columnId: 'username',
|
||||
})
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const [selectedRows, setSelectedRows] = React.useState(() => new Set<TableRowId>([]));
|
||||
|
||||
const {
|
||||
getRows,
|
||||
selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected },
|
||||
} = useTableFeatures(
|
||||
{
|
||||
columns,
|
||||
items: props.Users,
|
||||
},
|
||||
[
|
||||
useTableSelection({
|
||||
selectionMode: 'multiselect',
|
||||
selectedItems: selectedRows,
|
||||
onSelectionChange: (e, data) => {
|
||||
if (props.ReadOnly) return;
|
||||
if (props.onSelectionChange) props.onSelectionChange(props.Users.filter((v, i) => data.selectedItems.has(i)));
|
||||
setSelectedRows(data.selectedItems);
|
||||
}
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
const rows = getRows(row => {
|
||||
const selected = isRowSelected(row.rowId);
|
||||
return {
|
||||
...row,
|
||||
onClick: (e: React.MouseEvent) => { if (!props.ReadOnly) toggleRow(e, row.rowId) },
|
||||
onKeyDown: (e: React.KeyboardEvent) => {
|
||||
if (e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (!props.ReadOnly) toggleRow(e, row.rowId);
|
||||
}
|
||||
},
|
||||
selected,
|
||||
appearance: selected ? ('brand' as const) : ('none' as const),
|
||||
};
|
||||
});
|
||||
|
||||
const toggleAllKeydown = React.useCallback(
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === ' ') {
|
||||
toggleAllRows(e);
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
[toggleAllRows],
|
||||
);
|
||||
|
||||
|
||||
return (<Table aria-label="Table with multiselect">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{!props.ReadOnly && <TableSelectionCell
|
||||
checked={allRowsSelected ? true : someRowsSelected ? 'mixed' : false}
|
||||
onClick={toggleAllRows}
|
||||
onKeyDown={toggleAllKeydown}
|
||||
checkboxIndicator={{ 'aria-label': 'Select all rows ' }}
|
||||
/>}
|
||||
<TableHeaderCell>User</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map(({ item, selected, onClick, onKeyDown, appearance }) => (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
aria-selected={selected}
|
||||
appearance={appearance}
|
||||
>
|
||||
{!props.ReadOnly && <TableSelectionCell checked={selected} checkboxIndicator={{ 'aria-label': 'Select row' }} />}
|
||||
<TableCell>
|
||||
<TableCellLayout>
|
||||
<SPFxPeopleCard primaryText={item.displayName} serviceScope={props.ServiceScope} email={item.userPrincipalName} size={PersonaSize.size24} secondaryText={item.mail} />
|
||||
</TableCellLayout>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>);
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/teams/v1.14/MicrosoftTeams.schema.json",
|
||||
"manifestVersion": "1.14",
|
||||
"packageName": "react-group-membership-manager-client-side-solution",
|
||||
"id": "698a4f5b-686f-475d-81fb-9bcf98850b96",
|
||||
"version": "1.1.0",
|
||||
"developer": {
|
||||
"name": "Nick Brown",
|
||||
"websiteUrl": "https://nbdev.uk",
|
||||
"privacyUrl": "https://nbdev.uk",
|
||||
"termsOfUseUrl": "https://nbdev.uk"
|
||||
},
|
||||
"name": {
|
||||
"short": "Group Membership Manager",
|
||||
"full": "React Group Membership Manager"
|
||||
},
|
||||
"description": {
|
||||
"short": "Group Membership Manager",
|
||||
"full": "React Group Membership Manager using Fluent UI 9"
|
||||
},
|
||||
"icons": {
|
||||
"outline": "698a4f5b-686f-475d-81fb-9bcf98850b96_outline.png",
|
||||
"color": "698a4f5b-686f-475d-81fb-9bcf98850b96_color.png"
|
||||
},
|
||||
"accentColor": "#004578",
|
||||
"staticTabs": [
|
||||
{
|
||||
"contentUrl": "https://{teamSiteDomain}/_layouts/15/TeamsLogon.aspx?SPFX=true&dest=/_layouts/15/teamshostedapp.aspx%3Fteams%26personal%26componentId=698a4f5b-686f-475d-81fb-9bcf98850b96%26forceLocale={locale}",
|
||||
"name": "Group Membership Manager",
|
||||
"scopes": ["personal"],
|
||||
"entityId": "100001"
|
||||
}
|
||||
],
|
||||
"configurableTabs": [
|
||||
{
|
||||
"configurationUrl": "https://{teamSiteDomain}{teamSitePath}/_layouts/15/TeamsLogon.aspx?SPFX=true&dest={teamSitePath}/_layouts/15/teamshostedapp.aspx%3FopenPropertyPane=true%26teams%26componentId=698a4f5b-686f-475d-81fb-9bcf98850b96%26forceLocale={locale}",
|
||||
"canUpdateConfiguration": false,
|
||||
"scopes": [
|
||||
"team",
|
||||
"groupchat"
|
||||
],
|
||||
"context": [
|
||||
"channelTab",
|
||||
"privateChatTab",
|
||||
"meetingSidePanel",
|
||||
"meetingDetailsTab",
|
||||
"meetingChatTab"
|
||||
]
|
||||
}
|
||||
],
|
||||
"permissions": [
|
||||
"identity"
|
||||
],
|
||||
"validDomains": [
|
||||
"*.login.microsoftonline.com",
|
||||
"*.sharepoint.com",
|
||||
"resourceseng.blob.core.windows.net"
|
||||
],
|
||||
"webApplicationInfo": {
|
||||
"resource": "https://{teamSiteDomain}",
|
||||
"id": "00000003-0000-0ff1-ce00-000000000000"
|
||||
}
|
||||
}
|
|
@ -13,9 +13,7 @@
|
|||
"outDir": "lib",
|
||||
"inlineSources": false,
|
||||
"strictNullChecks": false,
|
||||
"noUnusedLocals": false,
|
||||
"noImplicitAny": true,
|
||||
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./node_modules/@microsoft"
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
yo @microsoft/sharepoint
|
||||
spfx-fast-serve
|
||||
npm install react react-dom leaflet
|
||||
npm i @spfxappdev/utility
|
||||
npm i
|
||||
npm install react-leaflet
|
||||
npm install -D @types/leaflet
|
||||
npm run serve
|
||||
gulp serve
|
||||
npm run serve
|
||||
npm install -D @types/leaflet babel-loader @babel/core @babel/preset-env @babel/plugin-proposal-nullish-coalescing-operator
|
||||
npm run serve
|
||||
exit
|
||||
npm run serve
|
||||
npm install @pnp/spfx-controls-react --save --save-exact
|
||||
npm run serve
|
||||
exit
|
||||
npm i react-leaflet-markercluster
|
||||
npm run serve
|
||||
npm install @pnp/spfx-property-controls --save --save-exact
|
||||
npm run serve
|
||||
gulp clean; gulp build; gulp bundle --ship; gulp package-solution --ship;
|
||||
npm run serve
|
||||
exit
|
||||
npm run serve
|
||||
gulp clean; gulp build; gulp bundle --ship; gulp package-solution --ship;
|
||||
npm run serve
|
||||
gulp clean; gulp build; gulp bundle --ship; gulp package-solution --ship;
|
||||
exit
|
|
@ -0,0 +1,39 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
.npm
|
||||
.cache
|
||||
.config
|
||||
.rushstack
|
||||
|
||||
|
||||
# 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
|
||||
*.scss.d.ts
|
|
@ -0,0 +1,16 @@
|
|||
!dist
|
||||
config
|
||||
|
||||
gulpfile.js
|
||||
|
||||
release
|
||||
src
|
||||
temp
|
||||
|
||||
tsconfig.json
|
||||
tslint.json
|
||||
|
||||
*.log
|
||||
|
||||
.yo-rc.json
|
||||
.vscode
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Hosted workbench",
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"url": "https://enter-your-SharePoint-site/_layouts/workbench.aspx",
|
||||
"webRoot": "${workspaceRoot}",
|
||||
"sourceMaps": true,
|
||||
"sourceMapPathOverrides": {
|
||||
"webpack:///.././src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../../src/*": "${webRoot}/src/*"
|
||||
},
|
||||
"runtimeArgs": [
|
||||
"--remote-debugging-port=9222",
|
||||
"-incognito"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// Place your settings in this file to overwrite default and user settings.
|
||||
{
|
||||
// Configure glob patterns for excluding files and folders in the file explorer.
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.DS_Store": true,
|
||||
"**/bower_components": true,
|
||||
"**/coverage": true,
|
||||
"**/lib-amd": true,
|
||||
"src/**/*.scss.ts": true
|
||||
},
|
||||
"typescript.tsdk": ".\\node_modules\\typescript\\lib"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"plusBeta": false,
|
||||
"isCreatingSolution": true,
|
||||
"version": "1.14.0",
|
||||
"libraryName": "spfxappdev-webparts-map",
|
||||
"libraryId": "cc048abe-6531-4295-ab7a-12a1c95de606",
|
||||
"environment": "spo",
|
||||
"packageManager": "npm",
|
||||
"solutionName": "spfxappdev.webparts.map",
|
||||
"solutionShortDescription": "spfxappdev.webparts.map description",
|
||||
"skipFeatureDeployment": true,
|
||||
"isDomainIsolated": false,
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
# Interactive Map
|
||||
|
||||
## Summary
|
||||
|
||||
This web parts displays a (world) map. An editor can set custom markers directly in the map. Each marker can configured individually. It is possible to determine the color of the pin, the icon in the pin or what should happen when the pin is clicked. It is even possible to change the tile layer in the web part properties.
|
||||
|
||||
|
||||
![EditMode](assets/WPPreview.png)
|
||||
|
||||
### Create new marker
|
||||
|
||||
![Create new Marker](assets/CreateNewMarker.png)
|
||||
|
||||
### Preview
|
||||
|
||||
![Interactive Map web part preview](assets/MapWPOverview.gif)
|
||||
|
||||
## Compatibility
|
||||
|
||||
![SPFx 1.14](https://img.shields.io/badge/SPFx-1.14-green.svg)
|
||||
![Node.js v14 | v12](https://img.shields.io/badge/Node.js-v14%20%7C%20v12-green.svg)
|
||||
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
|
||||
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
|
||||
![Does not work with SharePoint 2016 (Feature Pack 2)](https://img.shields.io/badge/SharePoint%20Server%202016%20(Feature%20Pack%202)-Incompatible-red.svg "SharePoint Server 2016 Feature Pack 2 requires SPFx 1.1")
|
||||
![Local Workbench Unsupported](https://img.shields.io/badge/Local%20Workbench-Unsupported-red.svg "Local workbench is no longer available as of SPFx 1.13 and above")
|
||||
![Hosted Workbench Compatible](https://img.shields.io/badge/Hosted%20Workbench-Compatible-green.svg)
|
||||
![Compatible with Remote Containers](https://img.shields.io/badge/Remote%20Containers-Compatible-green.svg)
|
||||
|
||||
|
||||
## Applies to
|
||||
|
||||
* [SharePoint Framework](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
|
||||
* [Microsoft 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
|
||||
|
||||
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
react-interactive-map | [Sergej Schwabauer](https://github.com/SPFxAppDev) ([@spfxappdev](https://twitter.com/spfxappdev))
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0|January 19, 2023|Initial release
|
||||
|
||||
|
||||
## Minimal path to awesome
|
||||
|
||||
* Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-interactive-map) then unzip it)
|
||||
* From your command line, change your current directory to the directory containing this sample (`react-interactive-map`, located under `samples`)
|
||||
* in the command line run:
|
||||
* `npm install`
|
||||
* `gulp serve`
|
||||
|
||||
> This sample can also be opened with [VS Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview). Visit <https://aka.ms/spfx-devcontainer> for further instructions.
|
||||
|
||||
## Features
|
||||
|
||||
This Web Part illustrates the following concepts on top of the SharePoint Framework:
|
||||
|
||||
* [Fluent UI React Controls](https://developer.microsoft.com/en-us/fluentui#/controls/web)
|
||||
* [OpenStreetMap](https://www.openstreetmap.org/)
|
||||
* [LeafletJS](https://leafletjs.com/) and the [react-leaflet](https://react-leaflet.js.org/) wrapper
|
||||
* [Leaflet Plugin "Marker cluster"](https://github.com/Leaflet/Leaflet.markercluster) ant the [react-leaflet-markercluster](https://www.npmjs.com/package/react-leaflet-markercluster) wrapper
|
||||
|
||||
## Help
|
||||
|
||||
We do not support samples, but this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.
|
||||
|
||||
If you're having issues building the solution, please run [spfx doctor](https://pnp.github.io/cli-microsoft365/cmd/spfx/spfx-doctor/) from within the solution folder to diagnose incompatibility issues with your environment.
|
||||
|
||||
You can try looking at [issues related to this sample](https://github.com/pnp/sp-dev-fx-webparts/issues?q=label%3A%22sample%3A%20react-interactive-map%22) to see if anybody else is having the same issues.
|
||||
|
||||
You can also try looking at [discussions related to this sample](https://github.com/pnp/sp-dev-fx-webparts/discussions?discussions_q=react-interactive-map) and see what the community is saying.
|
||||
|
||||
If you encounter any issues using this sample, [create a new issue](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected%2Csample%3A%20react-interactive-map&template=bug-report.yml&sample=react-interactive-map&authors=@SPFxAppDev&title=react-interactive-map%20-%20).
|
||||
|
||||
For questions regarding this sample, [create a new question](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aquestion%2Csample%3A%20react-interactive-map&template=question.yml&sample=react-interactive-map&authors=@SPFxAppDev&title=react-interactive-map%20-%20).
|
||||
|
||||
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aenhancement%2Csample%3A%20react-interactive-map&template=suggestion.yml&sample=react-interactive-map&authors=@SPFxAppDev&title=react-interactive-map%20-%20).
|
||||
|
||||
## Disclaimer
|
||||
|
||||
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||
|
||||
<img src="https://pnptelemetry.azurewebsites.net/sp-dev-fx-webparts/samples/react-interactive-map" />
|
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 24 MiB |
After Width: | Height: | Size: 679 KiB |
After Width: | Height: | Size: 2.1 MiB |
|
@ -0,0 +1,50 @@
|
|||
[
|
||||
{
|
||||
"name": "pnp-sp-dev-spfx-web-parts-react-interactive-map",
|
||||
"source": "pnp",
|
||||
"title": "Interactive Map",
|
||||
"shortDescription": "This web parts displays a (world) map. An editor can set custom markers directly in the map. Each marker can configured individually.",
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-interactive-map",
|
||||
"downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-interactive-map",
|
||||
"longDescription": [
|
||||
"This web parts displays a (world) map. An editor can set custom markers directly in the map. Each marker can configured individually."
|
||||
],
|
||||
"creationDateTime": "2023-01-31",
|
||||
"updateDateTime": "2023-01-31",
|
||||
"products": [
|
||||
"SharePoint"
|
||||
],
|
||||
"metadata": [
|
||||
{
|
||||
"key": "CLIENT-SIDE-DEV",
|
||||
"value": "React"
|
||||
},
|
||||
{
|
||||
"key": "SPFX-VERSION",
|
||||
"value": "1.16.1"
|
||||
}
|
||||
],
|
||||
"thumbnails": [
|
||||
{
|
||||
"type": "image",
|
||||
"order": 100,
|
||||
"url": "https://raw.githubusercontent.com/pnp/sp-dev-fx-webparts/main/samples/react-interactive-map/assets/WPPreview.png",
|
||||
"alt": "Web Part Preview"
|
||||
}
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"gitHubAccount": "SPFxAppDev",
|
||||
"pictureUrl": "https://github.com/SPFxAppDev.png",
|
||||
"name": "Sergej Schwabauer"
|
||||
}
|
||||
],
|
||||
"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": {
|
||||
"map-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/map/MapWebPart.js",
|
||||
"manifest": "./src/webparts/map/MapWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"MapWebPartStrings": "lib/webparts/map/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": "spfxappdev-webparts-map",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "SPFx app dev - interactive map webpart",
|
||||
"id": "cc048abe-6531-4295-ab7a-12a1c95de606",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": true,
|
||||
"isDomainIsolated": false,
|
||||
"developer": {
|
||||
"name": "Sergej Schwabauer",
|
||||
"websiteUrl": "https://spfx-app.dev/",
|
||||
"privacyUrl": "",
|
||||
"termsOfUseUrl": "",
|
||||
"mpnId": "Undefined-1.14.0"
|
||||
},
|
||||
"metadata": {
|
||||
"shortDescription": {
|
||||
"default": "Includes an interactive map module with which you can create markers"
|
||||
},
|
||||
"longDescription": {
|
||||
"default": "Includes an interactive map module with which you can create markers"
|
||||
},
|
||||
"screenshotPaths": [],
|
||||
"videoUrl": "",
|
||||
"categories": []
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
"title": "spfxappdev-webparts-map Feature",
|
||||
"description": "The feature that activates elements of the spfxappdev-webparts-map solution.",
|
||||
"id": "e90a5f60-6586-4a90-925e-70a78df55b29",
|
||||
"version": "1.0.0.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/spfxappdev-webparts-map.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"ipAddress": "0.0.0.0",
|
||||
"initialPage": "https://sscwebdev.sharepoint.com/sites/showroom/_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,6 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/s-KaiNet/spfx-fast-serve/master/schema/config.latest.schema.json",
|
||||
"cli": {
|
||||
"isLibraryComponent": false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* User webpack settings file. You can add your own settings here.
|
||||
* Changes from this file will be merged into the base webpack configuration file.
|
||||
* This file will not be overwritten by the subsequent spfx-fast-serve calls.
|
||||
*/
|
||||
|
||||
// you can add your project related webpack configuration here, it will be merged using webpack-merge module
|
||||
// i.e. plugins: [new webpack.Plugin()]
|
||||
|
||||
const path = require("path");
|
||||
|
||||
const webpackConfig = {
|
||||
resolve: {
|
||||
alias: {
|
||||
"@webparts": path.resolve(__dirname, "..", "src/webparts"),
|
||||
"@src": path.resolve(__dirname, "..", "src"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for even more fine-grained control, you can apply custom webpack settings using below function
|
||||
const transformConfig = function (initialWebpackConfig) {
|
||||
// transform the initial webpack config here, i.e.
|
||||
// initialWebpackConfig.plugins.push(new webpack.Plugin()); etc.
|
||||
|
||||
initialWebpackConfig.module.rules.push(
|
||||
{
|
||||
test: /node_modules[\/\\]@?react-leaflet[\/\\].*.js$/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
['@babel/preset-env', { targets: "defaults" }]
|
||||
],
|
||||
plugins: ['@babel/plugin-proposal-nullish-coalescing-operator']
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return initialWebpackConfig;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
webpackConfig,
|
||||
transformConfig
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
'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.`);
|
||||
build.addSuppression(/Warning - \[sass\] The local CSS class/gi);
|
||||
|
||||
var getTasks = build.rig.getTasks;
|
||||
build.rig.getTasks = function () {
|
||||
var result = getTasks.call(build.rig);
|
||||
|
||||
result.set('serve', result.get('serve-deprecated'));
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/* fast-serve */
|
||||
const { addFastServe } = require("spfx-fast-serve-helpers");
|
||||
addFastServe(build);
|
||||
/* end of fast-serve */
|
||||
|
||||
|
||||
/* CUSTOM ALIAS */
|
||||
const path = require('path');
|
||||
build.configureWebpack.mergeConfig({
|
||||
additionalConfiguration: (generatedConfiguration) => {
|
||||
if(!generatedConfiguration.resolve.alias){
|
||||
generatedConfiguration.resolve.alias = {};
|
||||
}
|
||||
|
||||
// webparts folder
|
||||
generatedConfiguration.resolve.alias['@webparts'] = path.resolve( __dirname, 'lib/webparts')
|
||||
|
||||
//root src folder
|
||||
generatedConfiguration.resolve.alias['@src'] = path.resolve( __dirname, 'lib')
|
||||
|
||||
|
||||
//Nullish Operator
|
||||
generatedConfiguration.module.rules.push(
|
||||
{
|
||||
test: /node_modules[\/\\]@?react-leaflet[\/\\].*.js$/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
['@babel/preset-env', { targets: "defaults" }]
|
||||
],
|
||||
plugins: ['@babel/plugin-proposal-nullish-coalescing-operator']
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return generatedConfiguration;
|
||||
}
|
||||
});
|
||||
|
||||
/* CUSTOM ALIAS END */
|
||||
|
||||
build.initialize(require('gulp'));
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg width="50px" height="50px" viewBox="0 0 50 50" version="1.2" baseProfile="tiny" xmlns="http://www.w3.org/2000/svg" overflow="inherit"><path d="M25.015 2.4c-7.8 0-14.121 6.204-14.121 13.854 0 7.652 14.121 32.746 14.121 32.746s14.122-25.094 14.122-32.746c0-7.65-6.325-13.854-14.122-13.854z"/></svg>
|
After Width: | Height: | Size: 301 B |
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "spfxappdev-webparts-map",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test",
|
||||
"serve": "gulp bundle --custom-serve --max_old_space_size=4096 && fast-serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "1.14.0",
|
||||
"@microsoft/sp-lodash-subset": "1.14.0",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.14.0",
|
||||
"@microsoft/sp-property-pane": "1.14.0",
|
||||
"@microsoft/sp-webpart-base": "1.14.0",
|
||||
"@pnp/spfx-controls-react": "3.6.0",
|
||||
"@pnp/spfx-property-controls": "3.5.0",
|
||||
"@spfxappdev/utility": "^1.1.0",
|
||||
"leaflet": "^1.7.1",
|
||||
"office-ui-fabric-react": "7.174.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-leaflet": "^3.2.5",
|
||||
"react-leaflet-markercluster": "^3.0.0-rc1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.5",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@microsoft/rush-stack-compiler-3.9": "0.4.47",
|
||||
"@microsoft/sp-build-web": "1.14.0",
|
||||
"@microsoft/sp-module-interfaces": "1.14.0",
|
||||
"@microsoft/sp-tslint-rules": "1.14.0",
|
||||
"@types/leaflet": "^1.7.9",
|
||||
"@types/react": "16.9.51",
|
||||
"@types/react-dom": "16.9.8",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"ajv": "~5.2.2",
|
||||
"babel-loader": "^8.2.3",
|
||||
"gulp": "~4.0.2",
|
||||
"spfx-fast-serve-helpers": "~1.14.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
|
||||
.autocomplete {
|
||||
display: block;
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
import * as React from 'react';
|
||||
import styles from './Autocomplete.module.scss';
|
||||
import { TextField, ITextFieldProps, Callout, ICalloutProps, DirectionalHint, ITextField, TextFieldBase } from 'office-ui-fabric-react';
|
||||
import { isNullOrEmpty, cssClasses, getDeepOrDefault, isFunction } from '@spfxappdev/utility';
|
||||
|
||||
|
||||
export interface IAutocompleteProps extends Omit<ITextFieldProps, "componentRef"> {
|
||||
showSuggestionsOnFocus?: boolean;
|
||||
minValueLength?: number;
|
||||
onLoadSuggestions?(newValue: string): void;
|
||||
onRenderSuggestions?(inputValue: string): JSX.Element;
|
||||
textFieldRef?(fluentUITextField: ITextField, autocompleteComponent: Autocomplete, htmlInput?: HTMLInputElement);
|
||||
onUpdated?(newValue: string);
|
||||
calloutProps?: Omit<ICalloutProps, "hidden" | "target" | "preventDismissOnScroll" | "directionalHint" | "directionalHintFixed" | "isBeakVisible">;
|
||||
}
|
||||
|
||||
interface IAutocompleteState {
|
||||
currentValue: string;
|
||||
isFlyoutVisible: boolean;
|
||||
}
|
||||
|
||||
export class Autocomplete extends React.Component<IAutocompleteProps, IAutocompleteState> {
|
||||
|
||||
public state: IAutocompleteState = {
|
||||
currentValue: isNullOrEmpty(this.props.defaultValue) ? "" : this.props.defaultValue,
|
||||
isFlyoutVisible: false,
|
||||
};
|
||||
|
||||
public static defaultProps: IAutocompleteProps = {
|
||||
showSuggestionsOnFocus: false,
|
||||
minValueLength: 3,
|
||||
calloutProps: {
|
||||
gapSpace: 0
|
||||
}
|
||||
};
|
||||
|
||||
private textFieldReference: ITextField = null;
|
||||
|
||||
private textFieldDomElement: HTMLInputElement = null;
|
||||
|
||||
private userIsTyping: boolean = false;
|
||||
|
||||
private lastValue: string = "";
|
||||
|
||||
private onUpdateValueText: string = "";
|
||||
|
||||
public render(): React.ReactElement<IAutocompleteProps> {
|
||||
|
||||
return (<>
|
||||
<TextField {...this.props}
|
||||
autoComplete={"off"}
|
||||
className={cssClasses(styles.autocomplete, this.props.className)}
|
||||
componentRef={(input: ITextField) => {
|
||||
this.textFieldReference = input;
|
||||
this.textFieldDomElement = getDeepOrDefault<HTMLInputElement>(input, "_textElement.current", null);
|
||||
|
||||
if(isFunction(this.props.textFieldRef)) {
|
||||
this.props.textFieldRef(input, this, this.textFieldDomElement);
|
||||
}
|
||||
|
||||
}}
|
||||
onFocus={(ev: any) => {
|
||||
if(this.props.showSuggestionsOnFocus) {
|
||||
this.handleSuggestionListVisibility();
|
||||
}
|
||||
|
||||
if(isFunction(this.props.onFocus)) {
|
||||
this.props.onFocus(ev);
|
||||
}
|
||||
}}
|
||||
onBlur={(ev: any) => {
|
||||
|
||||
this.onTextFieldBlur();
|
||||
|
||||
if(isFunction(this.props.onBlur)) {
|
||||
this.props.onBlur(ev);
|
||||
}
|
||||
}}
|
||||
onChange={(ev: any, newValue: string) => {
|
||||
this.onValueChanged(ev, newValue);
|
||||
}}
|
||||
defaultValue={this.state.currentValue}
|
||||
/>
|
||||
|
||||
{this.renderSuggesstionsFlyout()}
|
||||
</>);
|
||||
}
|
||||
|
||||
public updateValue(newValue: string): void {
|
||||
this.onUpdateValueText = newValue;
|
||||
|
||||
this.setState({
|
||||
currentValue: newValue
|
||||
}, () => {
|
||||
(this.textFieldReference as TextFieldBase).setState({
|
||||
uncontrolledValue: this.onUpdateValueText
|
||||
});
|
||||
|
||||
if(isFunction(this.props.onUpdated)) {
|
||||
this.props.onUpdated(newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private renderSuggesstionsFlyout(): JSX.Element {
|
||||
let minWidth: number = getDeepOrDefault<number>(this.props, "calloutProps.calloutMinWidth", -1);
|
||||
|
||||
if(minWidth <= 0) {
|
||||
minWidth = getDeepOrDefault<number>(this, "textFieldDomElement.clientWidth", -1);
|
||||
}
|
||||
|
||||
if(minWidth > 0) {
|
||||
this.props.calloutProps.calloutMinWidth = minWidth;
|
||||
}
|
||||
|
||||
return (<Callout
|
||||
{...this.props.calloutProps}
|
||||
hidden={!this.state.isFlyoutVisible}
|
||||
directionalHintFixed={true}
|
||||
isBeakVisible={false}
|
||||
target={this.textFieldDomElement}
|
||||
onDismiss={(ev?: any) => {
|
||||
this.hideSuggesstionsFlyout();
|
||||
|
||||
if(isFunction(this.props.calloutProps.onDismiss)) {
|
||||
this.props.calloutProps.onDismiss(ev);
|
||||
}
|
||||
}}
|
||||
preventDismissOnScroll={true}
|
||||
directionalHint={DirectionalHint.bottomCenter}>
|
||||
{isFunction(this.props.onRenderSuggestions) && this.props.onRenderSuggestions(this.state.currentValue)}
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
private onValueChanged(ev: any, newValue: string): void {
|
||||
this.userIsTyping = true;
|
||||
|
||||
this.state.currentValue = newValue;
|
||||
this.setState({
|
||||
currentValue: newValue
|
||||
});
|
||||
|
||||
this.handleSuggestionListVisibility();
|
||||
|
||||
if(isFunction(this.props.onChange)) {
|
||||
this.props.onChange(ev, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private onTextFieldBlur(): void {
|
||||
this.userIsTyping = false;
|
||||
window.setTimeout(() => {
|
||||
this.hideSuggesstionsFlyout();
|
||||
}, 150);
|
||||
}
|
||||
|
||||
private handleSuggestionListVisibility(): void {
|
||||
let val = this.state.currentValue;
|
||||
|
||||
if(isNullOrEmpty(val)) {
|
||||
this.hideSuggesstionsFlyout();
|
||||
return;
|
||||
}
|
||||
|
||||
if(val.length < this.props.minValueLength) {
|
||||
this.hideSuggesstionsFlyout();
|
||||
return;
|
||||
}
|
||||
|
||||
let valueWasChanged = false;
|
||||
|
||||
if(!val.Equals(this.lastValue)) {
|
||||
this.userIsTyping = false;
|
||||
valueWasChanged = true;
|
||||
}
|
||||
|
||||
if(!valueWasChanged) {
|
||||
this.showSuggesstionsFlyout();
|
||||
return;
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
if(this.userIsTyping) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showSuggesstionsFlyout();
|
||||
|
||||
if(isFunction(this.props.onLoadSuggestions)) {
|
||||
this.props.onLoadSuggestions(this.state.currentValue);
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
|
||||
private hideSuggesstionsFlyout(): void {
|
||||
this.setState({
|
||||
isFlyoutVisible: false
|
||||
});
|
||||
}
|
||||
|
||||
private showSuggesstionsFlyout(): void {
|
||||
this.setState({
|
||||
isFlyoutVisible: true
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
|
||||
|
||||
.iconpicker {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.suggesstion {
|
||||
display: block;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
padding: 5px 20px;
|
||||
border-bottom: solid 1px $ms-color-themeLighter;
|
||||
border-top: solid 1px $ms-color-themeLighter;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-child {
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
i {
|
||||
padding-right: 20px;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
import * as React from 'react';
|
||||
import styles from './IconPicker.module.scss';
|
||||
import { Icon, ITextField } from 'office-ui-fabric-react';
|
||||
import { allIcons } from './availableIcons';
|
||||
import { isNullOrEmpty, cssClasses, isFunction } from '@spfxappdev/utility';
|
||||
import { Autocomplete, IAutocompleteProps } from '@src/components/autocomplete/Autocomplete';
|
||||
|
||||
|
||||
export interface IIconPickerProps extends Omit<IAutocompleteProps, "onUpdated" | "onChange"> {
|
||||
enableDialogPicker?: boolean;
|
||||
dialogPickerIconName?: string;
|
||||
onIconChanged?(iconName: string): void;
|
||||
}
|
||||
|
||||
interface IIconPickerState {
|
||||
currentValue: string;
|
||||
}
|
||||
|
||||
export class IconPicker extends React.Component<IIconPickerProps, IIconPickerState> {
|
||||
|
||||
public state: IIconPickerState = {
|
||||
currentValue: isNullOrEmpty(this.props.defaultValue) ? "" : this.props.defaultValue
|
||||
};
|
||||
|
||||
public static defaultProps: IIconPickerProps = {
|
||||
dialogPickerIconName: "GroupedList",
|
||||
enableDialogPicker: true,
|
||||
showSuggestionsOnFocus: false,
|
||||
minValueLength: 0
|
||||
};
|
||||
|
||||
private inputValueOnClick: string = "";
|
||||
|
||||
private textFieldReference: ITextField = null;
|
||||
|
||||
private textFieldDomElement: HTMLInputElement = null;
|
||||
|
||||
private autocompleteRef: Autocomplete = null;
|
||||
|
||||
public render(): React.ReactElement<IIconPickerProps> {
|
||||
|
||||
return (<>
|
||||
<Autocomplete {...this.props}
|
||||
textFieldRef={(fluentUITextField: ITextField, autocompleteComponent: Autocomplete, htmlInput: HTMLInputElement) => {
|
||||
this.textFieldReference = fluentUITextField;
|
||||
this.textFieldDomElement = htmlInput;
|
||||
this.autocompleteRef = autocompleteComponent;
|
||||
if(isFunction(this.props.textFieldRef)) {
|
||||
this.props.textFieldRef(fluentUITextField, autocompleteComponent, this.textFieldDomElement);
|
||||
}
|
||||
}}
|
||||
onChange={(ev: any, name: string) => {
|
||||
if(isFunction(this.props.onIconChanged)) {
|
||||
this.props.onIconChanged(name);
|
||||
}
|
||||
}}
|
||||
onUpdated={(name: string) => {
|
||||
if(isFunction(this.props.onIconChanged)) {
|
||||
this.props.onIconChanged(name);
|
||||
}
|
||||
}}
|
||||
className={cssClasses(styles.iconpicker)}
|
||||
defaultValue={this.state.currentValue}
|
||||
onLoadSuggestions={(newValue: string) => {
|
||||
this.setState({
|
||||
currentValue: newValue
|
||||
});
|
||||
}}
|
||||
onRenderSuggestions={() => {
|
||||
return this.renderSuggesstionsFlyout();
|
||||
}}
|
||||
iconProps={{
|
||||
iconName: this.state.currentValue
|
||||
}} />
|
||||
|
||||
|
||||
</>);
|
||||
}
|
||||
|
||||
private renderSuggesstionsFlyout(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className={styles["suggesstion"]}>
|
||||
{allIcons.Where(icon => icon.StartsWith(this.state.currentValue)).map((iconName: string): JSX.Element => {
|
||||
return (<div
|
||||
key={`Icon_${iconName}`}
|
||||
onClick={() => {
|
||||
this.inputValueOnClick = iconName;
|
||||
|
||||
this.setState({
|
||||
currentValue: iconName
|
||||
});
|
||||
|
||||
this.autocompleteRef.updateValue(iconName);
|
||||
}}
|
||||
className={styles["suggesstion-item"]}>
|
||||
<Icon iconName={iconName} />
|
||||
<span>{iconName}</span>
|
||||
</div>);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
.inline-color-picker {
|
||||
padding: 5px;
|
||||
background: rgb(255, 255, 255);
|
||||
border-radius: 1px;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 0px 1px;
|
||||
display: inline-block;
|
||||
border: 1px solid rgb(166, 166, 166);
|
||||
cursor: pointer;
|
||||
|
||||
&-inner {
|
||||
width: 36px;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: darkgrey;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import * as React from 'react';
|
||||
import { ColorPicker, IColorPickerProps, getColorFromString, IColor, Callout, Label } from 'office-ui-fabric-react';
|
||||
import styles from './InlineColorPicker.module.scss';
|
||||
import { isset, isNullOrEmpty } from '@spfxappdev/utility';
|
||||
|
||||
export interface IInlineColorPickerProps extends IColorPickerProps {
|
||||
label?: string;
|
||||
isDisbaled?: boolean;
|
||||
}
|
||||
|
||||
interface IInlineColorPickerState {
|
||||
isPickerVisible: boolean;
|
||||
}
|
||||
|
||||
export class InlineColorPicker extends React.Component<IInlineColorPickerProps, IInlineColorPickerState> {
|
||||
|
||||
public state: IInlineColorPickerState = {
|
||||
isPickerVisible: false,
|
||||
};
|
||||
|
||||
public static defaultProps: IInlineColorPickerProps = {
|
||||
color: '#000000',
|
||||
isDisbaled: false
|
||||
};
|
||||
|
||||
private targetElement: HTMLDivElement = null;
|
||||
|
||||
public render(): React.ReactElement<IInlineColorPickerProps> {
|
||||
|
||||
|
||||
let bc: IColor = null;
|
||||
|
||||
if(typeof this.props.color != "string") {
|
||||
bc = this.props.color;
|
||||
}
|
||||
else {
|
||||
bc = getColorFromString(this.props.color);
|
||||
}
|
||||
|
||||
const customCss: React.CSSProperties = {
|
||||
background: `rgba(${bc.r}, ${bc.g}, ${bc.b}, ${bc.a/100})`
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isNullOrEmpty(this.props.label) &&
|
||||
<Label>{this.props.label}</Label>
|
||||
}
|
||||
<div
|
||||
className={styles['inline-color-picker'] + ` ${this.props.isDisbaled?styles['disabled']:''}`}
|
||||
ref={(r) => {
|
||||
if(isset(r)) {
|
||||
this.targetElement = r;
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
|
||||
if(this.props.isDisbaled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ isPickerVisible: true });
|
||||
}}>
|
||||
<div className={styles['inline-color-picker-inner']} style={customCss}></div>
|
||||
</div>
|
||||
{this.state.isPickerVisible &&
|
||||
<Callout target={this.targetElement} onDismiss={() => {
|
||||
this.setState({ isPickerVisible: false });
|
||||
}}>
|
||||
<ColorPicker {...this.props} />
|
||||
</Callout>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "3f860b48-1dc3-496d-bd28-b145672289cc",
|
||||
"alias": "MapWebPart",
|
||||
"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,
|
||||
"supportsFullBleed": true,
|
||||
"supportedHosts": ["SharePointWebPart", "TeamsPersonalApp", "TeamsTab", "SharePointFullPage"],
|
||||
"supportsThemeVariants": true,
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||
"group": { "default": "Other" },
|
||||
"title": {
|
||||
"default": "Interactive Map",
|
||||
"de-de": "Interaktive Karte"
|
||||
},
|
||||
"description": {
|
||||
"default": "An interactive map with which you can create markers",
|
||||
"de-de": "Eine interactive Karte mit der man Markierungen erstellen kann"
|
||||
},
|
||||
"officeFabricIconFontName": "MapPin",
|
||||
"properties": {
|
||||
"title": "",
|
||||
"markerItems": [],
|
||||
"markerCategories": [],
|
||||
"center": [51.505, -0.09],
|
||||
"startZoom": 13,
|
||||
"maxZoom": 30,
|
||||
"minZoom": 1,
|
||||
"dragging": true,
|
||||
"height": 400,
|
||||
"scrollWheelZoom": true,
|
||||
"plugins": {
|
||||
"searchBox": false,
|
||||
"markercluster": false,
|
||||
"legend": false,
|
||||
"zoomControl": true
|
||||
},
|
||||
"tileLayerUrl": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
"tileLayerAttribution": "© <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,280 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { DisplayMode, Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField,
|
||||
PropertyPaneToggle,
|
||||
PropertyPaneSlider,
|
||||
PropertyPaneButton
|
||||
} from '@microsoft/sp-property-pane';
|
||||
import { PropertyPaneWebPartInformation } from '@pnp/spfx-property-controls/lib/PropertyPaneWebPartInformation';
|
||||
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
|
||||
import * as strings from 'MapWebPartStrings';
|
||||
import Map from './components/Map';
|
||||
import { IMapProps, IMarker, IMarkerCategory } from './components/IMapProps';
|
||||
import ManageMarkerCategoriesDialog, { IManageMarkerCategoriesDialogProps } from './components/ManageMarkerCategoriesDialog';
|
||||
import { isNullOrEmpty } from '@spfxappdev/utility';
|
||||
import { Spinner, ISpinnerProps } from '@microsoft/office-ui-fabric-react-bundle';
|
||||
|
||||
export interface IMapPlugins {
|
||||
searchBox: boolean;
|
||||
markercluster: boolean;
|
||||
legend: boolean;
|
||||
zoomControl: boolean;
|
||||
}
|
||||
|
||||
export interface IMapWebPartProps {
|
||||
markerItems: IMarker[];
|
||||
markerCategories: IMarkerCategory[];
|
||||
title: string;
|
||||
center: [number, number];
|
||||
startZoom: number;
|
||||
maxZoom: number;
|
||||
minZoom: number;
|
||||
height: number;
|
||||
scrollWheelZoom: boolean;
|
||||
dragging: boolean;
|
||||
showPopUp: boolean;
|
||||
plugins: IMapPlugins;
|
||||
tileLayerUrl: string;
|
||||
tileLayerAttribution: string;
|
||||
|
||||
}
|
||||
|
||||
export default class MapWebPart extends BaseClientSideWebPart<IMapWebPartProps> {
|
||||
|
||||
private _isDarkTheme: boolean = false;
|
||||
|
||||
protected onInit(): Promise<void> {
|
||||
return super.onInit();
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
const element: React.ReactElement<IMapProps> = React.createElement(
|
||||
Map,
|
||||
{
|
||||
markerItems: this.properties.markerItems||[],
|
||||
markerCategories: this.properties.markerCategories||[],
|
||||
isEditMode: this.displayMode == DisplayMode.Edit,
|
||||
zoom: this.properties.startZoom,
|
||||
minZoom: this.properties.minZoom,
|
||||
maxZoom: this.properties.maxZoom,
|
||||
center: this.properties.center,
|
||||
title: this.properties.title,
|
||||
height: this.properties.height,
|
||||
plugins: this.properties.plugins,
|
||||
tileLayerUrl: isNullOrEmpty(this.properties.tileLayerUrl) ? "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" : this.properties.tileLayerUrl,
|
||||
tileLayerAttribution: isNullOrEmpty(this.properties.tileLayerAttribution) ? "© <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors" : this.properties.tileLayerAttribution,
|
||||
dragging: this.properties.dragging,
|
||||
scrollWheelZoom: this.properties.scrollWheelZoom,
|
||||
showPopUp: this.properties.showPopUp,
|
||||
|
||||
onMarkerCollectionChanged: (markerItems: IMarker[]) => {
|
||||
this.properties.markerItems = markerItems;
|
||||
},
|
||||
onMarkerCategoriesChanged: (markerCategories: IMarkerCategory[]) => {
|
||||
this.onMarkerCategoriesChanged(markerCategories);
|
||||
},
|
||||
onStartViewSet: (zoomLevel: number, lat: number, lng: number) => {
|
||||
this.properties.startZoom = zoomLevel;
|
||||
this.properties.center = [lat, lng];
|
||||
},
|
||||
|
||||
onTitleUpdate: (value: string) => {
|
||||
this.properties.title = value;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected onDisplayModeChanged(oldDisplayMode: DisplayMode): void {
|
||||
this.reload();
|
||||
}
|
||||
|
||||
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
|
||||
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
|
||||
|
||||
const reloadIfOneOfProps = ["height", "tileLayerUrl", "minZoom", "maxZoom", "tileLayerAttribution", "plugins.zoomControl"];
|
||||
|
||||
if(reloadIfOneOfProps.Contains(p => p.Equals(propertyPath))) {
|
||||
this.reload();
|
||||
}
|
||||
}
|
||||
|
||||
private reload(): void {
|
||||
|
||||
setTimeout(() => {
|
||||
const spinner: React.ReactElement<ISpinnerProps> = React.createElement(Spinner, {
|
||||
|
||||
});
|
||||
|
||||
ReactDom.render(spinner, this.domElement);
|
||||
|
||||
setTimeout(() => { this.render(); }, 300);
|
||||
}, 500);
|
||||
|
||||
|
||||
}
|
||||
|
||||
// protected get disableReactivePropertyChanges(): boolean {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
private onMarkerCategoriesChanged(markerCategories: IMarkerCategory[]): void {
|
||||
this.properties.markerCategories = markerCategories;
|
||||
this.render();
|
||||
}
|
||||
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
|
||||
if (!currentTheme) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isDarkTheme = !!currentTheme.isInverted;
|
||||
const {
|
||||
semanticColors
|
||||
} = currentTheme;
|
||||
this.domElement.style.setProperty('--bodyText', semanticColors.bodyText);
|
||||
this.domElement.style.setProperty('--link', semanticColors.link);
|
||||
this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered);
|
||||
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
groups: [
|
||||
{
|
||||
groupName: strings.WebPartPropertyGroupMapSettings,
|
||||
groupFields: [
|
||||
// PropertyPaneWebPartInformation({
|
||||
// description: `<div class='wp-settings-info'>${strings.WebPartPropertySettingsInfoLabel}</div>`,
|
||||
// key: 'Info_For_3f860b48-1dc3-496d-bd28-b145672289cc'
|
||||
// }),
|
||||
PropertyPaneSlider('minZoom', {
|
||||
label: strings.WebPartPropertyMinZoomLabel,
|
||||
max: 30,
|
||||
min: 0,
|
||||
step: 1
|
||||
}),
|
||||
PropertyPaneSlider('maxZoom', {
|
||||
label: strings.WebPartPropertyMaxZoomLabel,
|
||||
max: 30,
|
||||
min: 5,
|
||||
step: 1
|
||||
}),
|
||||
PropertyPaneSlider('height', {
|
||||
label: strings.WebPartPropertyHeightLabel,
|
||||
min: 100,
|
||||
max: 1200,
|
||||
step: 50
|
||||
}),
|
||||
PropertyPaneToggle('scrollWheelZoom', {
|
||||
label: strings.WebPartPropertyScrollWheelZoomLabel,
|
||||
}),
|
||||
PropertyPaneToggle('dragging', {
|
||||
label: strings.WebPartPropertyMapDraggingLabel,
|
||||
}),
|
||||
PropertyPaneToggle('showPopUp', {
|
||||
label: strings.WebPartPropertyShowPopUpLabel,
|
||||
}),
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
isCollapsed: true,
|
||||
groupName: strings.WebPartPropertyGroupTileLayerSettings,
|
||||
groupFields: [
|
||||
PropertyPaneWebPartInformation({
|
||||
description: `<div class='wp-settings-info'>${strings.WebPartPropertyTileLayerUrlInformationLabel}</div>`,
|
||||
key: 'Tile_For_3f860b48-1dc3-496d-bd28-b145672289cc'
|
||||
}),
|
||||
PropertyPaneTextField('tileLayerUrl', {
|
||||
label: strings.WebPartPropertyTileLayerUrlLabel
|
||||
}),
|
||||
PropertyPaneTextField('tileLayerAttribution', {
|
||||
label: strings.WebPartPropertyTileLayerAttributionLabel
|
||||
}),
|
||||
]
|
||||
},
|
||||
{
|
||||
isCollapsed: true,
|
||||
groupName: strings.WebPartPropertyGroupPlugins,
|
||||
groupFields: [
|
||||
PropertyPaneToggle('plugins.searchBox', {
|
||||
label: strings.WebPartPropertyPluginSearchboxLabel
|
||||
}),
|
||||
PropertyPaneToggle('plugins.markercluster', {
|
||||
label: strings.WebPartPropertyPluginMarkerClusterLabel,
|
||||
}),
|
||||
PropertyPaneToggle('plugins.zoomControl', {
|
||||
label: strings.WebPartPropertyPluginZoomControlLabel
|
||||
}),
|
||||
]
|
||||
},
|
||||
{
|
||||
isCollapsed: true,
|
||||
groupName: strings.WebPartPropertyGroupCategories,
|
||||
groupFields: [
|
||||
PropertyPaneButton(null, {
|
||||
text: strings.WebPartPropertyButtonManageCategories,
|
||||
onClick: (val: any) => {
|
||||
const dummyElement: HTMLDivElement = document.createElement("div");
|
||||
document.body.appendChild(dummyElement);
|
||||
|
||||
const element: React.ReactElement<IManageMarkerCategoriesDialogProps> = React.createElement(ManageMarkerCategoriesDialog, {
|
||||
markerCategories: this.properties.markerCategories,
|
||||
onDismiss: () => {
|
||||
dummyElement.remove();
|
||||
},
|
||||
onMarkerCategoriesChanged: (markerCategories: IMarkerCategory[]) => {
|
||||
dummyElement.remove();
|
||||
this.onMarkerCategoriesChanged(markerCategories);
|
||||
},
|
||||
});
|
||||
|
||||
ReactDom.render(element, dummyElement);
|
||||
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
PropertyPaneToggle('plugins.legend', {
|
||||
label: strings.WebPartPropertyPluginLegendLabel
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
groupName: strings.WebPartPropertyGroupAbout,
|
||||
groupFields: [
|
||||
PropertyPaneWebPartInformation({
|
||||
description: `<h3>Author</h3>
|
||||
<a href='https://spfx-app.dev/' data-interception="off" target='_blank'>SPFx-App.dev</a>
|
||||
<h3>Version</h3>
|
||||
${this.context.manifest.version}
|
||||
<h3>Web Part Instance id</h3>
|
||||
${this.context.instanceId}`,
|
||||
moreInfoLink: `https://spfxappdev.github.io/sp-map-webpart/`,
|
||||
key: '3f860b48-1dc3-496d-bd28-b145672289cc'
|
||||
})
|
||||
]
|
||||
}
|
||||
],
|
||||
displayGroupsAsAccordion: true,
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,387 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import { IMarker, IMarkerCategory, MarkerType } from './IMapProps';
|
||||
import './Map.module.scss';
|
||||
import { cloneDeep } from '@microsoft/sp-lodash-subset';
|
||||
import { Icon, Panel, TextField, IPanelProps, PrimaryButton, DefaultButton, IChoiceGroupOption, ChoiceGroup, IDropdownOption, Dropdown, getColorFromString, IColor, PanelType, Label, TooltipHost } from 'office-ui-fabric-react';
|
||||
import { Guid } from '@microsoft/sp-core-library';
|
||||
import { isNullOrEmpty, isFunction } from '@spfxappdev/utility';
|
||||
import { InlineColorPicker } from '@src/components/inlineColorPicker/InlineColorPicker';
|
||||
import { RichText } from "@pnp/spfx-controls-react/lib/RichText";
|
||||
import '@spfxappdev/utility/lib/extensions/StringExtensions';
|
||||
import '@spfxappdev/utility/lib/extensions/ArrayExtensions';
|
||||
import ManageMarkerCategoriesDialog from './ManageMarkerCategoriesDialog';
|
||||
import { MarkerIcon } from './MarkerIcon';
|
||||
import * as strings from 'MapWebPartStrings';
|
||||
import { IconPicker } from '@src/components/iconPicker/IconPicker';
|
||||
|
||||
export interface IAddOrEditPanelProps {
|
||||
markerItem: IMarker;
|
||||
markerCategories: IMarkerCategory[];
|
||||
onDismiss();
|
||||
onMarkerChanged(markerItem: IMarker, isNewMarker: boolean);
|
||||
onDeleteMarker(markerItem: IMarker);
|
||||
onChangePositionClick(markerItem: IMarker);
|
||||
onMarkerCategoriesChanged(markerCategories: IMarkerCategory[]);
|
||||
}
|
||||
|
||||
interface IAddOrEditPanelState {
|
||||
markerItem: IMarker;
|
||||
markerCategories: IMarkerCategory[];
|
||||
isSaveButtonDisabled: boolean;
|
||||
isManageCategoriesDialogVisible: boolean;
|
||||
|
||||
}
|
||||
|
||||
export default class AddOrEditPanel extends React.Component<IAddOrEditPanelProps, IAddOrEditPanelState> {
|
||||
|
||||
public state: IAddOrEditPanelState = {
|
||||
markerItem: cloneDeep(this.props.markerItem),
|
||||
markerCategories: cloneDeep(this.props.markerCategories),
|
||||
isSaveButtonDisabled: false,
|
||||
isManageCategoriesDialogVisible: false
|
||||
};
|
||||
|
||||
private readonly isNewMarker: boolean;
|
||||
|
||||
private readonly headerText: string;
|
||||
|
||||
private markerTypeOptions: IChoiceGroupOption[] = [
|
||||
{ key: 'Panel', text: strings.ChoiceGroupPanelLabel, iconProps: { iconName: 'SidePanel' } },
|
||||
{ key: 'Dialog', text: strings.ChoiceGroupDialogLabel, iconProps: { iconName: 'Favicon' } },
|
||||
{ key: 'Url', text: strings.ChoiceGroupUrlLabel, iconProps: { iconName: 'Link' } },
|
||||
{ key: 'None', text: strings.ChoiceGroupNoneLabel, iconProps: { iconName: 'FieldEmpty' } },
|
||||
];
|
||||
|
||||
private urlOptions: IChoiceGroupOption[] = [
|
||||
{ key: '_self', text: strings.ChoiceGroupTargetSelfLabel },
|
||||
{ key: '_blank', text: strings.ChoiceGroupTargetBlankLabel },
|
||||
{ key: 'embedded', text: strings.ChoiceGroupTargetEmbeddedLabel },
|
||||
];
|
||||
|
||||
constructor(props: IAddOrEditPanelProps) {
|
||||
super(props);
|
||||
|
||||
this.isNewMarker = this.props.markerItem.id.Equals(Guid.empty.toString());
|
||||
this.headerText = this.isNewMarker ? strings.PanelHeaderNewLabel : strings.PanelHeaderEditLabel;
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IAddOrEditPanelProps>, prevState: Readonly<IAddOrEditPanelState>, snapshot?: any): void {
|
||||
|
||||
if(!JSON.stringify(prevProps.markerCategories).Equals(JSON.stringify(this.props.markerCategories)) ||
|
||||
!JSON.stringify(prevProps.markerItem).Equals(JSON.stringify(this.props.markerItem))) {
|
||||
this.setState({
|
||||
markerCategories: cloneDeep(this.props.markerCategories),
|
||||
markerItem: cloneDeep(this.props.markerItem),
|
||||
isSaveButtonDisabled: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IAddOrEditPanelProps> {
|
||||
|
||||
const selectedCatId: string = this.state.markerCategories.Contains(cat => cat.id.Equals(this.state.markerItem.categoryId)) ? this.state.markerItem.categoryId : Guid.empty.toString();
|
||||
|
||||
return (
|
||||
<Panel
|
||||
type={PanelType.medium}
|
||||
isOpen={true}
|
||||
onDismiss={() => { this.onConfigPanelDismiss(); }}
|
||||
headerText={this.headerText}
|
||||
closeButtonAriaLabel={strings.CloseLabel}
|
||||
onRenderFooterContent={(props: IPanelProps) => {
|
||||
return this.renderPanelFooter();
|
||||
}}
|
||||
// Stretch panel content to fill the available height so the footer is positioned
|
||||
// at the bottom of the page
|
||||
isFooterAtBottom={true}
|
||||
>
|
||||
<Label>
|
||||
{strings.LabelCategory}
|
||||
<span
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
isManageCategoriesDialogVisible: true
|
||||
});
|
||||
}}
|
||||
className='manage-categories-label'>
|
||||
({strings.LabelManage})
|
||||
</span>
|
||||
</Label>
|
||||
<Dropdown
|
||||
placeholder={strings.PlaceholderSelectACategory}
|
||||
defaultSelectedKey={selectedCatId}
|
||||
onChange={(ev: any, option: IDropdownOption) => {
|
||||
this.state.markerItem.categoryId = option.key.toString();
|
||||
this.setState({
|
||||
markerItem: this.state.markerItem,
|
||||
isSaveButtonDisabled: false
|
||||
});
|
||||
|
||||
}}
|
||||
options={this.categoryOptions}
|
||||
/>
|
||||
<ChoiceGroup
|
||||
label={strings.LabelMarkerType}
|
||||
defaultSelectedKey={this.state.markerItem.type}
|
||||
onChange={(ev: any, option: IChoiceGroupOption) => {
|
||||
this.state.markerItem.type = option.key.toString() as MarkerType;
|
||||
|
||||
// if( this.state.markerItem.type == "None") {
|
||||
// this.state.markerItem.markerClickProps = undefined;
|
||||
// }
|
||||
|
||||
this.setState({
|
||||
markerItem: this.state.markerItem,
|
||||
isSaveButtonDisabled: false
|
||||
});
|
||||
}}
|
||||
options={this.markerTypeOptions} />
|
||||
|
||||
{this.renderNonCategorySettings()}
|
||||
{this.renderUrlSettings()}
|
||||
{this.renderPanelOrDialogSettings()}
|
||||
{this.renderManageCategoriesDialog()}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPanelFooter(): JSX.Element {
|
||||
return (<div className='panel-footer'>
|
||||
<PrimaryButton
|
||||
text={strings.SaveLabel}
|
||||
disabled={this.state.isSaveButtonDisabled}
|
||||
onClick={() => {
|
||||
|
||||
if(this.isNewMarker) {
|
||||
this.state.markerItem.id = Guid.newGuid().toString();
|
||||
}
|
||||
|
||||
this.onSaveMarkerClick(this.state.markerItem);
|
||||
}}
|
||||
/>
|
||||
|
||||
{!this.isNewMarker &&
|
||||
<>
|
||||
<DefaultButton text={strings.DeleteLabel} onClick={() => { this.onDeleteMarkerClick(this.state.markerItem); }} />
|
||||
<DefaultButton text={strings.ChangePositionLabel} onClick={() => { this.onChangePositionClick(this.state.markerItem); }} />
|
||||
</>
|
||||
}
|
||||
|
||||
<DefaultButton text={strings.CancelLabel} onClick={() => { this.onConfigPanelDismiss(); }} />
|
||||
</div>);
|
||||
}
|
||||
|
||||
private renderNonCategorySettings(): JSX.Element {
|
||||
|
||||
if(this.state.markerCategories.Contains(cat => cat.id.Equals(this.state.markerItem.categoryId))) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<InlineColorPicker
|
||||
label={strings.LabelMarkerColor}
|
||||
alphaType='none'
|
||||
color={getColorFromString(this.state.markerItem.iconProperties.markerColor)}
|
||||
onChange={(ev: any, color: IColor) => {
|
||||
this.state.markerItem.iconProperties.markerColor = "#" + color.hex;
|
||||
this.setState({
|
||||
markerItem: this.state.markerItem,
|
||||
isSaveButtonDisabled: false
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* <TextField label={strings.LabelIcon} description={strings.LabelLeaveEmpty} defaultValue={this.state.markerItem.iconProperties.iconName} onChange={(ev: any, iconName: string) => {
|
||||
this.state.markerItem.iconProperties.iconName = iconName;
|
||||
this.setState({
|
||||
markerItem: this.state.markerItem,
|
||||
isSaveButtonDisabled: false
|
||||
});
|
||||
}} /> */}
|
||||
|
||||
<IconPicker
|
||||
label={strings.LabelIcon}
|
||||
description={strings.LabelLeaveEmpty}
|
||||
defaultValue={this.state.markerItem.iconProperties.iconName}
|
||||
onIconChanged={(iconName: string) => {
|
||||
this.state.markerItem.iconProperties.iconName = iconName;
|
||||
this.setState({
|
||||
markerItem: this.state.markerItem,
|
||||
isSaveButtonDisabled: false
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<InlineColorPicker
|
||||
label={strings.LabelIconColor}
|
||||
alphaType='none'
|
||||
color={getColorFromString(this.state.markerItem.iconProperties.iconColor)}
|
||||
onChange={(ev: any, color: IColor) => {
|
||||
this.state.markerItem.iconProperties.iconColor = "#" + color.hex;
|
||||
this.setState({
|
||||
markerItem: this.state.markerItem,
|
||||
isSaveButtonDisabled: false
|
||||
});
|
||||
}}
|
||||
isDisbaled={isNullOrEmpty(this.state.markerItem.iconProperties.iconName)}
|
||||
/>
|
||||
|
||||
<Label>
|
||||
{strings.LabelTooltip}
|
||||
<TooltipHost content={strings.TooltipInfo}>
|
||||
<Icon className='info-tooltip' iconName='Info' />
|
||||
</TooltipHost>
|
||||
</Label>
|
||||
<TextField description={strings.LabelLeaveEmptyTooltip} defaultValue={this.state.markerItem.popuptext} onChange={(ev: any, popuptext: string) => {
|
||||
this.state.markerItem.popuptext = popuptext;
|
||||
this.setState({
|
||||
markerItem: this.state.markerItem,
|
||||
isSaveButtonDisabled: false
|
||||
});
|
||||
}} />
|
||||
|
||||
<Label>{strings.LabelPreview}</Label>
|
||||
<div style={{position: "relative", height: "36px", }}>
|
||||
<div style={{position: "absolute"}}>
|
||||
<MarkerIcon {...this.state.markerItem.iconProperties} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPanelOrDialogSettings(): JSX.Element {
|
||||
|
||||
if(!(this.state.markerItem.type == "Dialog" || this.state.markerItem.type == "Panel")) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
const headerLabel: string = this.state.markerItem.type == "Dialog" ? strings.LabelDialogHeader : strings.LabelPanelHeader;
|
||||
|
||||
return (<>
|
||||
<TextField label={headerLabel} defaultValue={this.state.markerItem.markerClickProps.content.headerText} onChange={(ev: any, headerText: string) => {
|
||||
this.state.markerItem.markerClickProps.content.headerText = headerText;
|
||||
this.setState({
|
||||
markerItem: this.state.markerItem,
|
||||
isSaveButtonDisabled: false
|
||||
});
|
||||
}} />
|
||||
|
||||
<Label>{strings.LabelContent}</Label>
|
||||
<RichText isEditMode={true} value={this.state.markerItem.markerClickProps.content.html} onChange={(content: string): string => {
|
||||
this.state.markerItem.markerClickProps.content.html = content;
|
||||
this.setState({
|
||||
markerItem: this.state.markerItem,
|
||||
isSaveButtonDisabled: false
|
||||
});
|
||||
|
||||
return content;
|
||||
}} />
|
||||
|
||||
</>);
|
||||
}
|
||||
|
||||
private renderUrlSettings(): JSX.Element {
|
||||
|
||||
if(this.state.markerItem.type != "Url") {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextField label={strings.LabelUrl} type='url' defaultValue={this.state.markerItem.markerClickProps.url.href} onChange={(ev: any, url: string) => {
|
||||
this.state.markerItem.markerClickProps.url.href = url;
|
||||
this.setState({
|
||||
markerItem: this.state.markerItem,
|
||||
isSaveButtonDisabled: false
|
||||
});
|
||||
}} />
|
||||
|
||||
<ChoiceGroup
|
||||
defaultSelectedKey={this.state.markerItem.markerClickProps.url.target}
|
||||
options={this.urlOptions}
|
||||
onChange={(ev: any, option: IChoiceGroupOption) => {
|
||||
(this.state.markerItem.markerClickProps.url.target as any) = option.key;
|
||||
|
||||
this.setState({
|
||||
markerItem: this.state.markerItem,
|
||||
isSaveButtonDisabled: false
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderManageCategoriesDialog(): JSX.Element {
|
||||
|
||||
if(!this.state.isManageCategoriesDialogVisible) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ManageMarkerCategoriesDialog
|
||||
markerCategories={this.props.markerCategories}
|
||||
onDismiss={() => {
|
||||
this.setState({
|
||||
isManageCategoriesDialogVisible: false
|
||||
});
|
||||
}}
|
||||
onMarkerCategoriesChanged={(markerCategories: IMarkerCategory[]) => {
|
||||
|
||||
this.setState({
|
||||
isManageCategoriesDialogVisible: false,
|
||||
markerCategories: markerCategories
|
||||
});
|
||||
|
||||
if(isFunction(this.props.onMarkerCategoriesChanged)) {
|
||||
this.props.onMarkerCategoriesChanged(markerCategories);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private onConfigPanelDismiss(): void {
|
||||
if(isFunction(this.props.onDismiss)) {
|
||||
this.props.onDismiss();
|
||||
}
|
||||
}
|
||||
|
||||
private onSaveMarkerClick(marker: IMarker): void {
|
||||
|
||||
if(isFunction(this.props.onMarkerChanged)) {
|
||||
this.props.onMarkerChanged(marker, this.isNewMarker);
|
||||
}
|
||||
}
|
||||
|
||||
private onDeleteMarkerClick(marker: IMarker): void {
|
||||
|
||||
if(isFunction(this.props.onDeleteMarker)) {
|
||||
this.props.onDeleteMarker(marker);
|
||||
}
|
||||
}
|
||||
|
||||
private onChangePositionClick(marker: IMarker): void {
|
||||
|
||||
if(isFunction(this.props.onChangePositionClick)) {
|
||||
this.props.onChangePositionClick(marker);
|
||||
}
|
||||
}
|
||||
|
||||
private get categoryOptions(): IDropdownOption[] {
|
||||
const categories: IDropdownOption[] = [
|
||||
{ key: Guid.empty.toString(), text: 'None' }
|
||||
];
|
||||
|
||||
this.state.markerCategories.forEach((category: IMarkerCategory) => {
|
||||
categories.push({ key: category.id, text: category.name });
|
||||
});
|
||||
|
||||
return categories;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import { Guid } from '@microsoft/sp-core-library';
|
||||
import * as L from 'leaflet';
|
||||
import { IMapPlugins } from '../MapWebPart';
|
||||
|
||||
export type MarkerType = "Panel"|"Dialog"|"Url"|"None";
|
||||
|
||||
export interface IMarkerClickProps {
|
||||
url: IMarkerUrlProperties;
|
||||
content: IMarkerContentProperties;
|
||||
}
|
||||
|
||||
export interface IMarkerUrlProperties {
|
||||
href: string;
|
||||
target: '_self'|'_blank'|'embedded';
|
||||
}
|
||||
|
||||
export interface IMarkerContentProperties {
|
||||
headerText: string;
|
||||
html: string;
|
||||
}
|
||||
|
||||
export interface IMarkerIcon {
|
||||
markerColor: string;
|
||||
iconName: string;
|
||||
iconColor: string;
|
||||
}
|
||||
|
||||
export interface IMarkerCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
popuptext?: string;
|
||||
iconProperties: IMarkerIcon;
|
||||
}
|
||||
|
||||
export interface IMarker {
|
||||
id: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
type: MarkerType;
|
||||
categoryId: string;
|
||||
iconProperties?: IMarkerIcon;
|
||||
popuptext?: string;
|
||||
markerClickProps?: IMarkerClickProps;
|
||||
}
|
||||
|
||||
export interface IMapProps {
|
||||
markerItems: IMarker[];
|
||||
markerCategories: IMarkerCategory[];
|
||||
isEditMode: boolean;
|
||||
zoom?: number;
|
||||
center?: [number, number];
|
||||
maxZoom?: number;
|
||||
minZoom?: number;
|
||||
title?: string;
|
||||
height: number;
|
||||
dragging: boolean;
|
||||
scrollWheelZoom: boolean;
|
||||
plugins: IMapPlugins;
|
||||
tileLayerUrl: string;
|
||||
tileLayerAttribution: string;
|
||||
showPopUp: boolean;
|
||||
|
||||
|
||||
onMarkerCollectionChanged(markerItems: IMarker[]);
|
||||
onMarkerCategoriesChanged(markerCategories: IMarkerCategory[]);
|
||||
onStartViewSet(zoomLevel: number, lat: number, lng: number);
|
||||
onTitleUpdate?: (value: string) => void;
|
||||
}
|
||||
|
||||
export const emptyMarkerItem: IMarker = {
|
||||
id: Guid.empty.toString(),
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
type: "Panel",
|
||||
markerClickProps: {
|
||||
url: { href: "", target: '_blank' },
|
||||
content: { html: '', headerText: '' }
|
||||
},
|
||||
categoryId: Guid.empty.toString(),
|
||||
iconProperties: {
|
||||
markerColor: "#000000",
|
||||
iconName: "FullCircleMask",
|
||||
iconColor: "#ffffff"
|
||||
},
|
||||
popuptext: null
|
||||
};
|
|
@ -0,0 +1,240 @@
|
|||
import * as React from 'react';
|
||||
import { IMarkerCategory } from './IMapProps';
|
||||
import './Map.module.scss';
|
||||
import { cloneDeep } from '@microsoft/sp-lodash-subset';
|
||||
import { Icon, Dialog, TextField, PrimaryButton, DefaultButton, getColorFromString, IColor, DialogFooter, DialogContent, DialogType, MessageBar, TooltipHost } from 'office-ui-fabric-react';
|
||||
import { Guid } from '@microsoft/sp-core-library';
|
||||
import { isNullOrEmpty, isFunction } from '@spfxappdev/utility';
|
||||
import { InlineColorPicker } from '@src/components/inlineColorPicker/InlineColorPicker';
|
||||
import '@spfxappdev/utility/lib/extensions/StringExtensions';
|
||||
import '@spfxappdev/utility/lib/extensions/ArrayExtensions';
|
||||
import { IconButton } from '@microsoft/office-ui-fabric-react-bundle';
|
||||
import { MarkerIcon } from './MarkerIcon';
|
||||
import * as strings from 'MapWebPartStrings';
|
||||
import { IconPicker } from '@src/components/iconPicker/IconPicker';
|
||||
|
||||
export interface IManageMarkerCategoriesDialogProps {
|
||||
markerCategories: IMarkerCategory[];
|
||||
onDismiss();
|
||||
onMarkerCategoriesChanged(markerCategories: IMarkerCategory[]);
|
||||
}
|
||||
|
||||
interface IManageMarkerCategoriesDialogState {
|
||||
markerCategories: IMarkerCategory[];
|
||||
isSaveButtonDisabled: boolean;
|
||||
isNewFormVisible: boolean;
|
||||
isDialogVisible: boolean;
|
||||
}
|
||||
|
||||
export default class ManageMarkerCategoriesDialog extends React.Component<IManageMarkerCategoriesDialogProps, IManageMarkerCategoriesDialogState> {
|
||||
|
||||
public state: IManageMarkerCategoriesDialogState = {
|
||||
markerCategories: cloneDeep(this.props.markerCategories),
|
||||
isSaveButtonDisabled: false,
|
||||
isNewFormVisible: false,
|
||||
isDialogVisible: true
|
||||
};
|
||||
|
||||
constructor(props: IManageMarkerCategoriesDialogProps) {
|
||||
super(props);
|
||||
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.validateForm();
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IManageMarkerCategoriesDialogProps> {
|
||||
return (
|
||||
<Dialog
|
||||
hidden={!this.state.isDialogVisible}
|
||||
onDismiss={() => { this.onDialogDismiss(); }}
|
||||
dialogContentProps={{
|
||||
title: strings.DialogTitleManageCategories,
|
||||
type: DialogType.close
|
||||
}}
|
||||
minWidth={800}
|
||||
modalProps={{
|
||||
isBlocking: true,
|
||||
className: "categories-dialog"
|
||||
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
|
||||
<div className='spfxappdev-grid'>
|
||||
<MessageBar className='category-messagebar'>
|
||||
{isNullOrEmpty(this.state.markerCategories) && <>{strings.InfoTextNoCategories} </>}{strings.InfoTextCategories}
|
||||
</MessageBar>
|
||||
|
||||
{!isNullOrEmpty(this.state.markerCategories) &&
|
||||
<>
|
||||
<div className='spfxappdev-grid-row grid-header'>
|
||||
<div className='spfxappdev-grid-col spfxappdev-sm1'></div>
|
||||
<div className='spfxappdev-grid-col spfxappdev-sm3'>{strings.LabelCategoryHeaderName}</div>
|
||||
<div className='spfxappdev-grid-col spfxappdev-sm1'>{strings.LabelMarkerColor}</div>
|
||||
<div className='spfxappdev-grid-col spfxappdev-sm3'>
|
||||
{strings.LabelIcon}
|
||||
<TooltipHost content={strings.LabelLeaveEmpty}>
|
||||
<Icon className='info-tooltip' iconName='Info' />
|
||||
</TooltipHost></div>
|
||||
<div className='spfxappdev-grid-col spfxappdev-sm1'>{strings.LabelIconColor}</div>
|
||||
<div className='spfxappdev-grid-col spfxappdev-sm2'>
|
||||
{strings.LabelTooltip}
|
||||
<TooltipHost content={strings.TooltipInfoCategory}>
|
||||
<Icon className='info-tooltip' iconName='Info' />
|
||||
</TooltipHost>
|
||||
</div>
|
||||
<div className='spfxappdev-grid-col spfxappdev-sm1'></div>
|
||||
</div>
|
||||
{this.state.markerCategories.map((cat: IMarkerCategory, index: number): JSX.Element => {
|
||||
return (<div key={cat.id} className='spfxappdev-grid-row categories-grid' data-catid={cat.id}>
|
||||
{this.renderForm(cat, index)}
|
||||
</div>);
|
||||
})}
|
||||
</>
|
||||
}
|
||||
<div className='spfxappdev-grid-row grid-footer'>
|
||||
|
||||
<div className='spfxappdev-grid-col spfxappdev-sm12'>
|
||||
<PrimaryButton
|
||||
text={strings.AddLabel}
|
||||
onClick={() => {
|
||||
this.onAddNewCatagoryButtonClick();
|
||||
}} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</DialogContent>
|
||||
|
||||
<DialogFooter>
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
|
||||
if(isFunction(this.props.onMarkerCategoriesChanged)) {
|
||||
this.props.onMarkerCategoriesChanged(this.state.markerCategories);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isDialogVisible: false
|
||||
});
|
||||
}}
|
||||
text={strings.SaveLabel}
|
||||
disabled={this.state.isSaveButtonDisabled}
|
||||
/>
|
||||
<DefaultButton onClick={() => {
|
||||
this.onDialogDismiss();
|
||||
}} text={strings.CancelLabel} />
|
||||
</DialogFooter>
|
||||
|
||||
</Dialog>);
|
||||
}
|
||||
|
||||
private renderForm(categoryItem: IMarkerCategory, index: number): JSX.Element {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='spfxappdev-grid-col spfxappdev-sm1'>
|
||||
<IconButton iconProps={{iconName: "Delete"}} onClick={() => {
|
||||
this.state.markerCategories.RemoveAt(index);
|
||||
this.validateForm();
|
||||
}} />
|
||||
</div>
|
||||
<div className='spfxappdev-grid-col spfxappdev-sm3'>
|
||||
<TextField
|
||||
required={true}
|
||||
defaultValue={categoryItem.name}
|
||||
onChange={(ev: any, name: string) => {
|
||||
this.state.markerCategories[index].name = name;
|
||||
this.validateForm();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='spfxappdev-grid-col spfxappdev-sm1'>
|
||||
<InlineColorPicker
|
||||
alphaType='none'
|
||||
color={getColorFromString(categoryItem.iconProperties.markerColor)}
|
||||
onChange={(ev: any, color: IColor) => {
|
||||
this.state.markerCategories[index].iconProperties.markerColor = "#" + color.hex;
|
||||
this.validateForm();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='spfxappdev-grid-col spfxappdev-sm3'>
|
||||
<IconPicker
|
||||
defaultValue={categoryItem.iconProperties.iconName}
|
||||
onIconChanged={(name: string) => {
|
||||
this.state.markerCategories[index].iconProperties.iconName = name;
|
||||
this.validateForm();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='spfxappdev-grid-col spfxappdev-sm1'>
|
||||
<InlineColorPicker
|
||||
alphaType='none'
|
||||
color={getColorFromString(categoryItem.iconProperties.iconColor)}
|
||||
onChange={(ev: any, color: IColor) => {
|
||||
this.state.markerCategories[index].iconProperties.iconColor = "#" + color.hex;
|
||||
this.validateForm();
|
||||
}}
|
||||
isDisbaled={isNullOrEmpty(categoryItem.iconProperties.iconName)}
|
||||
/>
|
||||
</div>
|
||||
<div className='spfxappdev-grid-col spfxappdev-sm2'>
|
||||
<TextField
|
||||
defaultValue={categoryItem.popuptext}
|
||||
onChange={(ev: any, popuptext: string) => {
|
||||
this.state.markerCategories[index].popuptext = popuptext;
|
||||
this.validateForm();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='spfxappdev-grid-col spfxappdev-sm1'>
|
||||
<div style={{position: "absolute"}}>
|
||||
<MarkerIcon {...categoryItem.iconProperties} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private validateForm(): void {
|
||||
|
||||
const isSaveBtnDisabled = this.state.markerCategories.Contains(cat => isNullOrEmpty(cat.name) || isNullOrEmpty(cat.iconProperties.markerColor));
|
||||
|
||||
this.setState({
|
||||
markerCategories: this.state.markerCategories,
|
||||
isSaveButtonDisabled: isSaveBtnDisabled
|
||||
});
|
||||
}
|
||||
|
||||
private onAddNewCatagoryButtonClick(): void {
|
||||
this.state.markerCategories.push(this.createNewCatagoryItem());
|
||||
|
||||
this.validateForm();
|
||||
}
|
||||
|
||||
private createNewCatagoryItem(): IMarkerCategory {
|
||||
const category: IMarkerCategory = {
|
||||
id: Guid.newGuid().toString(),
|
||||
name: "",
|
||||
iconProperties: {
|
||||
markerColor: "#000000",
|
||||
iconName: "FullCircleMask",
|
||||
iconColor: "#ffffff"
|
||||
}
|
||||
};
|
||||
|
||||
return category;
|
||||
}
|
||||
|
||||
private onDialogDismiss(): void {
|
||||
this.setState({
|
||||
isDialogVisible: false
|
||||
});
|
||||
this.props.onDismiss();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,223 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
|
||||
|
||||
.map {
|
||||
display: block;
|
||||
min-height: 400px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:global {
|
||||
.map-icon {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 5px;
|
||||
color: #fff;
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.display-mode .marker-type-none {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.manage-categories-label {
|
||||
font-size: 10px;
|
||||
color: #1190F4;
|
||||
padding-left: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.spfxappdev-grid {
|
||||
box-sizing: border-box;
|
||||
zoom: 1;
|
||||
padding: 0 8px;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
display: table;
|
||||
content: '';
|
||||
line-height: 0;
|
||||
-webkit-box-sizing: inherit;
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
&::after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
&-row {
|
||||
margin: 0 -8px;
|
||||
box-sizing: border-box;
|
||||
zoom: 1;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
display: table;
|
||||
content: '';
|
||||
line-height: 0;
|
||||
-webkit-box-sizing: inherit;
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
&::after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&-col {
|
||||
position: relative;
|
||||
min-height: 1px;
|
||||
// padding-left: 8px;
|
||||
// padding-right: 8px;
|
||||
box-sizing: border-box;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.spfxappdev-sm1 {
|
||||
width: 8.33%
|
||||
}
|
||||
|
||||
.spfxappdev-sm2 {
|
||||
width: 16.66%;
|
||||
}
|
||||
|
||||
.spfxappdev-sm3 {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.spfxappdev-sm4 {
|
||||
width: 33.33%;
|
||||
}
|
||||
|
||||
.spfxappdev-sm5 {
|
||||
width: 41.66%;
|
||||
}
|
||||
|
||||
.spfxappdev-sm6 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.spfxappdev-sm7 {
|
||||
width: 58.33%;
|
||||
}
|
||||
|
||||
.spfxappdev-sm8 {
|
||||
width: 66.66%;
|
||||
}
|
||||
|
||||
.spfxappdev-sm9 {
|
||||
width:75%;
|
||||
}
|
||||
|
||||
.spfxappdev-sm10 {
|
||||
width: 83.33%;
|
||||
}
|
||||
|
||||
.spfxappdev-sm11 {
|
||||
width: 91.66%;
|
||||
}
|
||||
|
||||
.spfxappdev-sm12 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.grid-header {
|
||||
background: $ms-color-themePrimary;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.grid-footer {
|
||||
margin-top: 10px
|
||||
}
|
||||
|
||||
.categories-grid {
|
||||
.spfxappdev-grid-col {
|
||||
padding: 10px 2px;
|
||||
}
|
||||
|
||||
&.spfxappdev-grid-row {
|
||||
border-bottom: solid 1px #aeaeae;
|
||||
|
||||
&:nth-child(odd) {
|
||||
background: $ms-color-themeLighter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.iframe-dialog,
|
||||
.categories-dialog {
|
||||
.ms-Dialog-content .ms-Dialog-header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.iframe-dialog {
|
||||
iframe {
|
||||
border: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ql-editor[contenteditable='true'] {
|
||||
border: solid 1px $ms-color-themeDarker !important;
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
button {
|
||||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-popup-close-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip-container {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.change-position-popup {
|
||||
text-align: center;
|
||||
|
||||
label {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.info-tooltip {
|
||||
font-size: 10px;
|
||||
padding-left: 4px;
|
||||
vertical-align: middle;
|
||||
cursor:help;
|
||||
}
|
||||
|
||||
.wp-settings-info {
|
||||
font-weight: 600;
|
||||
padding: 20px 0;
|
||||
color: $ms-color-themeDarker;
|
||||
}
|
||||
|
||||
.category-messagebar {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.CanvasControlToolbar {
|
||||
z-index: 2000 !important;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,582 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import styles from './Map.module.scss';
|
||||
import { IMapProps, IMarker, IMarkerCategory, IMarkerIcon, emptyMarkerItem } from './IMapProps';
|
||||
import { cloneDeep } from '@microsoft/sp-lodash-subset';
|
||||
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import "react-leaflet-markercluster/dist/styles.min.css";
|
||||
import * as L from 'leaflet';
|
||||
import { ContextualMenu, IContextualMenuItem, Panel, Dialog, IPanelProps, DefaultButton, PanelType, DialogType, DialogContent, Label, Separator, PrimaryButton } from 'office-ui-fabric-react';
|
||||
import { isset, isNullOrEmpty, getDeepOrDefault, cssClasses } from '@spfxappdev/utility';
|
||||
import '@spfxappdev/utility/lib/extensions/StringExtensions';
|
||||
import '@spfxappdev/utility/lib/extensions/ArrayExtensions';
|
||||
import { DisplayMode } from '@microsoft/sp-core-library';
|
||||
import { RichText } from "@pnp/spfx-controls-react/lib/RichText";
|
||||
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
|
||||
import AddOrEditPanel from './AddOrEditPanel';
|
||||
import { isFunction } from 'lodash';
|
||||
import { MarkerIcon } from './MarkerIcon';
|
||||
import MarkerClusterGroup from 'react-leaflet-markercluster';
|
||||
import * as strings from 'MapWebPartStrings';
|
||||
import SearchPlugin from './plugins/search/SearchPlugin';
|
||||
import LegendPlugin from './plugins/legend/LegendPlugin';
|
||||
|
||||
interface IMapState {
|
||||
markerItems: IMarker[];
|
||||
markerCategories: IMarkerCategory[];
|
||||
rightMouseTarget?: any;
|
||||
showAddOrEditMarkerPanel: boolean;
|
||||
currentMarker?: IMarker;
|
||||
showClickContent: boolean;
|
||||
changePositionMarkerId: string;
|
||||
}
|
||||
|
||||
export default class Map extends React.Component<IMapProps, IMapState> {
|
||||
|
||||
public state: IMapState = {
|
||||
markerItems: cloneDeep(this.props.markerItems),
|
||||
markerCategories: cloneDeep(this.props.markerCategories),
|
||||
showAddOrEditMarkerPanel: false,
|
||||
showClickContent: false,
|
||||
changePositionMarkerId: '-1'
|
||||
};
|
||||
|
||||
private allCatagories: Record<string, IMarkerCategory> = {};
|
||||
|
||||
private menuItems: IContextualMenuItem[] = [
|
||||
{
|
||||
key: 'newItem',
|
||||
text: strings.ContextMenuAddNewMarkerLabel,
|
||||
onClick: () => {
|
||||
this.onCreateNewMarkerContextMenuItemClick();
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'setStartView',
|
||||
text: strings.ContextMenuSetStartPositionLabel,
|
||||
onClick: () => {
|
||||
this.onSetStartView();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
private map: L.Map = null;
|
||||
|
||||
private allLeafletMarker: Record<string, L.Marker> = {};
|
||||
|
||||
private lastLatLngRightClickPosition: L.LatLng;
|
||||
|
||||
|
||||
constructor(props: IMapProps) {
|
||||
super(props);
|
||||
this.setAllCatagoriesDictionary();
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IMapProps>, prevState: Readonly<IMapState>, snapshot?: any): void {
|
||||
|
||||
if(!JSON.stringify(prevProps.markerCategories).Equals(JSON.stringify(this.props.markerCategories))) {
|
||||
this.setState({
|
||||
markerCategories: cloneDeep(this.props.markerCategories)
|
||||
}, () => {
|
||||
this.setAllCatagoriesDictionary();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IMapProps> {
|
||||
|
||||
this.allLeafletMarker = {};
|
||||
// const isZoomControlEnabled: boolean = this.props.isEditMode ? true : getDeepOrDefault<boolean>(this.props, "plugins.zoomControl", true);
|
||||
const isZoomControlEnabled: boolean = getDeepOrDefault<boolean>(this.props, "plugins.zoomControl", true);
|
||||
const isScrollWheelZoomEnabled: boolean = this.props.isEditMode ? true : getDeepOrDefault<boolean>(this.props, "scrollWheelZoom", true);
|
||||
const isDraggingEnabled: boolean = this.props.isEditMode ? true : getDeepOrDefault<boolean>(this.props, "dragging", true);
|
||||
//
|
||||
return (
|
||||
<div className={styles.map}>
|
||||
{(this.props.isEditMode || (!this.props.isEditMode && !isNullOrEmpty(this.props.title))) &&
|
||||
<WebPartTitle displayMode={this.props.isEditMode?DisplayMode.Edit:DisplayMode.Read}
|
||||
title={this.props.title}
|
||||
updateProperty={this.props.onTitleUpdate} />
|
||||
}
|
||||
|
||||
<MapContainer
|
||||
className={this.props.isEditMode ? "edit-mode" : "display-mode"}
|
||||
zoomControl={isZoomControlEnabled}
|
||||
center={this.props.center}
|
||||
zoom={this.props.zoom}
|
||||
maxZoom={this.props.maxZoom}
|
||||
minZoom={this.props.minZoom}
|
||||
scrollWheelZoom={isScrollWheelZoomEnabled}
|
||||
touchZoom={isScrollWheelZoomEnabled}
|
||||
doubleClickZoom={isScrollWheelZoomEnabled}
|
||||
dragging={isDraggingEnabled}
|
||||
whenCreated={(map: L.Map) => {
|
||||
map.on("contextmenu", (ev: L.LeafletEvent) => {
|
||||
|
||||
if (!this.props.isEditMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastLatLngRightClickPosition = (ev as any).latlng;
|
||||
|
||||
this.setState({
|
||||
rightMouseTarget: {
|
||||
x: ((ev as any).originalEvent as MouseEvent).clientX,
|
||||
y: ((ev as any).originalEvent as MouseEvent).clientY
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
this.map = map;
|
||||
}
|
||||
}
|
||||
style={{height: isNullOrEmpty(this.props.height) ? "400px" : `${this.props.height}px`}}
|
||||
>
|
||||
<TileLayer
|
||||
attribution={`<a href="https://spfx-app.dev/">SPFx-App.dev</a> | ${this.props.tileLayerAttribution}`}
|
||||
url={this.props.tileLayerUrl}
|
||||
/>
|
||||
|
||||
|
||||
{this.props.plugins.markercluster &&
|
||||
<MarkerClusterGroup>
|
||||
{this.renderMarker()}
|
||||
</MarkerClusterGroup>
|
||||
}
|
||||
|
||||
{!this.props.plugins.markercluster &&
|
||||
this.renderMarker()
|
||||
}
|
||||
|
||||
{this.renderSearchBox()}
|
||||
{this.renderLegend(isZoomControlEnabled)}
|
||||
</MapContainer>
|
||||
|
||||
|
||||
|
||||
{this.props.isEditMode &&
|
||||
<ContextualMenu
|
||||
items={this.menuItems}
|
||||
hidden={typeof this.state.rightMouseTarget == "undefined"}
|
||||
target={this.state.rightMouseTarget}
|
||||
onItemClick={() => {
|
||||
|
||||
}}
|
||||
onDismiss={() => {
|
||||
this.setState({
|
||||
rightMouseTarget: undefined
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
{this.showAddOrEditMarkerPanel()}
|
||||
{this.showClickContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderMarker(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{this.state.markerItems.map((marker: IMarker, index: number): JSX.Element => {
|
||||
const useCategory: boolean = isset(this.allCatagories[marker.categoryId]);
|
||||
const markerCategory: IMarkerCategory = useCategory ? this.allCatagories[marker.categoryId] : null;
|
||||
const popupText: string = !useCategory ? marker.popuptext : isNullOrEmpty(markerCategory.popuptext) ? markerCategory.name : markerCategory.popuptext;
|
||||
const isDraggable: boolean = marker.id.Equals(this.state.changePositionMarkerId);
|
||||
|
||||
return (
|
||||
<Marker
|
||||
draggable={isDraggable}
|
||||
position={[marker.latitude, marker.longitude]}
|
||||
key={`marker_${marker.id}`}
|
||||
icon={this.createIcon(marker, markerCategory)}
|
||||
ref={(ref: L.Marker) => {
|
||||
|
||||
if(!isset(ref)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.allLeafletMarker[marker.id] = ref;
|
||||
|
||||
if(this.state.changePositionMarkerId.Equals(marker.id)) {
|
||||
setTimeout(() => {
|
||||
ref.openPopup();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
}}
|
||||
eventHandlers={
|
||||
{
|
||||
|
||||
click: (ev: L.LeafletMouseEvent) => {
|
||||
|
||||
if(this.state.changePositionMarkerId.length >= 32) {
|
||||
return;
|
||||
}
|
||||
|
||||
let showEditPanel: boolean = this.props.isEditMode;
|
||||
|
||||
this.setState({
|
||||
currentMarker: marker,
|
||||
showClickContent: !showEditPanel,
|
||||
showAddOrEditMarkerPanel: showEditPanel
|
||||
});
|
||||
},
|
||||
mouseover: (ev: L.LeafletMouseEvent) => {
|
||||
|
||||
if(!this.props.showPopUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.state.changePositionMarkerId.length >= 32) {
|
||||
return;
|
||||
}
|
||||
|
||||
(ev.target as any).openPopup();
|
||||
},
|
||||
mouseout: (ev: L.LeafletMouseEvent) => {
|
||||
|
||||
if(!this.props.showPopUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.state.changePositionMarkerId.length >= 32) {
|
||||
return;
|
||||
}
|
||||
|
||||
(ev.target as any).closePopup();
|
||||
},
|
||||
dragend: (ev: L.DragEndEvent) => {
|
||||
const currentMarker = (ev.target as any);
|
||||
|
||||
setTimeout(() => {
|
||||
if(isset(marker)) {
|
||||
currentMarker.openPopup();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
{this.props.showPopUp && this.state.changePositionMarkerId != marker.id && !isNullOrEmpty(popupText) &&
|
||||
<Popup>
|
||||
{popupText}
|
||||
</Popup>
|
||||
}
|
||||
|
||||
{this.state.changePositionMarkerId == marker.id &&
|
||||
<Popup>
|
||||
<div className="change-position-popup">
|
||||
<Label>{strings.LabelChangePosition}</Label>
|
||||
<Separator />
|
||||
<PrimaryButton
|
||||
text={strings.SaveLabel}
|
||||
onClick={() => {
|
||||
|
||||
const currentMarker = this.allLeafletMarker[marker.id];
|
||||
const latLng: L.LatLng = currentMarker.getLatLng();
|
||||
|
||||
this.state.markerItems[index].latitude = latLng.lat;
|
||||
this.state.markerItems[index].longitude = latLng.lng;
|
||||
|
||||
currentMarker.dragging.disable();
|
||||
|
||||
this.setState({
|
||||
changePositionMarkerId: "-1",
|
||||
showAddOrEditMarkerPanel: true,
|
||||
markerItems: this.state.markerItems
|
||||
});
|
||||
|
||||
if(isFunction(this.props.onMarkerCollectionChanged)) {
|
||||
this.props.onMarkerCollectionChanged(this.state.markerItems);
|
||||
}
|
||||
|
||||
}}
|
||||
/>
|
||||
<DefaultButton
|
||||
text={strings.CancelLabel}
|
||||
onClick={() => {
|
||||
|
||||
const currentMarker = this.allLeafletMarker[marker.id];
|
||||
currentMarker.setLatLng([marker.latitude, marker.longitude]);
|
||||
|
||||
currentMarker.dragging.disable();
|
||||
|
||||
this.setState({
|
||||
changePositionMarkerId: "-1",
|
||||
showAddOrEditMarkerPanel: true
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Popup>
|
||||
}
|
||||
</Marker>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderLegend(isZoomControlEnabled: boolean): JSX.Element {
|
||||
if(!getDeepOrDefault<boolean>(this.props, "plugins.legend", false) || isNullOrEmpty(this.state.markerCategories)) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
return (
|
||||
<LegendPlugin isZoomControlVisible={isZoomControlEnabled} markerCategories={this.state.markerCategories} />
|
||||
);
|
||||
}
|
||||
|
||||
private renderSearchBox(): JSX.Element {
|
||||
|
||||
if(!this.props.plugins.searchBox) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchPlugin onLocationSelected={(lat: number, lon: number) => {
|
||||
this.map.setView([lat, lon], this.props.maxZoom > 18 ? 18 : this.props.maxZoom);
|
||||
|
||||
const defaultRadius = 12;
|
||||
const circleOptions = {
|
||||
inner: {
|
||||
color: '#136AEC',
|
||||
fillColor: '#2A93EE',
|
||||
fillOpacity: 1,
|
||||
weight: 1.5,
|
||||
opacity: 0.7,
|
||||
radius: defaultRadius / 4
|
||||
},
|
||||
outer: {
|
||||
color: "#136AEC",
|
||||
fillColor: "#136AEC",
|
||||
fillOpacity: 0.15,
|
||||
opacity: 0.3,
|
||||
weight: 1,
|
||||
radius: defaultRadius
|
||||
}
|
||||
};
|
||||
|
||||
L.circle([lat, lon], circleOptions.outer).addTo(this.map);
|
||||
L.circle([lat, lon], circleOptions.inner).addTo(this.map);
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
private showClickContent(): JSX.Element {
|
||||
if(!this.state.showClickContent || isNullOrEmpty(this.state.currentMarker)) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
if(this.state.currentMarker.type == "None") {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
if(this.state.currentMarker.type == "Url" && this.state.currentMarker.markerClickProps.url.target != "embedded") {
|
||||
window.open(this.state.currentMarker.markerClickProps.url.href, this.state.currentMarker.markerClickProps.url.target);
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
if (this.state.currentMarker.type == "Panel") {
|
||||
return (<Panel
|
||||
type={PanelType.medium}
|
||||
isOpen={true}
|
||||
onDismiss={() => { this.onContentPanelOrDialogDismiss(); }}
|
||||
headerText={this.state.currentMarker.markerClickProps.content.headerText}
|
||||
closeButtonAriaLabel="Close"
|
||||
onRenderFooterContent={(props: IPanelProps) => {
|
||||
return (<div>
|
||||
<DefaultButton onClick={() => { this.onContentPanelOrDialogDismiss(); }}>Close</DefaultButton>
|
||||
</div>);
|
||||
}}
|
||||
// Stretch panel content to fill the available height so the footer is positioned
|
||||
// at the bottom of the page
|
||||
isFooterAtBottom={true}
|
||||
>
|
||||
<RichText isEditMode={false} value={this.state.currentMarker.markerClickProps.content.html} />
|
||||
|
||||
</Panel>);
|
||||
}
|
||||
|
||||
const width: number = window.innerWidth - 100;
|
||||
const height: number = window.innerHeight - 300;
|
||||
let dialogWidth = 900;
|
||||
|
||||
if(width < dialogWidth || this.state.currentMarker.type == "Url") {
|
||||
dialogWidth = width;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
hidden={false}
|
||||
onDismiss={() => { this.onContentPanelOrDialogDismiss(); }}
|
||||
dialogContentProps={{
|
||||
title: this.state.currentMarker.markerClickProps.content.headerText,
|
||||
type: DialogType.close
|
||||
}}
|
||||
minWidth={dialogWidth}
|
||||
modalProps={{
|
||||
isBlocking: true,
|
||||
className: "iframe-dialog",
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
{this.state.currentMarker.type == "Dialog" && <RichText isEditMode={false} value={this.state.currentMarker.markerClickProps.content.html} />}
|
||||
{this.state.currentMarker.type == "Url" &&
|
||||
<div style={{height: `${height}px`}}>
|
||||
<iframe src={this.state.currentMarker.markerClickProps.url.href}></iframe>
|
||||
</div>
|
||||
}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private showAddOrEditMarkerPanel(): JSX.Element {
|
||||
|
||||
if(!this.state.showAddOrEditMarkerPanel || !this.props.isEditMode) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
return (
|
||||
<AddOrEditPanel
|
||||
markerCategories={this.state.markerCategories}
|
||||
markerItem={this.state.currentMarker}
|
||||
onDismiss={() => { this.onConfigPanelDismiss(); }}
|
||||
onDeleteMarker={(markerItem: IMarker) => {
|
||||
|
||||
const markerIndex: number = this.state.markerItems.IndexOf(m => m.id == markerItem.id);
|
||||
|
||||
|
||||
this.state.markerItems.RemoveAt(markerIndex);
|
||||
|
||||
if(isFunction(this.props.onMarkerCollectionChanged)) {
|
||||
this.props.onMarkerCollectionChanged(this.state.markerItems);
|
||||
}
|
||||
|
||||
this.state.rightMouseTarget = undefined;
|
||||
this.onConfigPanelDismiss();
|
||||
|
||||
|
||||
}}
|
||||
onChangePositionClick={(markerItem: IMarker) => {
|
||||
|
||||
this.setState({
|
||||
changePositionMarkerId: markerItem.id,
|
||||
showAddOrEditMarkerPanel: false
|
||||
});
|
||||
|
||||
|
||||
|
||||
}}
|
||||
onMarkerCategoriesChanged={(markerCategories: IMarkerCategory[]) => {
|
||||
this.state.markerCategories = markerCategories;
|
||||
|
||||
if(isFunction(this.props.onMarkerCategoriesChanged)) {
|
||||
this.props.onMarkerCategoriesChanged(markerCategories);
|
||||
}
|
||||
|
||||
this.setAllCatagoriesDictionary();
|
||||
|
||||
this.setState({
|
||||
markerCategories: markerCategories
|
||||
});
|
||||
}}
|
||||
onMarkerChanged={(markerItem: IMarker, isNewMarker: boolean) => {
|
||||
|
||||
if(isNewMarker) {
|
||||
this.state.markerItems.push(markerItem);
|
||||
}
|
||||
else {
|
||||
const markerIndex: number = this.state.markerItems.IndexOf(m => m.id == markerItem.id);
|
||||
|
||||
if(markerIndex >= 0) {
|
||||
this.state.markerItems[markerIndex] = markerItem;
|
||||
}
|
||||
}
|
||||
|
||||
this.state.rightMouseTarget = undefined;
|
||||
|
||||
if(isFunction(this.props.onMarkerCollectionChanged)) {
|
||||
this.props.onMarkerCollectionChanged(this.state.markerItems);
|
||||
}
|
||||
|
||||
this.onConfigPanelDismiss();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private onConfigPanelDismiss(): void {
|
||||
this.setState({
|
||||
showAddOrEditMarkerPanel: false,
|
||||
currentMarker: null
|
||||
});
|
||||
}
|
||||
|
||||
private onContentPanelOrDialogDismiss(): void {
|
||||
this.setState({
|
||||
showClickContent: false,
|
||||
currentMarker: null
|
||||
});
|
||||
}
|
||||
|
||||
private createIcon(marker: IMarker, markerCategory: IMarkerCategory ): L.Icon {
|
||||
const markerIcon = new L.Icon({
|
||||
iconAnchor: [13, 36],
|
||||
popupAnchor: [0, -36],
|
||||
shadowUrl: null,
|
||||
shadowSize: null,
|
||||
shadowAnchor: null,
|
||||
iconSize: new L.Point(27, 36),
|
||||
className: cssClasses('leaflet-div-icon', `marker-type-${marker.type.toLowerCase()}`)
|
||||
});
|
||||
|
||||
markerIcon.createIcon = (oldIcon: HTMLElement) => {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.classList.add("leaflet-marker-icon");
|
||||
wrapper.classList.add(`marker-type-${marker.type.toLowerCase()}`);
|
||||
|
||||
wrapper.dataset.markerid = marker.id;
|
||||
|
||||
wrapper.style.marginLeft = (markerIcon.options.iconAnchor[0] * -1) + "px";
|
||||
wrapper.style.marginTop = (markerIcon.options.iconAnchor[1] * -1) + "px";
|
||||
const iconProperties: IMarkerIcon = isNullOrEmpty(markerCategory) ? marker.iconProperties : markerCategory.iconProperties;
|
||||
ReactDom.render(<MarkerIcon {...iconProperties} />, wrapper);
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
return markerIcon as any as L.Icon;
|
||||
}
|
||||
|
||||
private onCreateNewMarkerContextMenuItemClick(): void {
|
||||
this.state.currentMarker = cloneDeep(emptyMarkerItem);
|
||||
this.state.currentMarker.latitude = this.lastLatLngRightClickPosition.lat;
|
||||
this.state.currentMarker.longitude = this.lastLatLngRightClickPosition.lng;
|
||||
this.state.showAddOrEditMarkerPanel = true;
|
||||
|
||||
this.setState({...this.state});
|
||||
}
|
||||
|
||||
private onSetStartView(): void {
|
||||
|
||||
if(isFunction(this.props.onStartViewSet)) {
|
||||
const zoom: number = this.map.getZoom();
|
||||
const latLng: L.LatLng = this.map.getCenter();
|
||||
this.props.onStartViewSet(zoom, latLng.lat, latLng.lng);
|
||||
}
|
||||
}
|
||||
|
||||
private setAllCatagoriesDictionary(): void {
|
||||
this.allCatagories = {};
|
||||
this.state.markerCategories.forEach((category: IMarkerCategory) => {
|
||||
this.allCatagories[category.id] = category;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { Icon } from 'office-ui-fabric-react';
|
||||
import * as React from 'react';
|
||||
import { IMarkerIcon } from './IMapProps';
|
||||
|
||||
|
||||
export const MarkerIcon: React.FunctionComponent<IMarkerIcon> = (iconProperties): JSX.Element => {
|
||||
|
||||
const iconColor: React.CSSProperties = {
|
||||
color: iconProperties.iconColor.slice()
|
||||
};
|
||||
|
||||
return (
|
||||
<span>
|
||||
<svg height="36px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" fill={iconProperties.markerColor}>
|
||||
{/* Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) */}
|
||||
<path d="M172.268 501.67C26.97 291.031 0 269.413 0 192 0 85.961 85.961 0 192 0s192 85.961 192 192c0 77.413-26.97 99.031-172.268 309.67-9.535 13.774-29.93 13.773-39.464 0z"/>
|
||||
</svg>
|
||||
<span className="map-icon" style={iconColor}><Icon iconName={iconProperties.iconName} /></span>
|
||||
</span>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
|
||||
|
||||
.map-plugin-legend {
|
||||
display: block;
|
||||
|
||||
|
||||
&-bottom {
|
||||
top: 80px;
|
||||
}
|
||||
|
||||
button {
|
||||
outline: none;
|
||||
border-radius: 2px;
|
||||
height: 32px;
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
font-size: 1.4em;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.map-legend {
|
||||
margin: 10px;
|
||||
|
||||
&-title {
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
&-marker-item {
|
||||
display: flex;
|
||||
padding: 5px 10px;
|
||||
align-items: center;
|
||||
|
||||
&-icon {
|
||||
width: 35px;
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 0 10px;
|
||||
padding: 1px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-marker-wrapper {
|
||||
position: relative;
|
||||
height: 36px;
|
||||
// float: left;
|
||||
|
||||
& > div {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import { Callout } from '@microsoft/office-ui-fabric-react-bundle';
|
||||
import { randomString } from '@spfxappdev/utility';
|
||||
import * as strings from 'MapWebPartStrings';
|
||||
import { Icon, Label, Separator } from 'office-ui-fabric-react';
|
||||
import * as React from 'react';
|
||||
import { IMarkerCategory } from '../../IMapProps';
|
||||
import { MarkerIcon } from '../../MarkerIcon';
|
||||
import styles from './LegendPlugin.module.scss';
|
||||
|
||||
|
||||
|
||||
export interface ILegendPluginProps {
|
||||
markerCategories: IMarkerCategory[];
|
||||
isZoomControlVisible: boolean;
|
||||
}
|
||||
|
||||
interface ILegendPluginState {
|
||||
isCalloutVisible: boolean;
|
||||
}
|
||||
|
||||
export default class LegendPlugin extends React.Component<ILegendPluginProps, ILegendPluginState> {
|
||||
|
||||
public state: ILegendPluginState = {
|
||||
isCalloutVisible: false
|
||||
};
|
||||
|
||||
private randomId: string;
|
||||
|
||||
constructor(props: ILegendPluginProps) {
|
||||
super(props);
|
||||
this.randomId = `map_legend_${randomString(6)}`;
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<ILegendPluginProps> {
|
||||
|
||||
let cssClass = styles['map-plugin-legend'];
|
||||
if(this.props.isZoomControlVisible) {
|
||||
cssClass += ' ' + styles['map-plugin-legend-bottom'];
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`leaflet-top leaflet-left ${cssClass}`}>
|
||||
<div className="leaflet-control leaflet-bar">
|
||||
<button
|
||||
type="button"
|
||||
id={this.randomId}
|
||||
onClick={() => {
|
||||
const isVisible: boolean = this.state.isCalloutVisible ? false : true;
|
||||
|
||||
this.setState({
|
||||
isCalloutVisible: isVisible
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon iconName="Info" />
|
||||
</button>
|
||||
<Callout
|
||||
target={`#${this.randomId}`}
|
||||
onDismiss={() => { this.setState({ isCalloutVisible: false }); }}
|
||||
hidden={!this.state.isCalloutVisible}>
|
||||
<div className='map-legend'>
|
||||
<Label className="map-legend-title">{strings.LegendLabel}</Label>
|
||||
<Separator />
|
||||
{this.props.markerCategories.map((cat: IMarkerCategory): JSX.Element => {
|
||||
return (
|
||||
<div key={`legend_${cat.id}`} className="map-legend-marker-item">
|
||||
<div className='map-legend-marker-item-icon'>
|
||||
<div className='map-legend-marker-wrapper'>
|
||||
<div style={{}}>
|
||||
<MarkerIcon {...cat.iconProperties} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Label>{cat.name}</Label>
|
||||
</div>);
|
||||
})}
|
||||
</div>
|
||||
</Callout>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
|
||||
|
||||
.map-plugin-search {
|
||||
display: block;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
|
||||
button {
|
||||
outline: none;
|
||||
border-radius: 2px;
|
||||
height: 32px;
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
font-size: 1.4em;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.textbox {
|
||||
display: inline-block !important;
|
||||
border-width: 0px;
|
||||
padding: 0;
|
||||
|
||||
div {
|
||||
border-width: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.suggesstion {
|
||||
display: block;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
padding: 5px 20px;
|
||||
border-bottom: solid 1px $ms-color-themeLighter;
|
||||
border-top: solid 1px $ms-color-themeLighter;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-child {
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
i {
|
||||
padding-right: 20px;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
import { isFunction, isNullOrEmpty } from '@spfxappdev/utility';
|
||||
import { Autocomplete } from '@src/components/autocomplete/Autocomplete';
|
||||
import * as strings from 'MapWebPartStrings';
|
||||
import { Icon } from 'office-ui-fabric-react';
|
||||
import * as React from 'react';
|
||||
import styles from './SearchPlugin.module.scss';
|
||||
|
||||
export interface ISearchPluginProps {
|
||||
nominatimUrl?: string;
|
||||
resultLimit?: number;
|
||||
onLocationSelected?(latitude: number, longitude: number): void;
|
||||
}
|
||||
|
||||
interface ISearchResult {
|
||||
place_id: number;
|
||||
display_name: string;
|
||||
lat: string;
|
||||
lon: string;
|
||||
}
|
||||
|
||||
interface ISearchPluginState {
|
||||
searchResult: Array<ISearchResult>;
|
||||
isSearchBoxVisible: boolean;
|
||||
}
|
||||
|
||||
export default class SearchPlugin extends React.Component<ISearchPluginProps, ISearchPluginState> {
|
||||
|
||||
public state: ISearchPluginState = {
|
||||
searchResult: [],
|
||||
isSearchBoxVisible: false
|
||||
};
|
||||
|
||||
public static defaultProps: ISearchPluginProps = {
|
||||
nominatimUrl: "https://nominatim.openstreetmap.org/search",
|
||||
resultLimit: 3
|
||||
};
|
||||
|
||||
|
||||
public render(): React.ReactElement<ISearchPluginProps> {
|
||||
return (
|
||||
<div className={styles["map-plugin-search"]}>
|
||||
{this.state.isSearchBoxVisible &&
|
||||
<Autocomplete
|
||||
className={styles['textbox']}
|
||||
onRenderSuggestions={(searchTerm: string) => {
|
||||
return this.renderSuggesstionsFlyout(searchTerm);
|
||||
}}
|
||||
onChange={async (ev: any, searchTerm: string) => {
|
||||
const result = await this.makeSearchRequest(searchTerm);
|
||||
this.setState({
|
||||
searchResult: result
|
||||
});
|
||||
}} />
|
||||
}
|
||||
|
||||
<button type="button" onClick={() => {
|
||||
const isVisible: boolean = this.state.isSearchBoxVisible ? false : true;
|
||||
|
||||
this.setState({
|
||||
isSearchBoxVisible: isVisible
|
||||
});
|
||||
}}>
|
||||
<Icon iconName="Search" />
|
||||
</button>
|
||||
</div>);
|
||||
}
|
||||
|
||||
private renderSuggesstionsFlyout(searchTerm: string): JSX.Element {
|
||||
|
||||
const results = this.state.searchResult;
|
||||
|
||||
if(isNullOrEmpty(results)) {
|
||||
return (<>
|
||||
{strings.NoSearchResultsLabel}
|
||||
</>);
|
||||
}
|
||||
|
||||
return (<div className={styles["suggesstion"]}>
|
||||
{results.map((location: ISearchResult, index: number): JSX.Element => {
|
||||
return (<div
|
||||
key={`Icon_${index}_${location.place_id}`}
|
||||
onClick={() => {
|
||||
|
||||
if(isFunction(this.props.onLocationSelected)) {
|
||||
this.props.onLocationSelected(parseFloat(location.lat), parseFloat(location.lon));
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isSearchBoxVisible: false
|
||||
});
|
||||
}}
|
||||
className={styles["suggesstion-item"]}>
|
||||
{location.display_name}
|
||||
</div>);
|
||||
})}
|
||||
</div>);
|
||||
|
||||
}
|
||||
|
||||
private async makeSearchRequest(searchTerm: string): Promise<ISearchResult[]> {
|
||||
|
||||
const response = await fetch(`${this.props.nominatimUrl}?format=json&limit=${this.props.resultLimit}&q=${searchTerm}`);
|
||||
const responseJson = await response.json();
|
||||
return responseJson;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"WebPartPropertyGroupMapSettings": "General settings",
|
||||
"WebPartPropertyGroupTileLayerSettings": "Tile Layer",
|
||||
"WebPartPropertyGroupPlugins": "Plugins/Controls",
|
||||
"WebPartPropertyGroupCategories": "Categories",
|
||||
"WebPartPropertyGroupAbout": "About",
|
||||
"WebPartPropertyPluginSearchboxLabel": "Show Searchbox",
|
||||
"WebPartPropertyPluginMarkerClusterLabel": "Enable marker cluster",
|
||||
"WebPartPropertyPluginLegendLabel": "Show legend",
|
||||
"WebPartPropertyButtonManageCategories": "Manage categories",
|
||||
"WebPartPropertyPluginZoomControlLabel": "Show zoom control",
|
||||
"WebPartPropertyScrollWheelZoomLabel": "Enable zoom on mouse wheel/touch (only in display mode)",
|
||||
"WebPartPropertyMapDraggingLabel": "Enable dragging in map (only in display mode)",
|
||||
"WebPartPropertyShowPopUpLabel": "Show tooltip when hovering the marker",
|
||||
"WebPartPropertySettingsInfoLabel": "Most of these settings take effect only after a page refresh and only in display mode",
|
||||
"WebPartPropertyMinZoomLabel": "Minimum zoom level (zoom out)",
|
||||
"WebPartPropertyMaxZoomLabel": "Maximum zoom level (zoom in, depends on Tile layer)",
|
||||
"WebPartPropertyHeightLabel": "Map height (in px)",
|
||||
"WebPartPropertyTileLayerUrlInformationLabel": "In this section you can change the tile layer and attribution. You can find more tile layers e.g. <a href='https://leaflet-extras.github.io/leaflet-providers/preview/' data-interception='off' target='_blank'>here</a>",
|
||||
"WebPartPropertyTileLayerUrlLabel": "Tile layer URL",
|
||||
"WebPartPropertyTileLayerAttributionLabel": "Tile layer attribution",
|
||||
"ContextMenuAddNewMarkerLabel": "Add a new marker here",
|
||||
"ContextMenuSetStartPositionLabel": "Make this view as start position",
|
||||
"LabelChangePosition": "Change position",
|
||||
"SaveLabel": "Save",
|
||||
"CancelLabel": "Cancel",
|
||||
"CloseLabel": "Close",
|
||||
"LegendLabel": "Legend",
|
||||
"ChoiceGroupPanelLabel": "Panel",
|
||||
"ChoiceGroupDialogLabel": "Dialog",
|
||||
"ChoiceGroupUrlLabel": "Url",
|
||||
"ChoiceGroupNoneLabel": "None (not clickable)",
|
||||
"ChoiceGroupTargetSelfLabel": "Open in same window",
|
||||
"ChoiceGroupTargetBlankLabel": "Open in new window",
|
||||
"ChoiceGroupTargetEmbeddedLabel": "Embedded (Dialog/iFrame)",
|
||||
"PanelHeaderNewLabel": "Create new marker",
|
||||
"PanelHeaderEditLabel": "Edit marker",
|
||||
"LabelCategory": "Category",
|
||||
"LabelManage": "Manage",
|
||||
"PlaceholderSelectACategory": "Select a category",
|
||||
"LabelMarkerType": "Type of marker (on click)",
|
||||
"DeleteLabel": "Delete",
|
||||
"ChangePositionLabel": "Change Position",
|
||||
"LabelMarkerColor": "Marker color",
|
||||
"LabelIcon": "Icon",
|
||||
"LabelLeaveEmpty": "Leave blank to show no icon",
|
||||
"LabelIconColor": "Icon color",
|
||||
"LabelTooltip": "Tooltip text",
|
||||
"TooltipInfoCategory": "For categories, the name of the category is displayed when hovering over the marker (if enabled in the web part settings). This text can be overwritten here.",
|
||||
"TooltipInfo": "When hovering over the marker, this text is displayed (if enabled in the webpart settings)",
|
||||
"LabelLeaveEmptyTooltip": "Leave blank to show no tooltip",
|
||||
"LabelPreview": "Preview",
|
||||
"LabelPanelHeader": "Panel Header",
|
||||
"LabelDialogHeader": "Dialog Header",
|
||||
"LabelContent": "Content",
|
||||
"LabelUrl": "Url",
|
||||
"DialogTitleManageCategories": "Manage categories",
|
||||
"InfoTextNoCategories": "Currently there are no categories.",
|
||||
"InfoTextCategories": "Markers can be assigned to categories. If you change the category, this change will be applied to all markers assigned to this category. If you delete a category, the default configuration is used.",
|
||||
"LabelCategoryHeaderName": "Name",
|
||||
"AddLabel": "Add",
|
||||
"NoSearchResultsLabel": "No Results"
|
||||
}
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
declare interface IMapWebPartStrings {
|
||||
WebPartPropertyGroupMapSettings: string;
|
||||
WebPartPropertyGroupTileLayerSettings: string;
|
||||
WebPartPropertyGroupPlugins: string;
|
||||
WebPartPropertyGroupCategories: string;
|
||||
WebPartPropertyGroupAbout: string;
|
||||
WebPartPropertyPluginSearchboxLabel: string;
|
||||
WebPartPropertyPluginMarkerClusterLabel: string;
|
||||
WebPartPropertyPluginLegendLabel: string;
|
||||
WebPartPropertyButtonManageCategories: string;
|
||||
WebPartPropertyPluginZoomControlLabel: string;
|
||||
WebPartPropertyScrollWheelZoomLabel: string;
|
||||
WebPartPropertyMapDraggingLabel: string;
|
||||
WebPartPropertyShowPopUpLabel: string;
|
||||
WebPartPropertySettingsInfoLabel: string;
|
||||
WebPartPropertyMinZoomLabel: string;
|
||||
WebPartPropertyMaxZoomLabel: string;
|
||||
WebPartPropertyHeightLabel: string;
|
||||
WebPartPropertyTileLayerUrlInformationLabel: string;
|
||||
WebPartPropertyTileLayerUrlLabel: string;
|
||||
WebPartPropertyTileLayerAttributionLabel: string;
|
||||
ContextMenuAddNewMarkerLabel: string;
|
||||
ContextMenuSetStartPositionLabel: string;
|
||||
LabelChangePosition: string;
|
||||
SaveLabel: string;
|
||||
CancelLabel: string;
|
||||
CloseLabel: string;
|
||||
LegendLabel: string;
|
||||
ChoiceGroupPanelLabel: string;
|
||||
ChoiceGroupDialogLabel: string;
|
||||
ChoiceGroupUrlLabel: string;
|
||||
ChoiceGroupNoneLabel: string;
|
||||
ChoiceGroupTargetSelfLabel: string;
|
||||
ChoiceGroupTargetBlankLabel: string;
|
||||
ChoiceGroupTargetEmbeddedLabel: string;
|
||||
PanelHeaderNewLabel: string;
|
||||
PanelHeaderEditLabel: string;
|
||||
LabelCategory: string;
|
||||
LabelManage: string;
|
||||
PlaceholderSelectACategory: string;
|
||||
LabelMarkerType: string;
|
||||
DeleteLabel: string;
|
||||
ChangePositionLabel: string;
|
||||
LabelMarkerColor: string;
|
||||
LabelIcon: string;
|
||||
LabelLeaveEmpty: string;
|
||||
LabelIconColor: string;
|
||||
LabelTooltip: string;
|
||||
TooltipInfoCategory: string;
|
||||
TooltipInfo: string;
|
||||
LabelLeaveEmptyTooltip: string;
|
||||
LabelPreview: string;
|
||||
LabelPanelHeader: string;
|
||||
LabelDialogHeader: string;
|
||||
LabelContent: string;
|
||||
LabelUrl: string;
|
||||
DialogTitleManageCategories: string;
|
||||
InfoTextNoCategories: string;
|
||||
InfoTextCategories: string;
|
||||
LabelCategoryHeaderName: string;
|
||||
AddLabel: string;
|
||||
NoSearchResultsLabel: string;
|
||||
}
|
||||
|
||||
declare module 'MapWebPartStrings' {
|
||||
const strings: IMapWebPartStrings;
|
||||
export = strings;
|
||||
}
|
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 542 B |
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"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",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@src/*": ["src/*"],
|
||||
"@webparts/*": ["src/webparts/*"]
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
|
@ -4032,9 +4032,9 @@
|
|||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"version": "2.6.8",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz",
|
||||
"integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==",
|
||||
"requires": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
}
|
||||
|
@ -4284,9 +4284,9 @@
|
|||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"version": "2.6.8",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz",
|
||||
"integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==",
|
||||
"requires": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
}
|
||||
|
@ -22902,9 +22902,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.28",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz",
|
||||
"integrity": "sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g=="
|
||||
"version": "0.7.33",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
|
||||
"integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.4.10",
|
||||
|
|
|
@ -23370,9 +23370,9 @@
|
|||
}
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.28",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz",
|
||||
"integrity": "sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g=="
|
||||
"version": "0.7.33",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
|
||||
"integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.4.10",
|
||||
|
|
|
@ -11026,8 +11026,8 @@ typedarray@^0.0.6:
|
|||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
|
||||
ua-parser-js@^0.7.9:
|
||||
version "0.7.28"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
|
||||
version "0.7.33"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532"
|
||||
|
||||
uglify-js@3.4.x:
|
||||
version "3.4.10"
|
||||
|
|
|
@ -24786,9 +24786,9 @@
|
|||
}
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.24",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.24.tgz",
|
||||
"integrity": "sha512-yo+miGzQx5gakzVK3QFfN0/L9uVhosXBBO7qmnk7c2iw1IhL212wfA3zbnI54B0obGwC/5NWub/iT9sReMx+Fw=="
|
||||
"version": "0.7.33",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
|
||||
"integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.4.10",
|
||||
|
|