initial Commit
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
||||
node_modules/
|
||||
lib/
|
||||
dist/
|
||||
temp/
|
||||
release/
|
||||
solution/
|
||||
sharepoint/solution/
|
||||
*.sppkg
|
||||
*.tgz
|
||||
coverage/
|
||||
*.resx.ts
|
||||
*.scss.ts
|
||||
*.log
|
||||
npm-debug.log*
|
||||
.vs/
|
||||
.idea/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"version": "1.4.1",
|
||||
"libraryName": "modern-promoted-link-list-webpart",
|
||||
"libraryId": "26608df8-0234-40ac-8f8e-d0679aa93fc3",
|
||||
"environment": "onprem",
|
||||
"packageManager": "npm",
|
||||
"solutionName": "modern-promoted-link-list-webpart",
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# Modern Promoted Link List
|
||||
|
||||
SPFx-1.4.1-WebPart für SharePoint Server Subscription Edition. Es stellt eine klassische Liste vom Typ **Höher gestufte Links / Promoted Links** als responsive Kacheln auf modernen Seiten dar.
|
||||
|
||||
## Funktionen
|
||||
|
||||
- Auswahl vorhandener Promoted-Links-Listen im Property Pane
|
||||
- Übernahme von Titel, Beschreibung, Hintergrundbild, Ziel, Startverhalten und Reihenfolge
|
||||
- einstellbare maximale Spaltenanzahl (1–6), Kachelhöhe (120–320 px) und Beschreibungsanzeige
|
||||
- responsive Darstellung für schmale Bereiche und mobile Geräte
|
||||
- Unterstützung für Linköffnung auf derselben Seite, in einem neuen Tab/Fenster oder in einem Dialogfenster
|
||||
- SharePoint-Themefarben, Tastaturfokus und zugängliche Beschriftungen
|
||||
- Prüfung der Ziel- und Bild-URLs gegen unsichere Schemas
|
||||
|
||||
## Verwendung
|
||||
|
||||
1. Falls noch nicht vorhanden, über **Websiteinhalte > App hinzufügen** eine Liste vom Typ **Höher gestufte Links** erstellen.
|
||||
2. Einträge in der klassischen Liste pflegen.
|
||||
3. Das WebPart **Modern Promoted Link List** auf einer modernen Seite hinzufügen.
|
||||
4. Im Property Pane die gewünschte Liste und das Layout auswählen.
|
||||
|
||||
Die Liste muss sich im selben Web wie die Seite befinden. Einträge ohne gültiges Linkziel werden nicht dargestellt.
|
||||
|
||||
## Build-Umgebung
|
||||
|
||||
- Node.js 8.17.0
|
||||
- npm 6.13.4
|
||||
- SharePoint Framework 1.4.1
|
||||
|
||||
```powershell
|
||||
npm install
|
||||
npm test
|
||||
gulp clean
|
||||
gulp bundle --ship
|
||||
gulp package-solution --ship
|
||||
```
|
||||
|
||||
Das Paket wird unter `sharepoint/solution/modern-promoted-link-list-webpart.sppkg` erzeugt.
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"modern-promoted-link-list-webpart": {
|
||||
"components": [{
|
||||
"entrypoint": "./lib/webparts/modernPromotedLinks/ModernPromotedLinksWebPart.js",
|
||||
"manifest": "./src/webparts/modernPromotedLinks/ModernPromotedLinksWebPart.manifest.json"
|
||||
}]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"ModernPromotedLinksWebPartStrings": "lib/webparts/modernPromotedLinks/loc/{locale}.js"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "modern-promoted-link-list-webpart",
|
||||
"title": "modern-promoted-link-list-webpart",
|
||||
"id": "26608df8-0234-40ac-8f8e-d0679aa93fc3",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": true
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/modern-promoted-link-list-webpart.sppkg"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://localhost:5432/workbench",
|
||||
"api": {
|
||||
"port": 5432,
|
||||
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
build.initialize(require('gulp'));
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "modern-promoted-link-list-webpart",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"engines": { "node": ">=6.9.0 <9.0.0" },
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "tsc --noEmit -p . && tsc -p tests/tsconfig.json && node temp/tests/tests/PromotedLinkCore.test.js && node tests/validate-project.js",
|
||||
"package": "npm test && gulp clean && gulp bundle --ship && gulp package-solution --ship"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "1.4.1",
|
||||
"@microsoft/sp-http": "1.4.1",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.4.1",
|
||||
"@microsoft/sp-webpart-base": "1.4.1",
|
||||
"@types/es6-promise": "0.0.33",
|
||||
"@types/react": "15.6.6",
|
||||
"@types/react-dom": "15.5.6",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"react": "15.6.2",
|
||||
"react-dom": "15.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "1.4.1",
|
||||
"@microsoft/sp-module-interfaces": "1.4.1",
|
||||
"@microsoft/sp-tslint-rules": "1.4.1",
|
||||
"@microsoft/sp-webpart-workbench": "1.4.1",
|
||||
"ajv": "5.2.2",
|
||||
"gulp": "~3.9.1"
|
||||
}
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module '*.module.scss' {
|
||||
const styles: { [className: string]: string };
|
||||
export = styles;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ModernPromotedLinksWebPart } from './webparts/modernPromotedLinks/ModernPromotedLinksWebPart';
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "f88df9e2-9e60-4e48-acb2-bd2898c5e75b",
|
||||
"alias": "ModernPromotedLinksWebPart",
|
||||
"componentType": "WebPart",
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
"requiresCustomScript": false,
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
|
||||
"group": { "default": "Other", "de-de": "Weitere" },
|
||||
"title": { "default": "Modern Promoted Link List", "de-de": "Moderne Linkkacheln" },
|
||||
"description": {
|
||||
"default": "Displays a classic Promoted Links list as responsive modern tiles.",
|
||||
"de-de": "Zeigt eine klassische Liste 'Höher gestufte Links' als responsive moderne Kacheln an."
|
||||
},
|
||||
"officeFabricIconFontName": "Tiles",
|
||||
"properties": {
|
||||
"listId": "",
|
||||
"columns": 4,
|
||||
"tileHeight": 180,
|
||||
"showDescriptions": true
|
||||
}
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
BaseClientSideWebPart,
|
||||
IPropertyPaneConfiguration,
|
||||
IPropertyPaneDropdownOption,
|
||||
PropertyPaneDropdown,
|
||||
PropertyPaneSlider,
|
||||
PropertyPaneToggle
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
import * as strings from 'ModernPromotedLinksWebPartStrings';
|
||||
import { normalizeColumns, normalizeTileHeight } from './PromotedLinkCore';
|
||||
import { PromotedLinksDataService } from './PromotedLinksDataService';
|
||||
import { IPromotedLinkItem, IPromotedLinksListOption } from './PromotedLinkTypes';
|
||||
import ModernPromotedLinks from './components/ModernPromotedLinks';
|
||||
import { IModernPromotedLinksProps } from './components/IModernPromotedLinksProps';
|
||||
import styles = require('./components/ModernPromotedLinks.module.scss');
|
||||
|
||||
export interface IModernPromotedLinksWebPartProps {
|
||||
listId: string;
|
||||
columns: number;
|
||||
tileHeight: number;
|
||||
showDescriptions: boolean;
|
||||
}
|
||||
|
||||
export default class ModernPromotedLinksWebPart extends BaseClientSideWebPart<IModernPromotedLinksWebPartProps> {
|
||||
private service: PromotedLinksDataService;
|
||||
private lists: IPromotedLinksListOption[] = [];
|
||||
private loadingLists: boolean = false;
|
||||
private requestVersion: number = 0;
|
||||
private disposed: boolean = false;
|
||||
|
||||
protected onInit(): Promise<void> {
|
||||
this.service = new PromotedLinksDataService(this.context);
|
||||
return super.onInit().then((): Promise<void> => this.loadLists());
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
const version: number = ++this.requestVersion;
|
||||
if (!this.properties.listId) { this.renderStatus(strings.ConfigureMessage, false); return; }
|
||||
this.renderStatus(strings.LoadingMessage, false);
|
||||
this.service.getItems(this.properties.listId).then((items: IPromotedLinkItem[]): void => {
|
||||
if (this.disposed || version !== this.requestVersion) { return; }
|
||||
if (!items.length) { this.renderStatus(strings.EmptyMessage, false); return; }
|
||||
const element: React.ReactElement<IModernPromotedLinksProps> = React.createElement(ModernPromotedLinks, {
|
||||
items: items,
|
||||
columns: normalizeColumns(this.properties.columns),
|
||||
tileHeight: normalizeTileHeight(this.properties.tileHeight),
|
||||
showDescriptions: this.properties.showDescriptions !== false,
|
||||
dialogWindowLabel: strings.DialogWindowLabel
|
||||
});
|
||||
ReactDom.render(element, this.domElement);
|
||||
}).catch((error: Error): void => {
|
||||
if (this.disposed || version !== this.requestVersion) { return; }
|
||||
if (window.console) { console.error('[ModernPromotedLinks] ' + strings.LoadErrorMessage, error); }
|
||||
this.renderStatus(strings.LoadErrorMessage, true);
|
||||
});
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
this.disposed = true;
|
||||
this.requestVersion++;
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version { return Version.parse('1.0'); }
|
||||
|
||||
protected onPropertyPaneConfigurationStart(): void {
|
||||
if (!this.lists.length && !this.loadingLists) { this.loadLists(); }
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
const listOptions: IPropertyPaneDropdownOption[] = [{ key: '', text: strings.SelectOption }];
|
||||
for (let i: number = 0; i < this.lists.length; i++) {
|
||||
listOptions.push({ key: this.lists[i].id, text: this.lists[i].title });
|
||||
}
|
||||
return {
|
||||
pages: [{
|
||||
header: { description: strings.PropertyPaneDescription },
|
||||
groups: [{
|
||||
groupName: strings.DataGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneDropdown('listId', {
|
||||
label: strings.ListFieldLabel,
|
||||
options: listOptions,
|
||||
disabled: this.loadingLists
|
||||
})
|
||||
]
|
||||
}, {
|
||||
groupName: strings.LayoutGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneSlider('columns', { label: strings.ColumnsFieldLabel, min: 1, max: 6, step: 1 }),
|
||||
PropertyPaneSlider('tileHeight', { label: strings.TileHeightFieldLabel, min: 120, max: 320, step: 10 }),
|
||||
PropertyPaneToggle('showDescriptions', { label: strings.ShowDescriptionsFieldLabel })
|
||||
]
|
||||
}]
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
private loadLists(): Promise<void> {
|
||||
this.loadingLists = true;
|
||||
this.refreshPropertyPane();
|
||||
return this.service.getLists().then((lists: IPromotedLinksListOption[]): void => {
|
||||
this.lists = lists;
|
||||
}).catch((error: Error): void => {
|
||||
this.lists = [];
|
||||
if (window.console) { console.error('[ModernPromotedLinks] Lists could not be loaded.', error); }
|
||||
}).then((): void => {
|
||||
this.loadingLists = false;
|
||||
this.refreshPropertyPane();
|
||||
});
|
||||
}
|
||||
|
||||
private refreshPropertyPane(): void {
|
||||
if (this.context && this.context.propertyPane) { this.context.propertyPane.refresh(); }
|
||||
}
|
||||
|
||||
private renderStatus(message: string, error: boolean): void {
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
const status: HTMLDivElement = document.createElement('div');
|
||||
status.className = error ? styles.error : styles.status;
|
||||
status.setAttribute('role', error ? 'alert' : 'status');
|
||||
status.textContent = message;
|
||||
this.domElement.innerHTML = '';
|
||||
this.domElement.appendChild(status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
export function normalizeColumns(value: number): number {
|
||||
const result: number = Math.round(Number(value) || 4);
|
||||
return Math.min(6, Math.max(1, result));
|
||||
}
|
||||
|
||||
export function normalizeTileHeight(value: number): number {
|
||||
const result: number = Math.round(Number(value) || 180);
|
||||
return Math.min(320, Math.max(120, result));
|
||||
}
|
||||
|
||||
export function normalizeNavigationUrl(value: string, webUrl: string): string {
|
||||
return normalizeUrl(value, webUrl, true);
|
||||
}
|
||||
|
||||
export function normalizeImageUrl(value: string, webUrl: string): string {
|
||||
return normalizeUrl(value, webUrl, false);
|
||||
}
|
||||
|
||||
export function isNewTabLaunch(value: string): boolean {
|
||||
return /^new\s*(tab|window)$/i.test(String(value || '').trim());
|
||||
}
|
||||
|
||||
export function isDialogLaunch(value: string): boolean {
|
||||
return /^dialog$/i.test(String(value || '').trim());
|
||||
}
|
||||
|
||||
function normalizeUrl(value: string, webUrl: string, allowSpecialLinks: boolean): string {
|
||||
const url: string = String(value || '').trim();
|
||||
if (!url || /[\u0000-\u001f\u007f]/.test(url) || /^\/\//.test(url)) { return ''; }
|
||||
if (/^https?:\/\//i.test(url)) { return url; }
|
||||
if (allowSpecialLinks && /^(mailto|tel):/i.test(url)) { return url; }
|
||||
if (/^[a-z][a-z0-9+.-]*:/i.test(url)) { return ''; }
|
||||
if (url.charAt(0) === '/' || (allowSpecialLinks && (url.charAt(0) === '#' || url.charAt(0) === '?'))) {
|
||||
return url;
|
||||
}
|
||||
const base: string = String(webUrl || '').replace(/\/$/, '');
|
||||
return base ? base + '/' + url.replace(/^\.\//, '') : '';
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface IPromotedLinksListOption {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface IPromotedLinkItem {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
linkUrl: string;
|
||||
imageUrl: string;
|
||||
launchBehavior: string;
|
||||
tileOrder: number;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { normalizeImageUrl, normalizeNavigationUrl } from './PromotedLinkCore';
|
||||
import { IPromotedLinkItem, IPromotedLinksListOption } from './PromotedLinkTypes';
|
||||
|
||||
export class PromotedLinksDataService {
|
||||
public constructor(private context: WebPartContext) { }
|
||||
|
||||
public getLists(): Promise<IPromotedLinksListOption[]> {
|
||||
const url: string = this.webUrl +
|
||||
'/_api/web/lists?$select=Id,Title,BaseTemplate&$filter=Hidden eq false and BaseTemplate eq 170&$orderby=Title';
|
||||
return this.getJson(url).then((payload: any): IPromotedLinksListOption[] => // tslint:disable-line:no-any
|
||||
this.results(payload).map((list: any): IPromotedLinksListOption => ({ // tslint:disable-line:no-any
|
||||
id: this.cleanGuid(list.Id),
|
||||
title: String(list.Title || list.Id)
|
||||
})));
|
||||
}
|
||||
|
||||
public getItems(listId: string): Promise<IPromotedLinkItem[]> {
|
||||
if (!this.isGuid(listId)) { return Promise.reject(new Error('Invalid list ID.')); }
|
||||
const fields: string = 'Id,Title,Description,BackgroundImageLocation,LinkLocation,LaunchBehavior,TileOrder';
|
||||
const url: string = this.webUrl + "/_api/web/lists(guid'" + this.cleanGuid(listId) + "')/items" +
|
||||
'?$select=' + fields + '&$orderby=TileOrder asc,Title asc&$top=5000';
|
||||
return this.getJson(url).then((payload: any): IPromotedLinkItem[] => // tslint:disable-line:no-any
|
||||
this.results(payload).map((item: any): IPromotedLinkItem => ({ // tslint:disable-line:no-any
|
||||
id: Number(item.Id),
|
||||
title: String(item.Title || ''),
|
||||
description: String(item.Description || ''),
|
||||
linkUrl: normalizeNavigationUrl(this.urlValue(item.LinkLocation), this.webUrl),
|
||||
imageUrl: normalizeImageUrl(this.urlValue(item.BackgroundImageLocation), this.webUrl),
|
||||
launchBehavior: String(item.LaunchBehavior || ''),
|
||||
tileOrder: Number(item.TileOrder) || 0
|
||||
})).filter((item: IPromotedLinkItem): boolean => !!item.linkUrl));
|
||||
}
|
||||
|
||||
private get webUrl(): string { return this.context.pageContext.web.absoluteUrl.replace(/\/$/, ''); }
|
||||
private get headers(): any { return { 'Accept': 'application/json;odata=verbose', 'OData-Version': '3.0' }; } // tslint:disable-line:no-any
|
||||
|
||||
private getJson(url: string): Promise<any> { // tslint:disable-line:no-any
|
||||
return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.headers })
|
||||
.then((response: SPHttpClientResponse): Promise<any> => { // tslint:disable-line:no-any
|
||||
if (response.ok) { return response.json(); }
|
||||
return response.text().then((text: string): Promise<any> => // tslint:disable-line:no-any
|
||||
Promise.reject(new Error('SharePoint request failed (' + response.status + '): ' + text)));
|
||||
});
|
||||
}
|
||||
|
||||
private results(payload: any): any[] { // tslint:disable-line:no-any
|
||||
const source: any = payload && payload.d !== undefined ? payload.d : payload; // tslint:disable-line:no-any
|
||||
return source && Array.isArray(source.results) ? source.results : (Array.isArray(source) ? source : []);
|
||||
}
|
||||
private urlValue(value: any): string { // tslint:disable-line:no-any
|
||||
if (!value) { return ''; }
|
||||
if (typeof value === 'string') { return value; }
|
||||
return String(value.Url || value.url || '');
|
||||
}
|
||||
private isGuid(value: string): boolean { return /^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(this.cleanGuid(value)); }
|
||||
private cleanGuid(value: string): string { return String(value || '').replace(/[{}]/g, '').toLowerCase(); }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { IPromotedLinkItem } from '../PromotedLinkTypes';
|
||||
|
||||
export interface IModernPromotedLinksProps {
|
||||
items: IPromotedLinkItem[];
|
||||
columns: number;
|
||||
tileHeight: number;
|
||||
showDescriptions: boolean;
|
||||
dialogWindowLabel: string;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
|
||||
.modernPromotedLinks {
|
||||
color: "[theme:bodyText, default:#323130]";
|
||||
font-family: "Segoe UI", "Segoe UI Web (West European)", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.columns1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||
.columns2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.columns3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.columns4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.columns5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
||||
.columns6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
||||
|
||||
.tile {
|
||||
background: "[theme:themePrimary, default:#0078d4]";
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.link {
|
||||
background-color: "[theme:themePrimary, default:#0078d4]";
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
color: #fff;
|
||||
display: block;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
transition: transform 120ms ease-out;
|
||||
|
||||
&:hover { color: #fff; transform: scale(1.015); }
|
||||
&:focus { color: #fff; outline: 2px solid "[theme:focusBorder, default:#605e5c]"; outline-offset: -2px; }
|
||||
}
|
||||
|
||||
.fallbackIcon {
|
||||
font-size: 42px;
|
||||
left: 20px;
|
||||
opacity: .75;
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, .05), rgba(0, 0, 0, .86));
|
||||
bottom: 0;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
left: 0;
|
||||
padding: 28px 16px 14px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.title { display: block; font-size: 18px; font-weight: 600; line-height: 24px; }
|
||||
.description {
|
||||
display: -webkit-box;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
margin-top: 3px;
|
||||
max-height: 36px;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.status { color: "[theme:bodyText, default:#323130]"; padding: 16px; }
|
||||
.error { color: "[theme:errorText, default:#a80000]"; padding: 16px; }
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.columns4, .columns5, .columns6 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.columns3, .columns4, .columns5, .columns6 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.columns2, .columns3, .columns4, .columns5, .columns6 { grid-template-columns: minmax(0, 1fr); }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as React from 'react';
|
||||
import { isDialogLaunch, isNewTabLaunch } from '../PromotedLinkCore';
|
||||
import { IPromotedLinkItem } from '../PromotedLinkTypes';
|
||||
import styles = require('./ModernPromotedLinks.module.scss');
|
||||
import { IModernPromotedLinksProps } from './IModernPromotedLinksProps';
|
||||
|
||||
export default class ModernPromotedLinks extends React.Component<IModernPromotedLinksProps, {}> {
|
||||
public render(): React.ReactElement<IModernPromotedLinksProps> {
|
||||
const gridClass: string = styles.grid + ' ' + styles['columns' + this.props.columns];
|
||||
return (
|
||||
<div className={styles.modernPromotedLinks}>
|
||||
<ul className={gridClass} role='list'>
|
||||
{this.props.items.map((item: IPromotedLinkItem): JSX.Element => this.renderTile(item))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderTile(item: IPromotedLinkItem): JSX.Element {
|
||||
const newTab: boolean = isNewTabLaunch(item.launchBehavior);
|
||||
const dialog: boolean = isDialogLaunch(item.launchBehavior);
|
||||
const imageStyle: React.CSSProperties = item.imageUrl ? { backgroundImage: 'url("' + this.cssUrl(item.imageUrl) + '")' } : {};
|
||||
const label: string = item.description ? item.title + '. ' + item.description : item.title;
|
||||
return (
|
||||
<li key={item.id} className={styles.tile} style={{ height: this.props.tileHeight + 'px' }}>
|
||||
<a className={styles.link} href={item.linkUrl} style={imageStyle} aria-label={label}
|
||||
target={newTab ? '_blank' : undefined} rel={newTab ? 'noopener noreferrer' : undefined}
|
||||
onClick={dialog ? (event: React.MouseEvent<HTMLAnchorElement>): void => this.openDialog(event, item) : undefined}>
|
||||
{!item.imageUrl && <span className={styles.fallbackIcon} aria-hidden='true'>↗</span>}
|
||||
<span className={styles.overlay}>
|
||||
<span className={styles.title}>{item.title || item.linkUrl}</span>
|
||||
{this.props.showDescriptions && item.description && <span className={styles.description}>{item.description}</span>}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
private openDialog(event: React.MouseEvent<HTMLAnchorElement>, item: IPromotedLinkItem): void {
|
||||
event.preventDefault();
|
||||
const width: number = Math.min(1100, Math.max(640, window.innerWidth - 80));
|
||||
const height: number = Math.min(800, Math.max(480, window.innerHeight - 80));
|
||||
const child: Window = window.open(item.linkUrl, this.props.dialogWindowLabel + item.id,
|
||||
'width=' + width + ',height=' + height + ',resizable=yes,scrollbars=yes') as Window;
|
||||
if (child) { child.opener = null; }
|
||||
}
|
||||
|
||||
private cssUrl(value: string): string {
|
||||
return String(value || '').replace(/["'()\\\r\n]/g, '');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
define([], function() {
|
||||
return {
|
||||
PropertyPaneDescription: 'Wählen Sie eine klassische Liste "Höher gestufte Links" aus und konfigurieren Sie die Kacheln.',
|
||||
DataGroupName: 'Datenquelle',
|
||||
LayoutGroupName: 'Darstellung',
|
||||
ListFieldLabel: 'Liste "Höher gestufte Links"',
|
||||
ColumnsFieldLabel: 'Maximale Spaltenanzahl',
|
||||
TileHeightFieldLabel: 'Kachelhöhe (px)',
|
||||
ShowDescriptionsFieldLabel: 'Beschreibungen anzeigen',
|
||||
SelectOption: 'Liste auswählen',
|
||||
ConfigureMessage: 'Bearbeiten Sie das WebPart und wählen Sie eine Liste "Höher gestufte Links" aus.',
|
||||
LoadingMessage: 'Links werden geladen…',
|
||||
EmptyMessage: 'Die ausgewählte Liste enthält keine gültigen Links.',
|
||||
LoadErrorMessage: 'Die Links konnten nicht geladen werden.',
|
||||
DialogWindowLabel: 'PromotedLinkDialog'
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
define([], function() {
|
||||
return {
|
||||
PropertyPaneDescription: 'Select a classic Promoted Links list and configure the tile layout.',
|
||||
DataGroupName: 'Data source',
|
||||
LayoutGroupName: 'Layout',
|
||||
ListFieldLabel: 'Promoted Links list',
|
||||
ColumnsFieldLabel: 'Maximum columns',
|
||||
TileHeightFieldLabel: 'Tile height (px)',
|
||||
ShowDescriptionsFieldLabel: 'Show descriptions',
|
||||
SelectOption: 'Select a list',
|
||||
ConfigureMessage: 'Edit the web part and select a Promoted Links list.',
|
||||
LoadingMessage: 'Loading promoted links…',
|
||||
EmptyMessage: 'The selected list contains no valid promoted links.',
|
||||
LoadErrorMessage: 'The promoted links could not be loaded.',
|
||||
DialogWindowLabel: 'PromotedLinkDialog'
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
declare interface IModernPromotedLinksWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
DataGroupName: string;
|
||||
LayoutGroupName: string;
|
||||
ListFieldLabel: string;
|
||||
ColumnsFieldLabel: string;
|
||||
TileHeightFieldLabel: string;
|
||||
ShowDescriptionsFieldLabel: string;
|
||||
SelectOption: string;
|
||||
ConfigureMessage: string;
|
||||
LoadingMessage: string;
|
||||
EmptyMessage: string;
|
||||
LoadErrorMessage: string;
|
||||
DialogWindowLabel: string;
|
||||
}
|
||||
|
||||
declare module 'ModernPromotedLinksWebPartStrings' {
|
||||
const strings: IModernPromotedLinksWebPartStrings;
|
||||
export = strings;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
isDialogLaunch,
|
||||
isNewTabLaunch,
|
||||
normalizeColumns,
|
||||
normalizeImageUrl,
|
||||
normalizeNavigationUrl,
|
||||
normalizeTileHeight
|
||||
} from '../src/webparts/modernPromotedLinks/PromotedLinkCore';
|
||||
|
||||
declare const console: { log(message: string): void };
|
||||
|
||||
let passed: number = 0;
|
||||
function assert(condition: boolean, message: string): void {
|
||||
if (!condition) { throw new Error(message); }
|
||||
passed++;
|
||||
}
|
||||
|
||||
const webUrl: string = 'http://portal.contoso.local/sites/demo';
|
||||
assert(normalizeNavigationUrl('Pages/Start.aspx', webUrl) === webUrl + '/Pages/Start.aspx', 'Relative links must be resolved.');
|
||||
assert(normalizeNavigationUrl('/sites/demo', webUrl) === '/sites/demo', 'Server-relative links must be retained.');
|
||||
assert(normalizeNavigationUrl('mailto:test@example.com', webUrl) === 'mailto:test@example.com', 'Mail links must be supported.');
|
||||
assert(normalizeNavigationUrl('javascript:alert(1)', webUrl) === '', 'JavaScript URLs must be rejected.');
|
||||
assert(normalizeNavigationUrl('data:text/html,test', webUrl) === '', 'Data URLs must be rejected.');
|
||||
assert(normalizeImageUrl('mailto:test@example.com', webUrl) === '', 'Special navigation URLs must not be accepted as images.');
|
||||
assert(normalizeImageUrl('/SiteAssets/tile.jpg', webUrl) === '/SiteAssets/tile.jpg', 'Server-relative image URLs must be retained.');
|
||||
assert(isNewTabLaunch('New tab'), 'New tab launch behavior must be recognized.');
|
||||
assert(isNewTabLaunch('New window'), 'New window launch behavior must be recognized.');
|
||||
assert(isDialogLaunch('Dialog'), 'Dialog launch behavior must be recognized.');
|
||||
assert(normalizeColumns(9) === 6 && normalizeColumns(0) === 4, 'Column values must be normalized.');
|
||||
assert(normalizeTileHeight(80) === 120 && normalizeTileHeight(400) === 320, 'Tile height must be normalized.');
|
||||
|
||||
console.log('Promoted link core tests passed: ' + passed);
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "../temp/tests",
|
||||
"rootDir": "..",
|
||||
"lib": ["es5", "dom", "es2015.promise"],
|
||||
"types": [],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"files": [
|
||||
"../src/webparts/modernPromotedLinks/PromotedLinkCore.ts",
|
||||
"PromotedLinkCore.test.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var root = path.resolve(__dirname, '..');
|
||||
var passed = 0;
|
||||
|
||||
function read(file) { return fs.readFileSync(path.join(root, file), 'utf8'); }
|
||||
function json(file) { return JSON.parse(read(file)); }
|
||||
function assert(condition, message) { if (!condition) { throw new Error(message); } passed++; }
|
||||
|
||||
var packageJson = json('package.json');
|
||||
var packageSolution = json('config/package-solution.json');
|
||||
var solution = packageSolution.solution;
|
||||
var config = json('config/config.json');
|
||||
var manifest = json('src/webparts/modernPromotedLinks/ModernPromotedLinksWebPart.manifest.json');
|
||||
var webPart = read('src/webparts/modernPromotedLinks/ModernPromotedLinksWebPart.ts');
|
||||
var service = read('src/webparts/modernPromotedLinks/PromotedLinksDataService.ts');
|
||||
var component = read('src/webparts/modernPromotedLinks/components/ModernPromotedLinks.tsx');
|
||||
var core = read('src/webparts/modernPromotedLinks/PromotedLinkCore.ts');
|
||||
var styles = read('src/webparts/modernPromotedLinks/components/ModernPromotedLinks.module.scss');
|
||||
|
||||
assert(packageJson.name === 'modern-promoted-link-list-webpart', 'npm package name is inconsistent.');
|
||||
assert(solution.name === packageJson.name && solution.title === packageJson.name, 'Solution naming is inconsistent.');
|
||||
assert(packageSolution.paths.zippedPackage === 'solution/modern-promoted-link-list-webpart.sppkg', 'Package file name is inconsistent.');
|
||||
assert(solution.includeClientSideAssets === true && solution.skipFeatureDeployment === true, 'Central asset deployment is not configured.');
|
||||
assert(manifest.componentType === 'WebPart' && manifest.alias === 'ModernPromotedLinksWebPart', 'WebPart manifest is invalid.');
|
||||
assert(!!config.bundles['modern-promoted-link-list-webpart'], 'Bundle name is inconsistent.');
|
||||
assert(service.indexOf('BaseTemplate eq 170') >= 0, 'Promoted Links list filtering is missing.');
|
||||
['BackgroundImageLocation', 'LinkLocation', 'LaunchBehavior', 'TileOrder'].forEach(function(field) {
|
||||
assert(service.indexOf(field) >= 0, 'Classic Promoted Links field is missing: ' + field);
|
||||
});
|
||||
assert(webPart.indexOf("PropertyPaneDropdown('listId'") >= 0, 'List picker is missing.');
|
||||
assert(webPart.indexOf("PropertyPaneSlider('columns'") >= 0 && webPart.indexOf("PropertyPaneSlider('tileHeight'") >= 0,
|
||||
'Layout controls are missing.');
|
||||
assert(component.indexOf("role='list'") >= 0 && component.indexOf('aria-label={label}') >= 0, 'Accessible list semantics are missing.');
|
||||
assert(core.indexOf("/^[a-z][a-z0-9+.-]*:/i") >= 0, 'Unsafe URL schemes are not rejected.');
|
||||
['bodyText', 'themePrimary', 'focusBorder', 'errorText'].forEach(function(slot) {
|
||||
assert(styles.indexOf('[theme:' + slot + ', default:') >= 0, 'SharePoint theme slot is missing: ' + slot);
|
||||
});
|
||||
|
||||
console.log('Modern Promoted Link List project validation passed: ' + passed);
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"typeRoots": ["./node_modules/@types", "./node_modules/@microsoft"],
|
||||
"types": ["es6-promise", "webpack-env"],
|
||||
"lib": ["es5", "dom", "es2015.collection"]
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
|
||||
"rules": {
|
||||
"class-name": false,
|
||||
"export-name": false,
|
||||
"forin": false,
|
||||
"label-position": false,
|
||||
"member-access": true,
|
||||
"no-arg": false,
|
||||
"no-console": false,
|
||||
"no-construct": false,
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": false,
|
||||
"no-function-expression": true,
|
||||
"no-internal-module": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-unnecessary-semicolons": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-with-statement": true,
|
||||
"semicolon": true,
|
||||
"trailing-comma": false,
|
||||
"typedef": false,
|
||||
"typedef-whitespace": false,
|
||||
"use-named-parameter": true,
|
||||
"variable-name": false,
|
||||
"whitespace": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user