Added two samples (image magnifier and youtube webpart) furthermore react-peoplepicker is upgraded to drop 1.3.0 (#310)

* first commit

* configured the property pane

* readme updated and demo gif  uploaded

* correction

* added author

* add more descriptions

* Updated to GA Version, New properties that allow to specify the number of items to display and which entities retrieve (User, SharePoint Groups, Distribution Lists, Security Groups).

* First release

* readme changed

* release date modified

* readme updated
This commit is contained in:
Giuliano De Luca 2017-10-09 10:48:46 +02:00 committed by Vesa Juvonen
parent 2e629e111d
commit 366c28a577
87 changed files with 1859 additions and 289 deletions

View File

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

View File

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

View File

@ -0,0 +1,75 @@
// 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",
"json.schemas": [
{
"fileMatch": [
"/config/config.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/configJson/schemas/config-v1.schema.json"
},
{
"fileMatch": [
"/config/copy-assets.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/copyAssets/copy-assets.schema.json"
},
{
"fileMatch": [
"/config/deploy-azure-storage.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/deployAzureStorage/deploy-azure-storage.schema.json"
},
{
"fileMatch": [
"/config/package-solution.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/packageSolution/package-solution.schema.json"
},
{
"fileMatch": [
"/config/serve.json"
],
"url": "./node_modules/@microsoft/gulp-core-build-serve/lib/serve.schema.json"
},
{
"fileMatch": [
"/config/tslint.json"
],
"url": "./node_modules/@microsoft/gulp-core-build-typescript/lib/schemas/tslint.schema.json"
},
{
"fileMatch": [
"/config/write-manifests.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/writeManifests/write-manifests.schema.json"
},
{
"fileMatch": [
"/config/configure-webpack.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/configureWebpack/configure-webpack.schema.json"
},
{
"fileMatch": [
"/config/configure-external-bundling-webpack.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/configureWebpack/configure-webpack-external-bundling.schema.json"
},
{
"fileMatch": [
"/copy-static-assets.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/copyStaticAssets/copy-static-assets.schema.json"
}
]
}

View File

@ -0,0 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.2.0",
"libraryName": "react-magnifier",
"libraryId": "1aae48ef-964a-42fa-9c99-493ddcb41b51",
"environment": "spo"
}
}

View File

@ -0,0 +1,51 @@
# Image Magnifier
## Summary
This web part allow to magnify an image, displaying a resolution more detailed through a lens.
![site page header configurator web part](./assets/spfx-react-image-magnifier.gif)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Prerequisites
> No pre-requisites
## Solution
Solution|Author(s)
--------|---------
react-image-magnifier|Giuliano De Luca ([@giuleon](https://twitter.com/giuleon) , [www.delucagiuliano.com](delucagiuliano.com))
## Version history
Version|Date|Comments
-------|----|--------
1.0|September 17, 2017|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- in the command line run:
- `npm install`
- `gulp serve`
## Features
This Web Part illustrates the following concepts on top of the SharePoint Framework:
- Image Magnifier - How obtain best resolution from an image.
- How to leverage the capabilities of the property pane.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-image-magnifier" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"image-magnifier-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/imageMagnifier/ImageMagnifierWebPart.js",
"manifest": "./src/webparts/imageMagnifier/ImageMagnifierWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"ImageMagnifierWebPartStrings": "lib/webparts/imageMagnifier/loc/{locale}.js"
}
}

View File

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

View File

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

View File

@ -0,0 +1,12 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-magnifier-client-side-solution",
"id": "1aae48ef-964a-42fa-9c99-493ddcb41b51",
"version": "1.0.0.0",
"skipFeatureDeployment": true
},
"paths": {
"zippedPackage": "solution/react-magnifier.sppkg"
}
}

View File

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

View File

@ -0,0 +1,45 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json",
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"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-case": true,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "https://publiccdn.sharepointonline.com/giuleon.sharepoint.com/cdn/spfx-image-magnifier"
}

View File

@ -0,0 +1,6 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.initialize(gulp);

View File

@ -0,0 +1,37 @@
{
"name": "react-magnifier",
"version": "1.0.0",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"author": {
"name": "Giuliano De Luca",
"url": "http://www.delucagiuliano.com"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "~1.2.0",
"@microsoft/sp-webpart-base": "~1.2.0",
"@types/react": "15.0.38",
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-test-utils": "0.14.15",
"@types/react-addons-update": "0.14.14",
"@types/react-dom": "0.14.18",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"react": "15.4.2",
"react-dom": "15.4.2"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.2.0",
"@microsoft/sp-module-interfaces": "~1.2.0",
"@microsoft/sp-webpart-workbench": "~1.2.0",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0"
}
}

View File

@ -0,0 +1,12 @@
export interface IImageMagnifierWebPartProps {
description: string;
smallImgUrl: string;
smallImgWidth: number;
smallImgHeight: number;
largeImgUrl: string;
largeImgWidth: number;
largeImgHeight: number;
cursorOffsetX: number;
cursorOffsetY: number;
size: number;
}

View File

@ -0,0 +1,35 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "307ecced-b9a6-4b72-bc02-4d7ea558345a",
"alias": "ImageMagnifierWebPart",
"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,
"preconfiguredEntries": [{
"groupId": "307ecced-b9a6-4b72-bc02-4d7ea558345a",
"group": { "default": "Under Development" },
"title": { "default": "ImageMagnifier" },
"description": { "default": "Magnify the image" },
"officeFabricIconFontName": "FileImage",
"properties": {
"description": "Configure the web part through the property pane to display the result",
"smallImgUrl": "",
"smallImgWidth": "",
"smallImgHeight": "",
"largeImgUrl": "",
"largeImgWidth": "",
"largeImgHeight": "",
"cursorOffsetX": "80",
"cursorOffsetY": "-80",
"size": "200"
}
}]
}

View File

@ -0,0 +1,86 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import * as strings from 'ImageMagnifierWebPartStrings';
import ImageMagnifier from './components/ImageMagnifier';
import { IImageMagnifierProps } from './components/IImageMagnifierProps';
import { IImageMagnifierWebPartProps } from './IImageMagnifierWebPartProps';
export default class ImageMagnifierWebPart extends BaseClientSideWebPart<IImageMagnifierWebPartProps> {
public render(): void {
const element: React.ReactElement<IImageMagnifierProps > = React.createElement(
ImageMagnifier,
{
description: this.properties.description,
smallImgUrl: this.properties.smallImgUrl,
smallImgWidth: this.properties.smallImgWidth,
smallImgHeight: this.properties.smallImgHeight,
largeImgUrl: this.properties.largeImgUrl,
largeImgWidth: this.properties.largeImgWidth,
largeImgHeight: this.properties.largeImgHeight,
cursorOffsetX: this.properties.cursorOffsetX,
cursorOffsetY: this.properties.cursorOffsetY,
size: this.properties.size
}
);
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('smallImgUrl', {
label: strings.SmallImgUrlFieldLabel
}),
PropertyPaneTextField('smallImgWidth', {
label: strings.SmallImgWidthFieldLabel
}),
PropertyPaneTextField('smallImgHeight', {
label: strings.SmallImgHeightFieldLabel
}),
PropertyPaneTextField('largeImgUrl', {
label: strings.LargeImgUrlFieldLabel
}),
PropertyPaneTextField('largeImgWidth', {
label: strings.LargeImgWidthFieldLabel
}),
PropertyPaneTextField('largeImgHeight', {
label: strings.LargeImgHeightFieldLabel
}),
PropertyPaneTextField('cursorOffsetX', {
label: strings.CursorOffsetXFieldLabel
}),
PropertyPaneTextField('cursorOffsetY', {
label: strings.CursorOffsetYFieldLabel
}),
PropertyPaneTextField('size', {
label: strings.SizeFieldLabel
}),
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,12 @@
export interface IImageMagnifierProps {
description: string;
smallImgUrl: string;
smallImgWidth: number;
smallImgHeight: number;
largeImgUrl: string;
largeImgWidth: number;
largeImgHeight: number;
cursorOffsetX: number;
cursorOffsetY: number;
size: number;
}

View File

@ -0,0 +1,52 @@
.imageMagnifier {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
padding: 20px;
}
.listItem {
max-width: 715px;
margin: 5px auto 5px auto;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: #0078d7;
border-color: #0078d7;
color: #ffffff;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
font-weight: 400;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: 600;
font-size: 14px;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,63 @@
import * as React from 'react';
import styles from './ImageMagnifier.module.scss';
import { IImageMagnifierProps } from './IImageMagnifierProps';
import { escape } from '@microsoft/sp-lodash-subset';
import { ImageMagnifierLens } from './ImageMagnifierLens/ImageMagnifierLens';
export default class ImageMagnifier extends React.Component<IImageMagnifierProps, {}> {
public render(): React.ReactElement<IImageMagnifierProps> {
let displayState = null;
debugger;
if (
this.props.smallImgUrl != "" &&
this.props.smallImgWidth.toString() != "" &&
this.props.smallImgHeight.toString() != "" &&
this.props.largeImgUrl != "" &&
this.props.largeImgWidth.toString() != "" &&
this.props.largeImgHeight.toString() != "" &&
this.props.cursorOffsetX.toString() != "" &&
this.props.cursorOffsetY.toString() != "" &&
this.props.size.toString() != "") {
displayState = 1;
} else {
displayState = 0;
}
if(displayState === 1) {
return (
<div className={styles.imageMagnifier}>
<div className={styles.container}>
<ImageMagnifierLens
image={{
src: this.props.smallImgUrl,
width: Number(this.props.smallImgWidth),
height: Number(this.props.smallImgHeight)
}}
zoomImage={{
src: this.props.largeImgUrl,
width: Number(this.props.largeImgWidth),
height: Number(this.props.largeImgHeight)
}}
cursorOffset={{ x: Number(this.props.cursorOffsetX), y: Number(this.props.cursorOffsetY) }}
size={ this.props.size }
/>
</div>
</div>
);
}
else {
return (
<div className={styles.imageMagnifier}>
<div className={styles.container}>
<div className={`ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
<div className="ms-Grid-col ms-lg10 ms-xl8 ms-xlPush2 ms-lgPush1">
<span className="ms-font-xl ms-fontColor-white">Image Magnifier Web Part</span>
<p className="ms-font-l ms-fontColor-white">{escape(this.props.description)}</p>
</div>
</div>
</div>
</div>
);
}
}
}

View File

@ -0,0 +1,26 @@
interface IImageMagnifierLensProps {
// the size of the magnifier window
size?: number,
// the offset of the zoom bubble from the cursor
cursorOffset?:({
x?: number,
y?: number
}),
// the size of the non-zoomed-in image
image?: ({
src?: string,
width?: number,
height?: number
}),
// the size of the zoomed-in image
zoomImage?: ({
src?: string,
width?: number,
height?: number
})
}
export default IImageMagnifierLensProps;

View File

@ -0,0 +1,8 @@
interface IImageMagnifierLensState {
x: number,
y: number,
offsetX: number,
offsetY: number
}
export default IImageMagnifierLensState;

View File

@ -0,0 +1,38 @@
interface IMagnifierProps {
// the size of the magnifier window
size?: number,
// x position on screen
x?: number,
// y position on screen
y?: number,
// x position relative to the image
offsetX?: number,
// y position relative to the image
offsetY?: number,
// the offset of the zoom bubble from the cursor
cursorOffset?: ({
x?: number,
y?: number
}),
// the size of the non-zoomed-in image
smallImage?: ({
src?: string,
width?: number,
height?: number
}),
// the size of the zoomed-in image
zoomImage?: ({
src?: string,
width?: number,
height?: number
})
}
export default IMagnifierProps;

View File

@ -0,0 +1,6 @@
interface IOffset {
x: number,
y: number
}
export default IOffset;

View File

@ -0,0 +1,116 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import MagnifierProps from './IMagnifierProps';
import ImageMagnifierLensProps from './IImageMagnifierLensProps';
import ImageMagnifierLensState from './IImageMagnifierLensState';
import IOffset from './IOffset';
export class Magnifier extends React.Component<MagnifierProps, {}> {
render (): React.ReactElement<MagnifierProps> {
let props = this.props;
let halfSize = props.size / 2;
let magX = props.zoomImage.width / props.smallImage.width;
let magY = props.zoomImage.height / props.smallImage.height;
let bgX = -(props.offsetX*magX - halfSize);
let bgY = -(props.offsetY*magY - halfSize);
let isVisible = props.offsetY < props.smallImage.height &&
props.offsetX < props.smallImage.width &&
props.offsetY > 0 &&
props.offsetX > 0;
return (
<div style={{
position: 'absolute',
display: isVisible ? 'block' : 'none',
top: props.y,
left: props.x,
width: props.size,
height: props.size,
marginLeft: -halfSize + props.cursorOffset.x,
marginTop: -halfSize + props.cursorOffset.y,
backgroundColor: 'white',
borderRadius: props.size,
boxShadow: `1px 1px 6px rgba(0,0,0,0.3)`
}}>
<div style={{
width: props.size,
height: props.size,
backgroundImage: `url(${props.zoomImage.src})`,
backgroundRepeat: 'no-repeat',
backgroundPosition: `${bgX}px ${bgY}px`,
borderRadius: props.size
}} />
</div>
);
}
}
export class ImageMagnifierLens extends React.Component<ImageMagnifierLensProps, ImageMagnifierLensState> {
private portalElement: HTMLDivElement = null;
constructor(props: ImageMagnifierLensProps, state: ImageMagnifierLensState) {
super(props);
this.setState({
x: 0,
y: 0,
offsetX: -1,
offsetY: -1
});
}
componentDidMount() {
document.addEventListener('mousemove', this.onMouseMove);
if (!this.portalElement) {
this.portalElement = document.createElement('div');
document.body.appendChild(this.portalElement);
}
this.componentDidUpdate();
}
componentWillUnmount() {
document.removeEventListener('mousemove', this.onMouseMove);
document.body.removeChild(this.portalElement);
this.portalElement = null;
}
onMouseMove = (e: any) => {
var offset = this.getOffset(ReactDOM.findDOMNode(this));
this.setState({
x: e.x + window.scrollX,
y: e.y + window.scrollY,
offsetX: e.x - offset.x,
offsetY: e.y - offset.y
});
}
componentDidUpdate() {
ReactDOM.render(<Magnifier
size={this.props.size}
smallImage={this.props.image}
zoomImage={this.props.zoomImage}
cursorOffset={this.props.cursorOffset}
{...this.state}
/>, this.portalElement);
}
private getOffset(el: HTMLDivElement): IOffset {
let x = 0;
let y = 0;
while( el && !isNaN( el.offsetLeft ) && !isNaN( el.offsetTop ) ) {
x += el.offsetLeft - el.scrollLeft;
y += el.offsetTop - el.scrollTop;
el = el.offsetParent as HTMLDivElement; //el.offsetParent;
}
return { x, y };
}
render () {
return (
<img {...this.props} src={this.props.image.src} />
);
}
}
export default ImageMagnifierLens;

View File

@ -0,0 +1,16 @@
define([], function() {
return {
"PropertyPaneDescription": "Configure the web part",
"BasicGroupName": "",
"DescriptionFieldLabel": "Description Field",
"SmallImgUrlFieldLabel": "Url small image",
"SmallImgWidthFieldLabel": "Width small image (px)",
"SmallImgHeightFieldLabel": "Height small image (px)",
"LargeImgUrlFieldLabel": "Url big image",
"LargeImgWidthFieldLabel": "Width small image (px)",
"LargeImgHeightFieldLabel": "Height small image (px)",
"CursorOffsetXFieldLabel": "The offset of the zoom from the cursor X (px)",
"CursorOffsetYFieldLabel": "The offset of the zoom from the cursor Y (px)",
"SizeFieldLabel": "Lens size (px)",
}
});

View File

@ -0,0 +1,19 @@
declare interface IImageMagnifierWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
SmallImgUrlFieldLabel: string,
SmallImgWidthFieldLabel: string,
SmallImgHeightFieldLabel: string,
LargeImgUrlFieldLabel: string,
LargeImgWidthFieldLabel: string,
LargeImgHeightFieldLabel: string,
CursorOffsetXFieldLabel: string,
CursorOffsetYFieldLabel: string,
SizeFieldLabel: string,
}
declare module 'ImageMagnifierWebPartStrings' {
const strings: IImageMagnifierWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,9 @@
/// <reference types="mocha" />
import { assert } from 'chai';
describe('ImageMagnifierWebPart', () => {
it('should do something', () => {
assert.ok(true);
});
});

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"types": [
"es6-promise",
"es6-collections",
"webpack-env"
]
}
}

View File

@ -0,0 +1,11 @@
// Type definitions for Microsoft ODSP projects
// Project: ODSP
/* Global definition for UNIT_TEST builds
Code that is wrapped inside an if(UNIT_TEST) {...}
block will not be included in the final bundle when the
--ship flag is specified */
declare const UNIT_TEST: boolean;
/* Global defintion for SPO builds */
declare const DATACENTER: boolean;

View File

@ -0,0 +1 @@
/// <reference path="@ms/odsp.d.ts" />

View File

@ -1,7 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.3.0",
"libraryName": "spfx-office-ui-fabric-people-picker",
"libraryId": "6349ba8f-d5aa-4cbf-87b4-910dad47324b",
"framework": "react"
"environment": "spo"
}
}

View File

@ -1,9 +1,9 @@
# People Picker (React)
## Summary
SharePoint Framework solution with the Office UI Fabric People Picker, the client web part across the SharePoint Search API is able to retrieve people.
SharePoint Framework solution with the Office UI Fabric People Picker, the client web part across the SharePoint Rest API is able to retrieve people and groups.
![React-People-Picker-gif](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-peoplepicker/assets/Preview.gif)
![React-People-Picker-gif](/assets/Preview.gif)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-green.svg)
@ -23,7 +23,8 @@ spfx-react-peoplepicker | Giuliano De Luca ([@giuleon](https://twitter.com/giule
Version|Date|Comments
-------|----|--------
1.0|May 21, 2017|Initial release
1.0.0|May 21, 2017|Initial release
1.0.1|Sep 28, 2017|Updated to GA Version, New properties that allow to specify the number of items to display and which entities retrieve (User, SharePoint Groups, Distribution Lists, Security Groups).
## 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.**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 678 KiB

After

Width:  |  Height:  |  Size: 414 KiB

View File

@ -1,13 +1,18 @@
{
"entries": [
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"office-ui-fabric-people-picker-bundle": {
"components": [
{
"entry": "./lib/webparts/officeUiFabricPeoplePicker/OfficeUiFabricPeoplePickerWebPart.js",
"manifest": "./src/webparts/officeUiFabricPeoplePicker/OfficeUiFabricPeoplePickerWebPart.manifest.json",
"outputPath": "./dist/office-ui-fabric-people-picker.bundle.js"
"entrypoint": "./lib/webparts/officeUiFabricPeoplePicker/OfficeUiFabricPeoplePickerWebPart.js",
"manifest": "./src/webparts/officeUiFabricPeoplePicker/OfficeUiFabricPeoplePickerWebPart.manifest.json"
}
],
"externals": {},
]
}
},
"localizedResources": {
"officeUiFabricPeoplePickerStrings": "webparts/officeUiFabricPeoplePicker/loc/{locale}.js"
}
"officeUiFabricPeoplePickerStrings": "lib/webparts/officeUiFabricPeoplePicker/loc/{locale}.js"
},
"externals": {}
}

View File

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

View File

@ -1,8 +1,10 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "spfx-office-ui-fabric-people-picker",
"id": "6349ba8f-d5aa-4cbf-87b4-910dad47324b",
"version": "1.0.0.0"
"version": "1.1.0.0",
"skipFeatureDeployment": false
},
"paths": {
"zippedPackage": "solution/spfx-office-ui-fabric-people-picker.sppkg"

View File

@ -1,3 +0,0 @@
{
"deployCdnPath": "temp/deploy"
}

View File

@ -1,4 +1,5 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"initialPage": "https://localhost:5432/workbench",
"https": true,

View File

@ -1,4 +1,5 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json",
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
@ -16,13 +17,11 @@
"export-name": false,
"forin": false,
"label-position": false,
"label-undefined": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-case": true,
"no-duplicate-key": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
@ -31,9 +30,6 @@
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-unused-imports": true,
"no-unused-variable": true,
"no-unreachable": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
@ -43,8 +39,7 @@
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false,
"prefer-const": true
"whitespace": false
}
}
}

View File

@ -1,3 +1,4 @@
{
"cdnBasePath": "<!-- PATH TO CDN -->"
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "https://publiccdn.sharepointonline.com/giuleon.sharepoint.com/cdn/SPFx-OfficeUIFabricPeoplePicker"
}

View File

@ -1,29 +1,27 @@
{
"name": "spfx-office-ui-fabric-people-picker",
"version": "0.0.1",
"version": "1.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"dependencies": {
"@microsoft/sp-client-base": "~0.7.0",
"@microsoft/sp-client-preview": "~0.9.0",
"@microsoft/sp-core-library": "~0.1.2",
"@microsoft/sp-webpart-base": "~0.4.0",
"@types/react": "0.14.46",
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-test-utils": "0.14.15",
"@types/react-addons-update": "0.14.14",
"react": "15.4.2",
"react-dom": "15.4.2",
"@types/react": "15.0.38",
"@types/react-dom": "0.14.18",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"office-ui-fabric-react": "0.69.0",
"react": "0.14.8",
"react-dom": "0.14.8"
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-update": "0.14.14",
"@types/react-addons-test-utils": "0.14.15",
"@microsoft/sp-core-library": "~1.3.0",
"@microsoft/sp-webpart-base": "~1.3.0",
"@microsoft/sp-lodash-subset": "~1.3.0",
"@types/webpack-env": ">=1.12.1 <1.14.0"
},
"devDependencies": {
"@microsoft/sp-build-web": "~0.9.0",
"@microsoft/sp-module-interfaces": "~0.7.0",
"@microsoft/sp-webpart-workbench": "~0.8.0",
"@microsoft/sp-build-web": "~1.3.0",
"@microsoft/sp-module-interfaces": "~1.3.0",
"@microsoft/sp-webpart-workbench": "~1.3.0",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0"

View File

@ -1,4 +1,9 @@
export interface IOfficeUiFabricPeoplePickerWebPartProps {
description: string;
typePicker: string;
principalTypeUser: boolean;
principalTypeSharePointGroup: boolean;
principalTypeSecurityGroup: boolean;
principalTypeDistributionList: boolean;
numberOfItems: number;
}

View File

@ -1,10 +1,11 @@
{
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "bb13df23-35f7-42c6-8c43-16be4fcd8daa",
"alias": "OfficeUiFabricPeoplePickerWebPart",
"componentType": "WebPart",
"version": "0.0.1",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
"preconfiguredEntries": [{
@ -15,7 +16,12 @@
"officeFabricIconFontName": "AddGroup",
"properties": {
"description": "Office UI Fabric People Picker",
"typePicker": ""
"typePicker": "Normal",
"principalTypeUser": true,
"principalTypeSharePointGroup": true,
"principalTypeSecurityGroup": false,
"principalTypeDistributionList": false,
"numberOfItems": 10
}
}]
}

View File

@ -6,6 +6,8 @@ import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneDropdown,
PropertyPaneToggle,
PropertyPaneSlider,
IWebPartContext
} from '@microsoft/sp-webpart-base';
@ -23,7 +25,12 @@ export default class OfficeUiFabricPeoplePickerWebPart extends BaseClientSideWeb
description: this.properties.description,
spHttpClient: this.context.spHttpClient,
siteUrl: this.context.pageContext.web.absoluteUrl,
typePicker: this.properties.typePicker
typePicker: this.properties.typePicker,
principalTypeUser: this.properties.principalTypeUser,
principalTypeSharePointGroup: this.properties.principalTypeSharePointGroup,
principalTypeSecurityGroup: this.properties.principalTypeSecurityGroup,
principalTypeDistributionList: this.properties.principalTypeDistributionList,
numberOfItems: this.properties.numberOfItems
}
);
@ -53,6 +60,32 @@ export default class OfficeUiFabricPeoplePickerWebPart extends BaseClientSideWeb
{ key: 'Compact', text: 'Compact' }
]
}),
PropertyPaneToggle('principalTypeUser', {
label: strings.principalTypeUserLabel,
checked: true,
}
),
PropertyPaneToggle('principalTypeSharePointGroup', {
label: strings.principalTypeSharePointGroupLabel,
checked: true,
}
),
PropertyPaneToggle('principalTypeSecurityGroup', {
label: strings.principalTypeSecurityGroupLabel,
checked: false,
}
),
PropertyPaneToggle('principalTypeDistributionList', {
label: strings.principalTypeDistributionListLabel,
checked: false,
}
),
PropertyPaneSlider('numberOfItems', {
label: strings.numberOfItemsFieldLabel,
min: 1,
max: 20,
step: 1
}),
]
}
]

View File

@ -5,4 +5,9 @@ export interface IOfficeUiFabricPeoplePickerProps {
spHttpClient: SPHttpClient;
siteUrl: string;
typePicker: string;
principalTypeUser: boolean;
principalTypeSharePointGroup: boolean;
principalTypeSecurityGroup: boolean;
principalTypeDistributionList: boolean;
numberOfItems: number;
}

View File

@ -0,0 +1,8 @@
export interface IOfficeUiFabricPeoplePickerWebPartProps {
description: string;
typePicker: string;
principalTypeUser: boolean;
principalTypeSharePointGroup: boolean;
principalTypeSecurityGroup: boolean;
principalTypeDistributionList: boolean;
}

View File

@ -1,14 +0,0 @@
export interface IPeopleDataResult {
RelevantResults: {
TotalRows: number,
Table: {
Rows: [{
Cells: [{
Key: string,
Value: string,
ValueType: string,
}]
}]
}
}
}

View File

@ -22,7 +22,6 @@ import {
} from 'office-ui-fabric-react/lib//Utilities';
import { people } from './PeoplePickerExampleData';
import { Label } from 'office-ui-fabric-react/lib/Label';
import { IPeopleDataResult } from './IPeopleDataResult';
import { IPersonaWithMenu } from 'office-ui-fabric-react/lib/components/pickers/PeoplePicker/PeoplePickerItems/PeoplePickerItem.Props';
import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu';
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
@ -81,6 +80,7 @@ export default class OfficeUiFabricPeoplePicker extends React.Component<IOfficeU
currentPicker: 1,
delayResults: false
};
}
public render(): React.ReactElement<IOfficeUiFabricPeoplePickerProps> {
@ -110,51 +110,130 @@ export default class OfficeUiFabricPeoplePicker extends React.Component<IOfficeU
@autobind
private _onFilterChanged(filterText: string, currentPersonas: IPersonaProps[], limitResults?: number) {
if (filterText) {
if (filterText.length > 4) {
return this.searchPeople(filterText, this._peopleList);
if (filterText.length > 2) {
return this._searchPeople(filterText, this._peopleList);
}
} else {
return [];
}
}
private searchPeople(terms: string, results: IPersonaProps[]): IPersonaProps[] | Promise<IPersonaProps[]> {
/**
* @function
* Returns fake people results for the Mock mode
*/
private searchPeopleFromMock(): IPersonaProps[] {
return this._peopleList = [
{
imageUrl: './images/persona-female.png',
imageInitials: 'PV',
primaryText: 'Annie Lindqvist',
secondaryText: 'Designer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
imageUrl: './images/persona-male.png',
imageInitials: 'AR',
primaryText: 'Aaron Reid',
secondaryText: 'Designer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
imageUrl: './images/persona-male.png',
imageInitials: 'AL',
primaryText: 'Alex Lundberg',
secondaryText: 'Software Developer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
imageUrl: './images/persona-male.png',
imageInitials: 'RK',
primaryText: 'Roko Kolar',
secondaryText: 'Financial Analyst',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
];
}
/**
* @function
* Returns people results after a REST API call
*/
private _searchPeople(terms: string, results: IPersonaProps[]): IPersonaProps[] | Promise<IPersonaProps[]> {
//return new Promise<IPersonaProps[]>((resolve, reject) => setTimeout(() => resolve(results), 2000));
if (this.props.siteUrl.toLowerCase().indexOf("wwww.contoso.com") >= 0) {
// If the running environment is local, load the data from the mock
return this.searchPeopleFromMock();
} else {
const userRequestUrl: string = `${this.props.siteUrl}/_api/SP.UI.ApplicationPages.ClientPeoplePickerWebServiceInterface.clientPeoplePickerSearchUser`;
let principalType: number = 0;
if (this.props.principalTypeUser === true) {
principalType += 1;
}
if (this.props.principalTypeSharePointGroup === true) {
principalType += 8;
}
if (this.props.principalTypeSecurityGroup === true) {
principalType += 4;
}
if (this.props.principalTypeDistributionList === true) {
principalType += 2;
}
const data = {
'queryParams': {
'AllowEmailAddresses': true,
'AllowMultipleEntities': false,
'AllUrlZones': false,
'MaximumEntitySuggestions': this.props.numberOfItems,
'PrincipalSource': 15,
// PrincipalType controls the type of entities that are returned in the results.
// Choices are All - 15, Distribution List - 2 , Security Groups - 4, SharePoint Groups - 8, User - 1.
// These values can be combined (example: 13 is security + SP groups + users)
'PrincipalType': principalType,
'QueryString': terms
}
};
return new Promise<IPersonaProps[]>((resolve, reject) =>
this.props.spHttpClient.get(`${this.props.siteUrl}/_api/search/query?querytext='*${terms}*'&rowlimit=10&sourceid='b09a7990-05ea-4af9-81ef-edfab16c4e31'`,
this.props.spHttpClient.post(userRequestUrl,
SPHttpClient.configurations.v1,
{
headers: {
'Accept': 'application/json;odata=nometadata',
'odata-version': ''
}
'Accept': 'application/json',
"content-type": "application/json"
},
body: JSON.stringify(data)
})
.then((response: SPHttpClientResponse): Promise<{ PrimaryQueryResult: IPeopleDataResult }> => {
.then((response: SPHttpClientResponse) => {
return response.json();
})
.then((response: { PrimaryQueryResult: IPeopleDataResult }): void => {
let relevantResults: any = response.PrimaryQueryResult.RelevantResults;
let resultCount: number = relevantResults.TotalRows;
.then((response: any): void => {
let relevantResults: any = JSON.parse(response.value);
let resultCount: number = relevantResults.length;
let people = [];
let persona: IPersonaProps = {};
if (resultCount > 0) {
relevantResults.Table.Rows.forEach(function (row) {
row.Cells.forEach(function (cell) {
//person[cell.Key] = cell.Value;
if (cell.Key === 'JobTitle')
persona.secondaryText = cell.Value;
if (cell.Key === 'PictureURL')
persona.imageUrl = cell.Value;
if (cell.Key === 'PreferredName')
persona.primaryText = cell.Value;
});
for (var index = 0; index < resultCount; index++) {
var p = relevantResults[index];
let account = p.Key.substr(p.Key.lastIndexOf('|') + 1);
persona.primaryText = p.DisplayText;
persona.imageUrl = `/_layouts/15/userphoto.aspx?size=S&accountname=${account}`;
persona.imageShouldFadeIn = true;
persona.secondaryText = p.EntityData.Title;
people.push(persona);
});
}
}
resolve(people);
}, (error: any): void => {
reject(this._peopleList = []);
}));
})
);
};
}
private _filterPersonasByText(filterText: string): IPersonaProps[] {

View File

@ -2,7 +2,6 @@ import { IPersonaProps } from 'office-ui-fabric-react/lib/Persona';
export const people: IPersonaProps[] = [
{
key: 0,
imageUrl: './images/persona-female.png',
imageInitials: 'PV',
primaryText: 'Annie Lindqvist',
@ -11,7 +10,6 @@ export const people: IPersonaProps[] = [
optionalText: 'Available at 4:00pm'
},
{
key: 1,
imageUrl: './images/persona-male.png',
imageInitials: 'AR',
primaryText: 'Aaron Reid',
@ -20,7 +18,6 @@ export const people: IPersonaProps[] = [
optionalText: 'Available at 4:00pm'
},
{
key: 2,
imageUrl: './images/persona-male.png',
imageInitials: 'AL',
primaryText: 'Alex Lundberg',
@ -29,7 +26,6 @@ export const people: IPersonaProps[] = [
optionalText: 'Available at 4:00pm'
},
{
key: 3,
imageUrl: './images/persona-male.png',
imageInitials: 'RK',
primaryText: 'Roko Kolar',
@ -37,184 +33,4 @@ export const people: IPersonaProps[] = [
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 4,
imageUrl: './images/persona-male.png',
imageInitials: 'CB',
primaryText: 'Christian Bergqvist',
secondaryText: 'Sr. Designer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 5,
imageUrl: './images/persona-female.png',
imageInitials: 'VL',
primaryText: 'Valentina Lovric',
secondaryText: 'Design Developer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 6,
imageUrl: './images/persona-male.png',
imageInitials: 'MS',
primaryText: 'Maor Sharett',
secondaryText: 'UX Designer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 7,
imageUrl: './images/persona-female.png',
imageInitials: 'PV',
primaryText: 'Anny Lindqvist',
secondaryText: 'Designer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 8,
imageUrl: './images/persona-male.png',
imageInitials: 'AR',
primaryText: 'Aron Reid',
secondaryText: 'Designer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 9,
imageUrl: './images/persona-male.png',
imageInitials: 'AL',
primaryText: 'Alix Lundberg',
secondaryText: 'Software Developer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 10,
imageUrl: './images/persona-male.png',
imageInitials: 'RK',
primaryText: 'Roko Kular',
secondaryText: 'Financial Analyst',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 11,
imageUrl: './images/persona-male.png',
imageInitials: 'CB',
primaryText: 'Christian Bergqvest',
secondaryText: 'Sr. Designer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 12,
imageUrl: './images/persona-female.png',
imageInitials: 'VL',
primaryText: 'Valintina Lovric',
secondaryText: 'Design Developer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 13,
imageUrl: './images/persona-male.png',
imageInitials: 'MS',
primaryText: 'Maor Sharet',
secondaryText: 'UX Designer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 14,
imageUrl: './images/persona-female.png',
imageInitials: 'VL',
primaryText: 'Anny Lindqvest',
secondaryText: 'SDE',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 15,
imageUrl: './images/persona-male.png',
imageInitials: 'MS',
primaryText: 'Alix Lunberg',
secondaryText: 'SE',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 16,
imageUrl: './images/persona-female.png',
imageInitials: 'VL',
primaryText: 'Annie Lindqvest',
secondaryText: 'SDET',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 17,
imageUrl: './images/persona-male.png',
imageInitials: 'MS',
primaryText: 'Alixander Lundberg',
secondaryText: 'Senior Manager of SDET',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 18,
imageUrl: './images/persona-female.png',
imageInitials: 'VL',
primaryText: 'Anny Lundqvist',
secondaryText: 'Junior Manager of Software',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 13,
imageUrl: './images/persona-male.png',
imageInitials: 'MS',
primaryText: 'Maor Shorett',
secondaryText: 'UX Designer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 12,
imageUrl: './images/persona-female.png',
imageInitials: 'VL',
primaryText: 'Valentina Lovrics',
secondaryText: 'Design Developer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 13,
imageUrl: './images/persona-male.png',
imageInitials: 'MS',
primaryText: 'Maor Sharet',
secondaryText: 'UX Designer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 12,
imageUrl: './images/persona-female.png',
imageInitials: 'VL',
primaryText: 'Valentina Lovrecs',
secondaryText: 'Design Developer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
{
key: 13,
imageUrl: './images/persona-male.png',
imageInitials: 'MS',
primaryText: 'Maor Sharitt',
secondaryText: 'UX Designer',
tertiaryText: 'In a meeting',
optionalText: 'Available at 4:00pm'
},
];

View File

@ -1,8 +1,13 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"PropertyPaneDescription": "Configuration Panel",
"BasicGroupName": "Configure the people picker",
"DescriptionFieldLabel": "Description Field",
"TypePickerLabel": "Choose the type of People Picker"
"TypePickerLabel": "Choose the type of People Picker",
"principalTypeUserLabel": "User",
"principalTypeSharePointGroupLabel": "SharePoint Groups",
"principalTypeSecurityGroupLabel": "Security Groups",
"principalTypeDistributionListLabel": "Distribution List",
"numberOfItemsFieldLabel": "Number of items to show"
}
});

View File

@ -3,6 +3,11 @@ declare interface IOfficeUiFabricPeoplePickerStrings {
BasicGroupName: string;
DescriptionFieldLabel: string;
TypePickerLabel: string;
principalTypeUserLabel: string;
principalTypeSharePointGroupLabel: string;
principalTypeSecurityGroupLabel: string;
principalTypeDistributionListLabel: string;
numberOfItemsFieldLabel: string;
}
declare module 'officeUiFabricPeoplePickerStrings' {

View File

@ -4,11 +4,12 @@
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"types": [
"es6-promise",
"es6-collections",
"webpack-env"
]
}

View File

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

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

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

View File

@ -0,0 +1,75 @@
// 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",
"json.schemas": [
{
"fileMatch": [
"/config/config.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/configJson/schemas/config-v1.schema.json"
},
{
"fileMatch": [
"/config/copy-assets.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/copyAssets/copy-assets.schema.json"
},
{
"fileMatch": [
"/config/deploy-azure-storage.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/deployAzureStorage/deploy-azure-storage.schema.json"
},
{
"fileMatch": [
"/config/package-solution.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/packageSolution/package-solution.schema.json"
},
{
"fileMatch": [
"/config/serve.json"
],
"url": "./node_modules/@microsoft/gulp-core-build-serve/lib/serve.schema.json"
},
{
"fileMatch": [
"/config/tslint.json"
],
"url": "./node_modules/@microsoft/gulp-core-build-typescript/lib/schemas/tslint.schema.json"
},
{
"fileMatch": [
"/config/write-manifests.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/writeManifests/write-manifests.schema.json"
},
{
"fileMatch": [
"/config/configure-webpack.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/configureWebpack/configure-webpack.schema.json"
},
{
"fileMatch": [
"/config/configure-external-bundling-webpack.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/configureWebpack/configure-webpack-external-bundling.schema.json"
},
{
"fileMatch": [
"/copy-static-assets.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/copyStaticAssets/copy-static-assets.schema.json"
}
]
}

View File

@ -0,0 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.3.1",
"libraryName": "react-youtube",
"libraryId": "ca0fd050-e358-4093-93cd-bc3d74e131e5",
"environment": "spo"
}
}

View File

@ -0,0 +1,50 @@
# Youtube Web Part
## Summary
This web part allows to search and view the Youtube videos, across the Youtube API, directly on a SharePoint page, furthermore the property panel offers the possibility to specify the api key, the number of items to display and it is also possible specify a Youtube Channel Id.
![Demo](./assets/Preview.gif)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Prerequisites
> Is necessary to have a google developer api key in order to perform rest api call
## Solution
Solution|Author(s)
--------|---------
react-youtube|Giuliano De Luca ([@giuleon](https://twitter.com/giuleon) , [www.delucagiuliano.com](delucagiuliano.com))
## Version history
Version|Date|Comments
-------|----|--------
1.0|October 02, 2017|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- in the command line run:
- `npm install`
- `gulp serve`
## Features
This Web Part illustrates the following concepts on top of the SharePoint Framework:
- Youtube - How to leverage the Youtube capabilities.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-youtube" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"youtube-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/youtube/YoutubeWebPart.js",
"manifest": "./src/webparts/youtube/YoutubeWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"YoutubeWebPartStrings": "lib/webparts/youtube/loc/{locale}.js"
}
}

View File

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

View File

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

View File

@ -0,0 +1,12 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-youtube-client-side-solution",
"id": "ca0fd050-e358-4093-93cd-bc3d74e131e5",
"version": "1.0.0.0",
"skipFeatureDeployment": true
},
"paths": {
"zippedPackage": "solution/react-youtube.sppkg"
}
}

View File

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

View File

@ -0,0 +1,45 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json",
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"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-case": true,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "https://publiccdn.sharepointonline.com/giuleon.sharepoint.com/cdn/SPFx-youtube"
}

6
samples/react-youtube/gulpfile.js vendored Normal file
View File

@ -0,0 +1,6 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.initialize(gulp);

View File

@ -0,0 +1,36 @@
{
"name": "react-youtube",
"version": "1.0.0",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "~1.3.0",
"@microsoft/sp-lodash-subset": "~1.3.0",
"@microsoft/sp-webpart-base": "~1.3.0",
"@types/react": "15.0.38",
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-test-utils": "0.14.15",
"@types/react-addons-update": "0.14.14",
"@types/react-dom": "0.14.18",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"axios": "^0.16.2",
"lodash": "^4.17.4",
"react": "15.4.2",
"react-dom": "15.4.2"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.3.0",
"@microsoft/sp-module-interfaces": "~1.3.0",
"@microsoft/sp-webpart-workbench": "~1.3.0",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0"
}
}

View File

@ -0,0 +1,27 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "b4be3e02-7b4a-4a01-8fb6-4c369a6cacb7",
"alias": "YoutubeWebPart",
"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,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "Youtube" },
"description": { "default": "Bring youtube on SharePoint" },
"officeFabricIconFontName": "Video",
"properties": {
"description": "Youtube web part",
"maxResults": 3
}
}]
}

View File

@ -0,0 +1,75 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneSlider
} from '@microsoft/sp-webpart-base';
import * as strings from 'YoutubeWebPartStrings';
import Youtube from './components/Youtube';
import { IYoutubeProps } from './components/IYoutubeProps';
export interface IYoutubeWebPartProps {
description: string;
apiKey: string;
channelId: string;
maxResults: number;
}
export default class YoutubeWebPart extends BaseClientSideWebPart<IYoutubeWebPartProps> {
public render(): void {
const element: React.ReactElement<IYoutubeProps > = React.createElement(
Youtube,
{
description: this.properties.description,
apiKey: this.properties.apiKey,
channelId: this.properties.channelId,
maxResults: this.properties.maxResults
}
);
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
}),
PropertyPaneTextField('apiKey', {
label: strings.ApiKeyFieldLabel
}),
PropertyPaneTextField('channelId', {
label: strings.ChannelIdFieldLabel
}),
PropertyPaneSlider('maxResults', {
label: strings.MaxResults,
min: 1,
max: 10,
step: 1
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,6 @@
export interface IYoutubeProps {
description: string;
apiKey: string;
channelId: string;
maxResults: number;
}

View File

@ -0,0 +1,5 @@
export interface IYoutubeState {
videos: string[];
selectedVideo: string;
}

View File

@ -0,0 +1,4 @@
export interface ISearchBarProps {
onSearchTermChange: ((term: any) => void);
}

View File

@ -0,0 +1,3 @@
export interface ISearchBarState {
term: string;
}

View File

@ -0,0 +1,39 @@
import * as React from 'react';
import { ISearchBarProps } from './ISearchBarProps';
import { ISearchBarState } from './ISearchBarState';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { autobind } from 'office-ui-fabric-react/lib/Utilities';
export class SearchBar extends React.Component<ISearchBarProps, ISearchBarState> {
constructor(props) {
super(props);
this.state = {
term: ''
};
}
render(): React.ReactElement<ISearchBarProps> {
{
return (
<div className="search-bar">
<TextField
label='Search a video'
iconProps={{ iconName: 'Search' }}
value={this.state.term}
onChanged={this.onInputChange}
/>
</div>
);
}
}
@autobind
private onInputChange(term: string) {
this.setState({ term });
this.props.onSearchTermChange(term);
}
}
export default SearchBar;

View File

@ -0,0 +1,19 @@
import * as React from 'react';
export const VideoDetail = ({ video }) => {
if (!video) {
return <div>Please be sure that the web part is properly configured with a valid API key!</div>;
}
const videoId = video.id.videoId;
const url = `https://www.youtube.com/embed/${videoId}`;
return (
<div className="ms-Grid-col ms-lg8">
<iframe src={url} frameBorder="0" allowFullScreen></iframe>
<div><h2>{video.snippet.title}</h2></div>
<span className="label label-default">{video.snippet.channelTitle}</span>
<div>{video.snippet.description}</div>
</div>
);
};

View File

@ -0,0 +1,24 @@
import * as React from 'react';
import VideoListItem from '../VideoListItem/VideoListItem';
export const VideoList = (props) => {
const videoItems = props.videos.map((video, index) => {
return (
<VideoListItem
key={index}
video={video}
onVideoSelect={props.onVideoSelect} />
)
});
return (
// <ul className="ms-Grid-col ms-lg4 ms-xl4">
// {videoItems}
// </ul>
<ul className="ms-Grid-col ms-lg4 ms-xl4">
{videoItems}
</ul>
);
};
// export default VideoList;

View File

@ -0,0 +1,27 @@
import * as React from 'react';
import { Image } from 'office-ui-fabric-react/lib/Image';
import { Label } from 'office-ui-fabric-react/lib/Label';
const VideoListItem = ({ video, onVideoSelect }) => {
const imageUrl = video.snippet.thumbnails.default.url;
return (
// <li onClick={() => onVideoSelect(video)} className="list-group-item">
// <div className="video-list media">
// <div className="media-left">
// <img className="media-object" src={imageUrl}/>
// </div>
// <div className="media-body">
// <div className="media-heading">{video.snippet.title}</div>
// </div>
// </div>
// </li>
<li onClick={() => onVideoSelect(video)} >
<Image src={imageUrl} />
<Label>{video.snippet.title}</Label>
</li>
)
};
export default VideoListItem;

View File

@ -0,0 +1,56 @@
.youtube {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
padding: 20px;
}
.listItem {
max-width: 715px;
margin: 5px auto 5px auto;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: "[theme:themePrimary, default:#0078d7]";
border-color: "[theme:themePrimary, default:#0078d7]";
color: #ffffff;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
font-weight: 400;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: 600;
font-size: 14px;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}
ul {
list-style: none;
}

View File

@ -0,0 +1,78 @@
import * as React from 'react';
import styles from './Youtube.module.scss';
import { IYoutubeProps } from './IYoutubeProps';
import { IYoutubeState } from './IYoutubeState';
import { escape } from '@microsoft/sp-lodash-subset';
import { SearchBar } from './SearchBar/SearchBar';
import { VideoDetail } from './VideoDetail/VideoDetail';
import { VideoList } from './VideoList/VideoList'
import * as _ from 'lodash';
import axios from 'axios';
const ROOT_URL = 'https://www.googleapis.com/youtube/v3/search';
export default class Youtube extends React.Component<IYoutubeProps, IYoutubeState> {
constructor(props) {
super(props);
this.state = {
videos: [],
selectedVideo: null
};
this.videoSearch(this.props.apiKey, '', this.props.channelId, this.props.maxResults.toString())
}
public render(): React.ReactElement<IYoutubeProps> {
const videoSearch = _.debounce((term) => { this.videoSearch(this.props.apiKey, term, this.props.channelId, this.props.maxResults.toString()) }, 300);
return (
<div className={styles.youtube}>
<div className={styles.container}>
<div className={`ms-Grid-row ms-bgColor-white ms-fontColor-black ${styles.row}`}>
<p className="ms-font-l">{escape(this.props.description)}</p>
<div>
<SearchBar onSearchTermChange={videoSearch} />
<br />
<VideoDetail video={this.state.selectedVideo} />
<VideoList
onVideoSelect={selectedVideo => this.setState({ selectedVideo })}
videos={this.state.videos} />
</div>
</div>
</div>
</div>
);
}
private videoSearch(key: string, term: string, channelId: string, maxResults: string): any {
if (!key) {
//throw new Error('Youtube Search expected key, received undefined');
console.log('Youtube Search expected key, received undefined');
return;
}
let params = {
part: 'snippet',
key: key,
q: term,
maxResults: maxResults,
type: 'video',
channelId: channelId || null
};
axios.get(ROOT_URL, { params: params })
.then((response) => {
// if (callback) { callback(response.data.items); }
// return response.data.items;
this.setState({
videos: response.data.items,
selectedVideo: response.data.items[0]
});
})
.catch((error) => {
console.error(error);
});
}
}

View File

@ -0,0 +1,10 @@
define([], function() {
return {
"PropertyPaneDescription": "In order to use this web part is necessary to use a valid API key",
"BasicGroupName": "Configuration Panel",
"DescriptionFieldLabel": "Description Field",
"ApiKeyFieldLabel": "Insert your Google API Key",
"ChannelIdFieldLabel": "Optionally you can restrict the search in a Channel",
"MaxResults": "Max items to diplay"
}
});

View File

@ -0,0 +1,13 @@
declare interface IYoutubeWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
ApiKeyFieldLabel: string;
ChannelIdFieldLabel: string;
MaxResults: string;
}
declare module 'YoutubeWebPartStrings' {
const strings: IYoutubeWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,9 @@
/// <reference types="mocha" />
import { assert } from 'chai';
describe('YoutubeWebPart', () => {
it('should do something', () => {
assert.ok(true);
});
});

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"types": [
"es6-promise",
"es6-collections",
"webpack-env"
]
}
}

View File

@ -0,0 +1,11 @@
// Type definitions for Microsoft ODSP projects
// Project: ODSP
/* Global definition for UNIT_TEST builds
Code that is wrapped inside an if(UNIT_TEST) {...}
block will not be included in the final bundle when the
--ship flag is specified */
declare const UNIT_TEST: boolean;
/* Global defintion for SPO builds */
declare const DATACENTER: boolean;

View File

@ -0,0 +1 @@
/// <reference path="@ms/odsp.d.ts" />