├── .gitattributes ├── typings ├── tsd.d.ts └── @ms │ └── odsp.d.ts ├── config ├── copy-assets.json ├── write-manifests.json ├── deploy-azure-storage.json ├── serve.json ├── config.json ├── package-solution.json └── tslint.json ├── src ├── components │ ├── MegaMenu.module.scss │ ├── FlyoutColumn.module.scss │ ├── MobileMenu.module.scss │ ├── TopLevelMenu.module.scss │ ├── Common.scss │ ├── MenuLink.module.scss │ ├── Flyout.module.scss │ ├── FlyoutColumnHeading.module.scss │ ├── MenuLink.tsx │ ├── MobileMenu.tsx │ ├── FlyoutColumnHeading.tsx │ ├── Flyout.tsx │ ├── FlyoutColumn.tsx │ ├── TopLevelMenu.tsx │ └── MegaMenu.tsx ├── extensions │ └── modernSpMegaMenu │ │ ├── loc │ │ ├── en-us.js │ │ └── myStrings.d.ts │ │ ├── ModernSpMegaMenuApplicationCustomizer.manifest.json │ │ └── ModernSpMegaMenuApplicationCustomizer.ts ├── model │ ├── Link.ts │ ├── TopLevelMenu.ts │ └── FlyoutColumn.ts └── service │ ├── MegaMenuService.ts │ └── MegaMenuSampleData.ts ├── pics ├── Desktop.png ├── MobileExpanded.png ├── DesktopAnnotated.png └── MobileContracted.png ├── gulpfile.js ├── .npmignore ├── .yo-rc.json ├── tsconfig.json ├── sharepoint └── assets │ └── elements.xml ├── .gitignore ├── .editorconfig ├── package.json ├── .vscode ├── launch.json └── settings.json ├── README.md └── provisioning └── create-lists.ps1 /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /typings/tsd.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /config/copy-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "deployCdnPath": "temp/deploy" 3 | } 4 | -------------------------------------------------------------------------------- /src/components/MegaMenu.module.scss: -------------------------------------------------------------------------------- 1 | .null { 2 | position:fixed; 3 | } -------------------------------------------------------------------------------- /src/components/FlyoutColumn.module.scss: -------------------------------------------------------------------------------- 1 | .null { 2 | position:fixed; 3 | } -------------------------------------------------------------------------------- /pics/Desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lee-borlace/modern-sp-mega-menu/HEAD/pics/Desktop.png -------------------------------------------------------------------------------- /pics/MobileExpanded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lee-borlace/modern-sp-mega-menu/HEAD/pics/MobileExpanded.png -------------------------------------------------------------------------------- /src/components/MobileMenu.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding-top: 5px; 3 | padding-bottom: 5px; 4 | } -------------------------------------------------------------------------------- /pics/DesktopAnnotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lee-borlace/modern-sp-mega-menu/HEAD/pics/DesktopAnnotated.png -------------------------------------------------------------------------------- /pics/MobileContracted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lee-borlace/modern-sp-mega-menu/HEAD/pics/MobileContracted.png -------------------------------------------------------------------------------- /src/components/TopLevelMenu.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding-top: 6px; 3 | padding-bottom: 6px; 4 | cursor: pointer; 5 | } -------------------------------------------------------------------------------- /config/write-manifests.json: -------------------------------------------------------------------------------- 1 | { 2 | "cdnBasePath": "https://publiccdn.sharepointonline.com/lee79trial.sharepoint.com/CDN/ModernPageGaApplication" 3 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gulp = require('gulp'); 4 | const build = require('@microsoft/sp-build-web'); 5 | 6 | build.initialize(gulp); 7 | -------------------------------------------------------------------------------- /src/components/Common.scss: -------------------------------------------------------------------------------- 1 | $desktop-width: 640px; 2 | 3 | 4 | @mixin desktop { 5 | @media (min-width: #{$desktop-width}) { 6 | @content; 7 | } 8 | } -------------------------------------------------------------------------------- /src/extensions/modernSpMegaMenu/loc/en-us.js: -------------------------------------------------------------------------------- 1 | define([], function() { 2 | return { 3 | "Title": "ModernSpMegaMenuApplicationCustomizer" 4 | } 5 | }); -------------------------------------------------------------------------------- /src/components/MenuLink.module.scss: -------------------------------------------------------------------------------- 1 | .link { 2 | padding-bottom: 5px; 3 | display:block; 4 | } 5 | 6 | .link, .link:visited, .link:hover, .link:active { 7 | color: inherit; 8 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | .vscode 3 | coverage 4 | node_modules 5 | sharepoint 6 | src 7 | temp 8 | 9 | # Files 10 | *.csproj 11 | .git* 12 | .yo-rc.json 13 | gulpfile.js 14 | tsconfig.json 15 | -------------------------------------------------------------------------------- /config/deploy-azure-storage.json: -------------------------------------------------------------------------------- 1 | { 2 | "workingDir": "./temp/deploy/", 3 | "account": "", 4 | "container": "modern-sp-mega-menu", 5 | "accessKey": "" 6 | } -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "@microsoft/generator-sharepoint": { 3 | "version": "1.1.1", 4 | "libraryName": "modern-sp-mega-menu", 5 | "libraryId": "62ed767d-26fc-4ffe-8f31-b174537d545e", 6 | "environment": "spo" 7 | } 8 | } -------------------------------------------------------------------------------- /src/model/Link.ts: -------------------------------------------------------------------------------- 1 | // A HTML hyperlink. URL and opening in new tab are optional. 2 | export class Link { 3 | public level2ParentId?: number; 4 | public text:string; 5 | public url?:string; 6 | public openInNewTab?:boolean; 7 | } -------------------------------------------------------------------------------- /src/model/TopLevelMenu.ts: -------------------------------------------------------------------------------- 1 | import { FlyoutColumn } from './FlyoutColumn'; 2 | 3 | // Top-level mega menu item. 4 | export class TopLevelMenu { 5 | public id: number; 6 | public text: string; 7 | public columns?: FlyoutColumn[]; 8 | } -------------------------------------------------------------------------------- /src/extensions/modernSpMegaMenu/loc/myStrings.d.ts: -------------------------------------------------------------------------------- 1 | declare interface IModernSpMegaMenuStrings { 2 | Title: string; 3 | } 4 | 5 | declare module 'modernSpMegaMenuStrings' { 6 | const strings: IModernSpMegaMenuStrings; 7 | export = strings; 8 | } 9 | -------------------------------------------------------------------------------- /config/serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 4321, 3 | "initialPage": "https://localhost:5432/workbench", 4 | "https": true, 5 | "api": { 6 | "port": 5432, 7 | "entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Flyout.module.scss: -------------------------------------------------------------------------------- 1 | @import 'Common'; 2 | 3 | .container { 4 | 5 | cursor: pointer; 6 | 7 | @include desktop { 8 | padding-top: 30px; 9 | padding-bottom: 30px; 10 | position:fixed; 11 | z-index:5000; 12 | } 13 | } -------------------------------------------------------------------------------- /src/model/FlyoutColumn.ts: -------------------------------------------------------------------------------- 1 | import {Link} from './Link'; 2 | 3 | // A column of links to show in the mega menu flyout. Each column has a header with optional link, and a number of links to show under it. 4 | export class FlyoutColumn { 5 | public id:number; 6 | public level1ParentId: number; 7 | public heading?:Link; 8 | public links?:Link[]; 9 | } -------------------------------------------------------------------------------- /src/components/FlyoutColumnHeading.module.scss: -------------------------------------------------------------------------------- 1 | .headingLink, .headingNoLink { 2 | padding-bottom: 10px; 3 | display:block; 4 | } 5 | 6 | .headingLink, .headingLink:visited, .headingLink:hover, .headingLink:active, .headingNoLink, .headingNoLink:visited, .headingNoLink:hover, .headingNoLink:active { 7 | color: inherit; 8 | } 9 | 10 | .mobile { 11 | font-style: italic; 12 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "forceConsistentCasingInFileNames": true, 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "declaration": true, 8 | "sourceMap": true, 9 | "experimentalDecorators": true, 10 | "types": [ 11 | "es6-promise", 12 | "es6-collections", 13 | "webpack-env" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /typings/@ms/odsp.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for Microsoft ODSP projects 2 | // Project: ODSP 3 | 4 | /* Global definition for UNIT_TEST builds 5 | Code that is wrapped inside an if(UNIT_TEST) {...} 6 | block will not be included in the final bundle when the 7 | --ship flag is specified */ 8 | declare const UNIT_TEST: boolean; 9 | 10 | /* Global defintion for SPO builds */ 11 | declare const DATACENTER: boolean; -------------------------------------------------------------------------------- /sharepoint/assets/elements.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependency directories 7 | node_modules 8 | 9 | # Build generated files 10 | dist 11 | lib 12 | solution 13 | temp 14 | *.sppkg 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # OSX 20 | .DS_Store 21 | 22 | # Visual Studio files 23 | .ntvs_analysis.dat 24 | .vs 25 | bin 26 | obj 27 | 28 | # Resx Generated Code 29 | *.resx.ts 30 | 31 | # Styles Generated Code 32 | *.scss.ts 33 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "entry": "./lib/extensions/modernSpMegaMenu/ModernSpMegaMenuApplicationCustomizer.js", 5 | "manifest": "./src/extensions/modernSpMegaMenu/ModernSpMegaMenuApplicationCustomizer.manifest.json", 6 | "outputPath": "./dist/modern-sp-mega-menu.bundle.js" 7 | } 8 | ], 9 | "externals": {}, 10 | "localizedResources": { 11 | "modernSpMegaMenuStrings": "extensions/modernSpMegaMenu/loc/{locale}.js" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # we recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | 23 | [{package,bower}.json] 24 | indent_style = space 25 | indent_size = 2 -------------------------------------------------------------------------------- /src/extensions/modernSpMegaMenu/ModernSpMegaMenuApplicationCustomizer.manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json", 3 | 4 | "id": "c8e19244-8f9d-45d9-806e-1b1d71ed53fb", 5 | "alias": "ModernSpMegaMenuApplicationCustomizer", 6 | "componentType": "Extension", 7 | "extensionType": "ApplicationCustomizer", 8 | "version": "*", // The "*" signifies that the version should be taken from the package.json 9 | "manifestVersion": 2, 10 | "safeWithCustomScriptDisabled": true 11 | } 12 | -------------------------------------------------------------------------------- /config/package-solution.json: -------------------------------------------------------------------------------- 1 | { 2 | "solution": { 3 | "name": "Mega Menu for Modern SP", 4 | "id": "62ed767d-26fc-4ffe-8f31-b174537d545e", 5 | "version": "1.0.0.1", 6 | "features": [ 7 | { 8 | "title": "Mega Menu for Modern SP", 9 | "description": "Mega Menu for Modern SP", 10 | "id": "c93e2e29-6e44-4c4e-bbe5-23fe34ec200b", 11 | "version": "1.0.0.1", 12 | "assets": { 13 | "elementManifests": [ 14 | "elements.xml" 15 | ] 16 | } 17 | } 18 | ] 19 | 20 | }, 21 | "paths": { 22 | "zippedPackage": "solution/modern-sp-mega-menu.sppkg" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modern-sp-mega-menu", 3 | "version": "0.0.1", 4 | "private": true, 5 | "engines": { 6 | "node": ">=0.10.0" 7 | }, 8 | "dependencies": { 9 | "@microsoft/sp-application-base": "1.1.1", 10 | "@microsoft/sp-core-library": "~1.1.0", 11 | "@microsoft/sp-listview-extensibility": "0.1.1", 12 | "@microsoft/sp-webpart-base": "~1.1.1", 13 | "@types/webpack-env": ">=1.12.1 <1.14.0", 14 | "sp-pnp-js": "^2.0.7" 15 | }, 16 | "devDependencies": { 17 | "@microsoft/sp-build-web": "~1.1.0", 18 | "@microsoft/sp-module-interfaces": "~1.1.0", 19 | "@microsoft/sp-webpart-workbench": "~1.1.0", 20 | "gulp": "~3.9.1", 21 | "@types/chai": ">=3.4.34 <3.6.0", 22 | "@types/mocha": ">=2.2.33 <2.6.0" 23 | }, 24 | "scripts": { 25 | "build": "gulp bundle", 26 | "clean": "gulp clean", 27 | "test": "gulp test" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "name": "In-site debugging", 10 | "type": "chrome", 11 | "request": "launch", 12 | "url": "https://lee79trial.sharepoint.com/sites/Site13/_layouts/15/viewlsts.aspx?view=14", 13 | "webRoot": "${workspaceRoot}", 14 | "sourceMaps": true, 15 | "sourceMapPathOverrides": { 16 | "webpack:///../../../src/*": "${webRoot}/src/*", 17 | "webpack:///../../../../src/*": "${webRoot}/src/*", 18 | "webpack:///../../../../../src/*": "${webRoot}/src/*" 19 | }, 20 | "runtimeArgs": [ 21 | "--remote-debugging-port=9222" 22 | ] 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/components/MenuLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'office-ui-fabric-react/lib/Link'; 3 | 4 | import styles from './MenuLink.module.scss'; 5 | 6 | import { TopLevelMenu as TopLevelMenuModel } from '../model/TopLevelMenu'; 7 | import { FlyoutColumn as FlyoutColumnModel } from '../model/FlyoutColumn'; 8 | import { Link as LinkModel } from '../model/Link'; 9 | 10 | export interface IMenuLinkProps { 11 | item:LinkModel; 12 | mobileMode: boolean; 13 | } 14 | 15 | export interface IMenuLinkState { 16 | } 17 | 18 | export class MenuLink extends React.Component { 19 | 20 | constructor(props) { 21 | super(props); 22 | } 23 | 24 | public render(): React.ReactElement { 25 | 26 | return ( 27 | 32 | {this.props.item.text} 33 | 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/MobileMenu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './MobileMenu.module.scss'; 3 | import { withResponsiveMode, ResponsiveMode } from 'office-ui-fabric-react/lib/utilities/decorators/withResponsiveMode'; 4 | 5 | export interface IMobileMenuProps { 6 | handleTouched: () => void; 7 | responsiveMode?: ResponsiveMode; 8 | } 9 | 10 | export interface IMobileMenuState { 11 | } 12 | 13 | @withResponsiveMode 14 | export class MobileMenu extends React.Component { 15 | 16 | constructor(props) { 17 | super(props); 18 | } 19 | 20 | public render(): React.ReactElement { 21 | 22 | return ( 23 |
27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 | ); 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /config/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | // Display errors as warnings 3 | "displayAsWarning": true, 4 | // The TSLint task may have been configured with several custom lint rules 5 | // before this config file is read (for example lint rules from the tslint-microsoft-contrib 6 | // project). If true, this flag will deactivate any of these rules. 7 | "removeExistingRules": true, 8 | // When true, the TSLint task is configured with some default TSLint "rules.": 9 | "useDefaultConfigAsBase": false, 10 | // Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules 11 | // which are active, other than the list of rules below. 12 | "lintConfig": { 13 | // Opt-in to Lint rules which help to eliminate bugs in JavaScript 14 | "rules": { 15 | "class-name": false, 16 | "export-name": false, 17 | "forin": false, 18 | "label-position": false, 19 | "member-access": true, 20 | "no-arg": false, 21 | "no-console": false, 22 | "no-construct": false, 23 | "no-duplicate-case": true, 24 | "no-duplicate-variable": true, 25 | "no-eval": false, 26 | "no-function-expression": true, 27 | "no-internal-module": true, 28 | "no-shadowed-variable": true, 29 | "no-switch-case-fall-through": true, 30 | "no-unnecessary-semicolons": true, 31 | "no-unused-expression": true, 32 | "no-unused-imports": true, 33 | "no-use-before-declare": true, 34 | "no-with-statement": true, 35 | "semicolon": true, 36 | "trailing-comma": false, 37 | "typedef": false, 38 | "typedef-whitespace": false, 39 | "use-named-parameter": true, 40 | "valid-typeof": true, 41 | "variable-name": false, 42 | "whitespace": false 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/components/FlyoutColumnHeading.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'office-ui-fabric-react/lib/Link'; 3 | 4 | import styles from './FlyoutColumnHeading.module.scss'; 5 | 6 | import { TopLevelMenu as TopLevelMenuModel } from '../model/TopLevelMenu'; 7 | import { FlyoutColumn as FlyoutColumnModel } from '../model/FlyoutColumn'; 8 | import { Link as LinkModel } from '../model/Link'; 9 | 10 | export interface IFlyoutColumnHeadingProps { 11 | item: LinkModel; 12 | mobileMode: boolean; 13 | headingTouched: () => void; 14 | } 15 | 16 | export interface IFlyoutColumnHeadingState { 17 | } 18 | 19 | export class FlyoutColumnHeading extends React.Component { 20 | 21 | constructor(props) { 22 | super(props); 23 | } 24 | 25 | public render(): React.ReactElement { 26 | 27 | // Heading has a link, and we're not in mobile mode. 28 | if (this.props.item.url && !this.props.mobileMode) { 29 | return ( 30 | 36 | {this.props.item.text} 37 | 38 | ); 39 | } 40 | // Heading is just text, or we're in mobile mode. In mobile mode we render as just a heading so it can be clicked to expand 41 | // and view level 3 items instead of navigating to item. TODO : better approach which allows link to be navigated 42 | // and also to expand the sub-items. 43 | else { 44 | return ( 45 |
49 | {this.props.item.text} 50 |
51 | ); 52 | } 53 | 54 | } 55 | 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/extensions/modernSpMegaMenu/ModernSpMegaMenuApplicationCustomizer.ts: -------------------------------------------------------------------------------- 1 | import { override } from '@microsoft/decorators'; 2 | import { Log } from '@microsoft/sp-core-library'; 3 | import { 4 | BaseApplicationCustomizer, 5 | Placeholder 6 | } from '@microsoft/sp-application-base'; 7 | import * as React from 'react'; 8 | import * as ReactDom from 'react-dom'; 9 | 10 | import * as strings from 'modernSpMegaMenuStrings'; 11 | import { MegaMenu, IMegaMenuProps } from '../../components/MegaMenu'; 12 | import { MegaMenuService } from '../../service/MegaMenuService'; 13 | import { TopLevelMenu } from '../../model/TopLevelMenu'; 14 | 15 | export interface IModernSpMegaMenuApplicationCustomizerProperties { 16 | } 17 | 18 | export default class ModernSpMegaMenuApplicationCustomizer 19 | extends BaseApplicationCustomizer { 20 | 21 | private headerPlaceholder: Placeholder; 22 | 23 | @override 24 | public onInit(): Promise { 25 | return Promise.resolve(); 26 | } 27 | 28 | @override 29 | public onRender(): void { 30 | 31 | if (!this.headerPlaceholder) { 32 | this.headerPlaceholder = this.context.placeholders.tryAttach( 33 | 'PageHeader', 34 | { 35 | onDispose: this._onDispose 36 | }); 37 | 38 | if (this.headerPlaceholder) { 39 | if (this.headerPlaceholder.domElement) { 40 | 41 | console.log("PageHeader placeholder is OK.") 42 | 43 | 44 | MegaMenuService.getMenuItems(this.context.pageContext.site.absoluteUrl) 45 | .then((topLevelMenus: TopLevelMenu[]) => { 46 | 47 | const element: React.ReactElement = React.createElement( 48 | MegaMenu, 49 | { 50 | topLevelMenuItems: topLevelMenus 51 | }); 52 | 53 | ReactDom.render(element, this.headerPlaceholder.domElement); 54 | 55 | }) 56 | .catch((error: any) => { 57 | console.error(`Error trying to read menu items or render component : ${error.message}`); 58 | }); 59 | 60 | } else { 61 | console.error('PageHeader placeholder has no DOM element.'); 62 | } 63 | } 64 | else { 65 | console.error('PageHeader placeholder not found.'); 66 | } 67 | 68 | } 69 | 70 | 71 | } 72 | 73 | // dispose code would go here 74 | private _onDispose(): void { 75 | } 76 | 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/components/Flyout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import styles from './Flyout.module.scss'; 4 | 5 | import { FlyoutColumn } from './FlyoutColumn'; 6 | 7 | import { TopLevelMenu as TopLevelMenuModel } from '../model/TopLevelMenu'; 8 | import { FlyoutColumn as FlyoutColumnModel } from '../model/FlyoutColumn'; 9 | import { Link as LinkModel } from '../model/Link'; 10 | 11 | export interface IFlyoutProps { 12 | topLevelItem: TopLevelMenuModel; 13 | handleFocused: (topLevelItem: TopLevelMenuModel) => void; 14 | handleLostFocus: () => void; 15 | } 16 | 17 | export interface IFlyoutState { 18 | } 19 | 20 | export class Flyout extends React.Component { 21 | 22 | constructor(props) { 23 | super(props); 24 | this.handleFocused = this.handleFocused.bind(this); 25 | } 26 | 27 | public render(): React.ReactElement { 28 | 29 | // Max width to be divided up is 66%, because we have a spacer column on each end of the set of columns, 30 | // each taking up 2/12 of the width (i.e. 4/12 total = a third of the page). 31 | const columns = this.props.topLevelItem.columns.map((column: FlyoutColumnModel) => 32 | 36 | > 37 | 38 | ); 39 | 40 | return ( 41 |
48 |
49 |
50 | 51 |
52 |
53 | 54 | {columns} 55 | 56 |
57 |
58 | 59 |
60 |
61 |
62 | ); 63 | } 64 | 65 | handleFocused() { 66 | this.props.handleFocused(this.props.topLevelItem); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | // Configure glob patterns for excluding files and folders in the file explorer. 4 | "files.exclude": { 5 | "**/.git": true, 6 | "**/.DS_Store": true, 7 | "**/bower_components": true, 8 | "**/coverage": true, 9 | "**/lib-amd": true, 10 | "src/**/*.scss.ts": true 11 | }, 12 | "typescript.tsdk": ".\\node_modules\\typescript\\lib", 13 | "json.schemas": [ 14 | { 15 | "fileMatch": [ 16 | "/config/config.json" 17 | ], 18 | "url": "./node_modules/@microsoft/sp-build-web/lib/schemas/config.schema.json" 19 | }, 20 | { 21 | "fileMatch": [ 22 | "/config/copy-assets.json" 23 | ], 24 | "url": "./node_modules/@microsoft/sp-build-core-tasks/lib/copyAssets/copy-assets.schema.json" 25 | }, 26 | { 27 | "fileMatch": [ 28 | "/config/deploy-azure-storage.json" 29 | ], 30 | "url": "./node_modules/@microsoft/sp-build-core-tasks/lib/deployAzureStorage/deploy-azure-storage.schema.json" 31 | }, 32 | { 33 | "fileMatch": [ 34 | "/config/package-solution.json" 35 | ], 36 | "url": "./node_modules/@microsoft/sp-build-core-tasks/lib/packageSolution/package-solution.schema.json" 37 | }, 38 | { 39 | "fileMatch": [ 40 | "/config/serve.json" 41 | ], 42 | "url": "./node_modules/@microsoft/gulp-core-build-serve/lib/serve.schema.json" 43 | }, 44 | { 45 | "fileMatch": [ 46 | "/config/tslint.json" 47 | ], 48 | "url": "./node_modules/@microsoft/gulp-core-build-typescript/lib/schemas/tslint.schema.json" 49 | }, 50 | { 51 | "fileMatch": [ 52 | "/config/write-manifests.json" 53 | ], 54 | "url": "./node_modules/@microsoft/sp-build-core-tasks/lib/writeManifests/write-manifests.schema.json" 55 | }, 56 | { 57 | "fileMatch": [ 58 | "/config/configure-webpack.json" 59 | ], 60 | "url": "./node_modules/@microsoft/sp-build-core-tasks/lib/configureWebpack/configure-webpack.schema.json" 61 | }, 62 | { 63 | "fileMatch": [ 64 | "/config/configure-external-bundling-webpack.json" 65 | ], 66 | "url": "./node_modules/@microsoft/sp-build-core-tasks/lib/configureWebpack/configure-webpack-external-bundling.schema.json" 67 | }, 68 | { 69 | "fileMatch": [ 70 | "/copy-static-assets.json" 71 | ], 72 | "url": "./node_modules/@microsoft/sp-build-core-tasks/lib/copyStaticAssets/copy-static-assets.schema.json" 73 | } 74 | ] 75 | } -------------------------------------------------------------------------------- /src/components/FlyoutColumn.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { withResponsiveMode, ResponsiveMode } from 'office-ui-fabric-react/lib/utilities/decorators/withResponsiveMode'; 3 | 4 | import styles from './FlyoutColumn.module.scss'; 5 | import { FlyoutColumnHeading } from './FlyoutColumnHeading'; 6 | import { MenuLink } from './MenuLink'; 7 | 8 | import { TopLevelMenu as TopLevelMenuModel } from '../model/TopLevelMenu'; 9 | import { FlyoutColumn as FlyoutColumnModel } from '../model/FlyoutColumn'; 10 | import { Link as LinkModel } from '../model/Link'; 11 | 12 | export interface IFlyoutColumnProps { 13 | header: LinkModel; 14 | links: LinkModel[]; 15 | responsiveMode?: ResponsiveMode; 16 | widthPercent: number; 17 | } 18 | 19 | export interface IFlyoutColumnState { 20 | showLinksWhenMobile: boolean; 21 | } 22 | 23 | @withResponsiveMode 24 | export class FlyoutColumn extends React.Component { 25 | 26 | constructor(props) { 27 | super(props); 28 | 29 | this.handleHeadingTouched = this.handleHeadingTouched.bind(this); 30 | 31 | this.state = { 32 | showLinksWhenMobile: false 33 | }; 34 | } 35 | 36 | public render(): React.ReactElement { 37 | 38 | var responsiveMode = this.props.responsiveMode; 39 | if (responsiveMode === undefined) { 40 | responsiveMode = ResponsiveMode.large; 41 | } 42 | var mobileMode = responsiveMode < ResponsiveMode.large; 43 | 44 | const links = !mobileMode || (mobileMode && this.state.showLinksWhenMobile) ? this.props.links.map((item: LinkModel) => 45 | 49 | 50 | ) : null; 51 | 52 | return ( 53 |
59 | 64 | {links} 65 |
66 | ); 67 | } 68 | 69 | handleHeadingTouched() { 70 | this.setState((prevState, props) => { 71 | return { 72 | showLinksWhenMobile: !prevState.showLinksWhenMobile 73 | } 74 | }); 75 | } 76 | 77 | 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/components/TopLevelMenu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { withResponsiveMode, ResponsiveMode } from 'office-ui-fabric-react/lib/utilities/decorators/withResponsiveMode'; 3 | 4 | import styles from './TopLevelMenu.module.scss'; 5 | 6 | import { TopLevelMenu as TopLevelMenuModel } from '../model/TopLevelMenu'; 7 | import { FlyoutColumn as FlyoutColumnModel } from '../model/FlyoutColumn'; 8 | import { Link as LinkModel } from '../model/Link'; 9 | 10 | export interface ITopLevelMenuProps { 11 | topLevelMenu: TopLevelMenuModel; 12 | handleFocused: (topLevelMenu: TopLevelMenuModel) => void; 13 | handleTouched: (topLevelMenu: TopLevelMenuModel) => void; 14 | handleLostFocus: () => void; 15 | selectedTopLevelMenuId: number; 16 | responsiveMode?: ResponsiveMode; 17 | widthPercent: number; 18 | } 19 | 20 | export interface ITopLevelMenuState { 21 | } 22 | 23 | @withResponsiveMode 24 | export class TopLevelMenu extends React.Component { 25 | 26 | 27 | constructor(props) { 28 | super(props); 29 | 30 | this.handleMouseEnter = this.handleMouseEnter.bind(this); 31 | this.handleMouseLeave = this.handleMouseLeave.bind(this); 32 | this.handleTouched = this.handleTouched.bind(this); 33 | } 34 | 35 | public render(): React.ReactElement { 36 | 37 | var responsiveMode = this.props.responsiveMode; 38 | if (responsiveMode === undefined) { 39 | responsiveMode = ResponsiveMode.large; 40 | } 41 | var mobileMode = responsiveMode < ResponsiveMode.large; 42 | 43 | return ( 44 |
63 | {this.props.topLevelMenu.text} 64 |
65 | ); 66 | } 67 | 68 | handleMouseEnter() { 69 | 70 | var responsiveMode = this.props.responsiveMode; 71 | if (responsiveMode === undefined) { 72 | responsiveMode = ResponsiveMode.large; 73 | } 74 | var mobileMode = responsiveMode < ResponsiveMode.large; 75 | 76 | // Only respond to on mouse enter if we're not in mobile mode. 77 | if (!mobileMode) { 78 | this.props.handleFocused(this.props.topLevelMenu); 79 | } 80 | } 81 | 82 | 83 | handleTouched() { 84 | this.props.handleTouched(this.props.topLevelMenu); 85 | } 86 | 87 | 88 | handleMouseLeave() { 89 | 90 | var responsiveMode = this.props.responsiveMode; 91 | if (responsiveMode === undefined) { 92 | responsiveMode = ResponsiveMode.large; 93 | } 94 | var mobileMode = responsiveMode < ResponsiveMode.large; 95 | 96 | // Only handle mouse leave if not in mobile mode. 97 | if (!mobileMode) { 98 | this.props.handleLostFocus(); 99 | } 100 | } 101 | 102 | 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # modern-sp-mega-menu 2 | 3 | ## Introduction 4 | _NOTE_ - This repo isn't being actively maintained! It's probably not current anymore and there are probably better ways to do things now. I put it together a couple of years ago while learning some concepts, but don't have the time to maintain it or look at issues myself. Hope you get some value nonetheless! 5 | 6 | This is a mega menu for the modern SharePoint Online experience. It uses the preview version of SharePoint Framework Extensions. See [here](https://dev.office.com/sharepoint/docs/spfx/extensions/overview-extensions) for all info about getting SPFX extensions up and running e.g. toolchain, deploying to CDN etc. 7 | 8 | The purpose of the project was to get some practice with several technologies - 9 | - SPFX 10 | - React 11 | - Office UI Fabric 12 | - Responsive Design 13 | - Typescript 14 | - SASS 15 | - PnP JS & PnP Powershell 16 | 17 | Office UI Fabric and indeed the modern SPO experiences are built on React, so it was a sound decision to use in this project and a useful technology to learn. 18 | 19 | The menu is responsive, and renders differently on desktop or smaller screens. 20 | 21 | Desktop view : 22 | 23 | ![Desktop](pics/Desktop.png "Desktop") 24 | 25 | Mobile view (contracted) : 26 | 27 | ![Mobile Contracted](pics/MobileContracted.png "Mobile Contracted") 28 | 29 | Mobile view (expanded) (*I'm not a UX person or a designer - the mobile view of the menu gives the idea, but isn't the work of Leonardo da Vinci*) : 30 | 31 | ![Mobile Expanded](pics/MobileExpanded.png "Mobile Expanded") 32 | 33 | The menu consists of 3 levels of navigation. On the desktop view, hovering over the L1 item shows the L2 and L3 items under it. In the mobile view, the L2 items are touched to view the items under them. 34 | 35 | ![Desktop](pics/DesktopAnnotated.png "Desktop") 36 | 37 | The project generates an SPFX app / add-in, which is uploaded to the app catalog in the normal way, and added to sites to take effect. Note, due to current SPFX limitations, you must install the app to any sites you want it to take effect on. Hopefully in future it will be possible to deploy once to an entire site collection. 38 | 39 | ## Configuring SPO for Mega Menu 40 | The mega menu is driven by 3 lists in the root of the site collection. These define level 1, 2 and 3 items respectively. Level 2 items look up to level 1 items, and level 3 items look up to level 2 items. The names of these lists are hard-coded and cannot be re-configured without rebuilding the code. They are "Mega Menu - Level 1", "Mega Menu - Level 2" and "Mega Menu - Level 3". 41 | 42 | The script *provisioning\create-lists.ps1* creates the 3 required lists at the root site of the site collection, and populates with initial sample data. Note this requires PnP Powershell module to be installed. See [here](https://github.com/SharePoint/PnP-PowerShell). 43 | 44 | Usage : 45 | 46 | ```powershell 47 | .\create-lists.ps1 -siteCollectionUrl 48 | ``` 49 | 50 | Now add app *Mega Menu for Modern SP* to sites as desired. 51 | 52 | ## Packaging and running locally 53 | 54 | 1. `gulp bundle` 55 | 2. `gulp package-solution` 56 | 3. `gulp serve --nobrowser` 57 | 4. Upload to app catalog 58 | 59 | ## Packaging and running from CDN 60 | 61 | 1. `gulp bundle --ship` 62 | 2. `gulp package-solution --ship` 63 | 3. Re-upload solution to app store. 64 | 4. Manually upload the files from *temp/deploy* to the CDN folder as described in SPFX docco. 65 | 66 | ## Some limitations 67 | - At this pre-release point, SPFX extensions seem to be intermittent in their ability to find particular page placeholders. If it is consistently failing to find the header placeholder (see console), try hard refreshing the page in the browser, or opening the same page in a new tab. 68 | - The components use the Fabric UI theme classes e.g. `ms-bgColor-themePrimary`. These seem to behave differently depending on which page you're on - for actual modern content pages, they use the configured theme, but for system pages e.g. View All Site Content they use a grey theme. 69 | - Due to current SPFX limitations, you must install the app to any sites you want it to take effect on. Hopefully in future it will be possible to deploy once to an entire site collection. 70 | - This solution only applies to **modern** pages in SPO. To apply to classic pages too, you would need a parallel solution to inject the 71 | same code the old-fashioned way e.g. custom action. 72 | 73 | ## How it works 74 | The entry point of the extension is class `ModernSpMegaMenuApplicationCustomizer`. The `onRender()` method looks for the "PageHeader" placeholder and inserts a `MegaMenu` React component into it. That component is then composed of other components to render out the various menu items and the menu flyout. 75 | 76 | The `MegaMenu` component has a property called `topLevelMenuItems` which defines the top-level menu items to display. 2 levels of nested references inside those contain the level 2 and level 3 menu items. 77 | 78 | The items to display are read from the configuration lists via class `MegaMenuService`. This uses the Typescript typings for the PnP JS libraries which make it much easier to interface with SP and provide a layer over the top of the REST API. PnP JS is also used to cache the result in the browser's session cache. 79 | 80 | The user interface inside the React components is provided by Office UI Fabric, which is the Office design language, and is intended to help make your apps look like native Office 365. It provides a lot of Bootstrap-like classes and utilities, and provides some nice ways for React components to detect responsive state, e.g. adding `@withResponsiveMode` to the React component class. 81 | -------------------------------------------------------------------------------- /src/service/MegaMenuService.ts: -------------------------------------------------------------------------------- 1 | import pnp from 'sp-pnp-js'; 2 | import { Web } from 'sp-pnp-js/lib/sharepoint/webs'; 3 | 4 | import { TopLevelMenu } from '../model/TopLevelMenu' 5 | import { FlyoutColumn } from '../model/FlyoutColumn' 6 | import { Link } from '../model/Link' 7 | 8 | import { sampleData } from './MegaMenuSampleData' 9 | 10 | export class MegaMenuService { 11 | 12 | static readonly useSampleData: boolean = false; 13 | 14 | static readonly level1ListName: string = "Mega Menu - Level 1"; 15 | static readonly level2ListName: string = "Mega Menu - Level 2"; 16 | static readonly level3ListName: string = "Mega Menu - Level 3"; 17 | 18 | static readonly cacheKey: string = "MegaMenuTopLevelItems"; 19 | 20 | // Get items for the menu and cache the result in session cache. 21 | public static getMenuItems(siteCollectionUrl:string): Promise { 22 | 23 | if (!MegaMenuService.useSampleData) { 24 | 25 | return new Promise((resolve, reject) => { 26 | 27 | // See if we've cached the result previously. 28 | var topLevelItems: TopLevelMenu[] = pnp.storage.session.get(MegaMenuService.cacheKey); 29 | 30 | if (topLevelItems) { 31 | console.log("Found mega menu items in cache."); 32 | resolve(topLevelItems); 33 | } 34 | else { 35 | 36 | console.log("Didn't find mega menu items in cache, getting from list."); 37 | 38 | var level1ItemsPromise = MegaMenuService.getMenuItemsFromSp(MegaMenuService.level1ListName, siteCollectionUrl); 39 | var level2ItemsPromise = MegaMenuService.getMenuItemsFromSp(MegaMenuService.level2ListName, siteCollectionUrl); 40 | var level3ItemsPromise = MegaMenuService.getMenuItemsFromSp(MegaMenuService.level3ListName, siteCollectionUrl); 41 | 42 | Promise.all([level1ItemsPromise, level2ItemsPromise, level3ItemsPromise]) 43 | .then((results: any[][]) => { 44 | 45 | topLevelItems = MegaMenuService.convertItemsFromSp(results[0], results[1], results[2]); 46 | 47 | // Store in session cache. 48 | pnp.storage.session.put(MegaMenuService.cacheKey, topLevelItems); 49 | 50 | resolve(topLevelItems); 51 | }); 52 | } 53 | }); 54 | } 55 | else { 56 | return new Promise((resolve, reject) => { 57 | resolve(sampleData); 58 | }); 59 | } 60 | 61 | } 62 | 63 | // Get raw results from SP. 64 | private static getMenuItemsFromSp(listName: string, siteCollectionUrl:string): Promise { 65 | 66 | return new Promise((resolve, reject) => { 67 | 68 | let web = new Web(siteCollectionUrl); 69 | 70 | // TODO : Note that passing in url and using this approach is a workaround. I would have liked to just 71 | // call pnp.sp.site.rootWeb.lists, however when running this code on SPO modern pages, the REST call ended 72 | // up with a corrupt URL. However it was OK on View All Site content pages, etc. 73 | web.lists 74 | .getByTitle(listName) 75 | .items 76 | .orderBy("SortOrder") 77 | .get() 78 | .then((items: any[]) => { 79 | resolve(items); 80 | }) 81 | .catch((error: any) => { 82 | reject(error); 83 | }); 84 | }); 85 | 86 | } 87 | 88 | 89 | // Convert results from SP into actual entities with correct relationships. 90 | private static convertItemsFromSp(level1: any[], level2: any[], level3: any[]): TopLevelMenu[] { 91 | 92 | var level1Dictionary: { [id: number]: TopLevelMenu; } = {}; 93 | var level2Dictionary: { [id: number]: FlyoutColumn; } = {}; 94 | 95 | // Convert level 1 items and store in dictionary. 96 | var level1Items: TopLevelMenu[] = level1.map((item: any) => { 97 | var newItem = { 98 | id: item.Id, 99 | text: item.Title, 100 | columns: [] 101 | }; 102 | 103 | level1Dictionary[newItem.id] = newItem; 104 | 105 | return newItem; 106 | }); 107 | 108 | // Convert level 2 items and store in dictionary. 109 | var level2Items: FlyoutColumn[] = level2.map((item: any) => { 110 | var newItem = { 111 | id: item.Id, 112 | heading: { 113 | text: item.Title, 114 | url: item.Url ? item.Url.Url : "", 115 | openInNewTab: item.OpenInNewTab 116 | }, 117 | links: [], 118 | level1ParentId: item.Level1ItemId 119 | }; 120 | 121 | level2Dictionary[newItem.id] = newItem; 122 | 123 | return newItem; 124 | }); 125 | 126 | // Convert level 3 items and store in dictionary. 127 | var level3Items: Link[] = level3.map((item: any) => { 128 | return { 129 | level2ParentId: item.Level2ItemId, 130 | text: item.Title, 131 | url: item.Url.Url, 132 | openInNewTab: item.OpenInNewTab 133 | }; 134 | }); 135 | 136 | // Now link the entities into the desired structure. 137 | for (let l3 of level3Items) { 138 | level2Dictionary[l3.level2ParentId].links.push(l3); 139 | } 140 | 141 | for (let l2 of level2Items) { 142 | level1Dictionary[l2.level1ParentId].columns.push(l2); 143 | } 144 | 145 | var retVal: TopLevelMenu[] = []; 146 | 147 | for (let l1 of level1Items) { 148 | retVal.push(l1); 149 | } 150 | 151 | return retVal; 152 | 153 | } 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | } -------------------------------------------------------------------------------- /src/service/MegaMenuSampleData.ts: -------------------------------------------------------------------------------- 1 | import { TopLevelMenu } from '../model/TopLevelMenu' 2 | import { FlyoutColumn } from '../model/FlyoutColumn' 3 | import { Link } from '../model/Link' 4 | 5 | export const sampleData: TopLevelMenu[] = [ 6 | { 7 | id: 1, 8 | text: "Organisation", 9 | columns: [ 10 | { 11 | level1ParentId:-1, 12 | id:-1, 13 | heading: { text: "Corporate", url: "/", openInNewTab: false, level2ParentId:-1 }, 14 | links: [ 15 | { text: "Finance", url: "/", openInNewTab: false, level2ParentId:-1 }, 16 | { text: "Information Services", url: "/", openInNewTab: false, level2ParentId:-1 }, 17 | { text: "HSE", url: "/", openInNewTab: false, level2ParentId:-1 }, 18 | { text: "Human Resources", url: "/", openInNewTab: false, level2ParentId:-1 } 19 | ] 20 | }, 21 | { 22 | level1ParentId:-1, 23 | id:-1, 24 | heading: { text: "R & D", level2ParentId:-1 }, 25 | links: [ 26 | { text: "Atomic Energy", url: "/", openInNewTab: false, level2ParentId:-1 }, 27 | { text: "Black Hole Generation", url: "/", openInNewTab: false, level2ParentId:-1 }, 28 | { text: "Time Travel", url: "/", openInNewTab: false, level2ParentId:-1 }, 29 | { text: "Weaponry", url: "/", openInNewTab: false, level2ParentId:-1 } 30 | ] 31 | }, 32 | { 33 | level1ParentId:-1, 34 | id:-1, 35 | heading: { text: "Projects" , level2ParentId:-1}, 36 | links: [ 37 | { text: "Terrestrial", url: "/", openInNewTab: false, level2ParentId:-1 }, 38 | { text: "Deep Space", url: "/", openInNewTab: false, level2ParentId:-1 }, 39 | { text: "Underwater", url: "/", openInNewTab: false, level2ParentId:-1 }, 40 | { text: "Dimension X", url: "/", openInNewTab: false, level2ParentId:-1 } 41 | ] 42 | }, 43 | { 44 | level1ParentId:-1, 45 | id:-1, 46 | heading: { text: "Production", level2ParentId:-1 }, 47 | links: [ 48 | { text: "Moon Dust Extraction", url: "/", openInNewTab: false, level2ParentId:-1 }, 49 | { text: "Dark Matter Mining", url: "/", openInNewTab: false, level2ParentId:-1 }, 50 | { text: "Earth Core Leeching", url: "/", openInNewTab: false, level2ParentId:-1 } 51 | ] 52 | } 53 | ] 54 | }, 55 | { 56 | id: 2, 57 | text: "The Management Team", 58 | columns: [ 59 | { 60 | level1ParentId:-1, 61 | id:-1, 62 | heading: { text: "The Board", level2ParentId:-1 }, 63 | links: [ 64 | { text: "Jack Jackson", url: "/", openInNewTab: false, level2ParentId:-1 }, 65 | { text: "Jane Janeson", url: "/", openInNewTab: false, level2ParentId:-1 }, 66 | { text: "Bob Bobson", url: "/", openInNewTab: false, level2ParentId:-1 }, 67 | { text: "Michelle Michelleson", url: "/", openInNewTab: false, level2ParentId:-1 } 68 | ] 69 | }, 70 | { 71 | level1ParentId:-1, 72 | id:-1, 73 | heading: { text: "Executive", level2ParentId:-1 }, 74 | links: [ 75 | { text: "Michael Michaelson", url: "/", openInNewTab: false, level2ParentId:-1 }, 76 | { text: "Lee Leeson", url: "/", openInNewTab: false, level2ParentId:-1 }, 77 | { text: "Clare Clareson", url: "/", openInNewTab: false, level2ParentId:-1 }, 78 | { text: "Mathew Mathewson", url: "/", openInNewTab: false, level2ParentId:-1 } 79 | ] 80 | } 81 | ] 82 | }, 83 | { 84 | id: 3, 85 | text: "Resources", 86 | columns: [ 87 | { 88 | level1ParentId:-1, 89 | id:-1, 90 | heading: { text: "Search Engines", level2ParentId:-1 }, 91 | links: [ 92 | { text: "Google", url: "/", openInNewTab: false, level2ParentId:-1 }, 93 | { text: "Bing", url: "/", openInNewTab: false, level2ParentId:-1 }, 94 | { text: "Gargle", url: "/", openInNewTab: false , level2ParentId:-1}, 95 | { text: "Thingo", url: "/", openInNewTab: false , level2ParentId:-1} 96 | ] 97 | }, 98 | { 99 | level1ParentId:-1, 100 | id:-1, 101 | heading: { text: "Policies" , level2ParentId:-1}, 102 | links: [ 103 | { text: "Political", url: "/", openInNewTab: false , level2ParentId:-1}, 104 | { text: "Ethical", url: "/", openInNewTab: false, level2ParentId:-1 } 105 | ] 106 | }, 107 | { 108 | level1ParentId:-1, 109 | id:-1, 110 | heading: { text: "Procedures", level2ParentId:-1 }, 111 | links: [ 112 | { text: "Alien Lifeforms", url: "/", openInNewTab: false, level2ParentId:-1}, 113 | { text: "Rifts in Spacetime", url: "/", openInNewTab: false, level2ParentId:-1 }, 114 | { text: "Changed the Past", url: "/", openInNewTab: false, level2ParentId:-1 }, 115 | { text: "Earth Core Issues", url: "/", openInNewTab: false, level2ParentId:-1 }, 116 | { text: "Lost in Space", url: "/", openInNewTab: false, level2ParentId:-1 }, 117 | ] 118 | }, 119 | ] 120 | }, 121 | { 122 | id: 4, 123 | text: "News and Events", 124 | columns: [ 125 | { 126 | level1ParentId:-1, 127 | id:-1, 128 | heading: { text: "News", level2ParentId:-1 }, 129 | links: [ 130 | { text: "Earth", url: "/", openInNewTab: false, level2ParentId:-1 }, 131 | { text: "Mars", url: "/", openInNewTab: false, level2ParentId:-1 }, 132 | { text: "Dimension X", url: "/", openInNewTab: false, level2ParentId:-1 } 133 | ] 134 | }, 135 | { 136 | level1ParentId:-1, 137 | id:-1, 138 | heading: { text: "Events", level2ParentId:-1 }, 139 | links: [ 140 | { text: "Mars Family Days", url: "/", openInNewTab: false, level2ParentId:-1 }, 141 | { text: "Earth", url: "/", openInNewTab: false, level2ParentId:-1 }, 142 | { text: "Dimension X", url: "/", openInNewTab: false, level2ParentId:-1 }, 143 | { text: "Bottom of the Ocean", url: "/", openInNewTab: false , level2ParentId:-1} 144 | ] 145 | } 146 | ] 147 | } 148 | ] -------------------------------------------------------------------------------- /src/components/MegaMenu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Layer, LayerHost } from 'office-ui-fabric-react/lib/Layer'; 3 | import { withResponsiveMode, ResponsiveMode } from 'office-ui-fabric-react/lib/utilities/decorators/withResponsiveMode'; 4 | 5 | import { TopLevelMenu } from './TopLevelMenu'; 6 | import { Flyout } from './Flyout'; 7 | import { MobileMenu } from './MobileMenu'; 8 | 9 | import styles from './MegaMenu.module.scss'; 10 | 11 | import { TopLevelMenu as TopLevelMenuModel } from '../model/TopLevelMenu'; 12 | import { FlyoutColumn as FlyoutColumnModel } from '../model/FlyoutColumn'; 13 | import { Link as LinkModel } from '../model/Link'; 14 | 15 | export interface IMegaMenuProps { 16 | topLevelMenuItems: TopLevelMenuModel[]; 17 | responsiveMode?: ResponsiveMode; 18 | } 19 | 20 | export interface IMegaMenuState { 21 | showFlyout: boolean; 22 | cursorInTopLevelMenu: boolean; 23 | cursorInFlyout: boolean; 24 | selectedTopLevelItem: TopLevelMenuModel; 25 | showTopLevelMenuItemsWhenMobile: boolean; //For mobile mode only, this determines whether or not to show top level menu items. 26 | } 27 | 28 | @withResponsiveMode 29 | export class MegaMenu extends React.Component { 30 | 31 | constructor(props) { 32 | super(props); 33 | 34 | this.state = { 35 | showFlyout: false, 36 | cursorInTopLevelMenu: false, 37 | cursorInFlyout: false, 38 | selectedTopLevelItem: null, 39 | showTopLevelMenuItemsWhenMobile: false 40 | }; 41 | 42 | // These are needed to ensure "this" resolves in the functions. 43 | this.handleFocusedTopLevelMenu = this.handleFocusedTopLevelMenu.bind(this); 44 | this.handleLostFocusTopLevelMenu = this.handleLostFocusTopLevelMenu.bind(this); 45 | this.handleFocusedFlyout = this.handleFocusedFlyout.bind(this); 46 | this.handleLostFocusFlyout = this.handleLostFocusFlyout.bind(this); 47 | this.handleMobileMenuTouched = this.handleMobileMenuTouched.bind(this); 48 | } 49 | 50 | 51 | public render(): React.ReactElement { 52 | 53 | var responsiveMode = this.props.responsiveMode; 54 | if (responsiveMode === undefined) { 55 | responsiveMode = ResponsiveMode.large; 56 | } 57 | 58 | var mobileMode = responsiveMode < ResponsiveMode.large; 59 | 60 | const topLevelItems = this.props.topLevelMenuItems.map((item: TopLevelMenuModel) => 61 | 69 | 70 | ); 71 | 72 | return ( 73 |
74 | 75 | {mobileMode && ( 76 | 79 | )} 80 | 81 | {(!mobileMode || (mobileMode && this.state.showTopLevelMenuItemsWhenMobile)) && ( 82 |
88 |
89 | 90 | {topLevelItems} 91 | 92 |
93 |
94 | )} 95 | 96 | {this.state.showFlyout && 97 | 102 | 103 | } 104 | 105 |
106 | ); 107 | } 108 | 109 | 110 | 111 | handleFocusedTopLevelMenu(selectedTopLevelItem: TopLevelMenuModel) { 112 | 113 | this.setState((prevState, props) => ({ 114 | showFlyout: prevState.showFlyout, 115 | cursorInTopLevelMenu: true, 116 | cursorInFlyout: prevState.cursorInFlyout, 117 | selectedTopLevelItem: selectedTopLevelItem, 118 | showTopLevelMenuItemsWhenMobile: prevState.showTopLevelMenuItemsWhenMobile, 119 | })); 120 | 121 | this.checkFlyoutVisibility(); 122 | } 123 | 124 | handleLostFocusTopLevelMenu() { 125 | this.setState((prevState, props) => ({ 126 | showFlyout: prevState.showFlyout, 127 | cursorInTopLevelMenu: false, 128 | cursorInFlyout: prevState.cursorInFlyout, 129 | selectedTopLevelItem: prevState.selectedTopLevelItem, 130 | showTopLevelMenuItemsWhenMobile: prevState.showTopLevelMenuItemsWhenMobile, 131 | })); 132 | 133 | this.checkFlyoutVisibility(); 134 | } 135 | 136 | handleFocusedFlyout(selectedTopLevelItem: TopLevelMenuModel) { 137 | this.setState((prevState, props) => ({ 138 | showFlyout: prevState.showFlyout, 139 | cursorInTopLevelMenu: prevState.cursorInTopLevelMenu, 140 | cursorInFlyout: true, 141 | selectedTopLevelItem: selectedTopLevelItem, 142 | showTopLevelMenuItemsWhenMobile: prevState.showTopLevelMenuItemsWhenMobile, 143 | })); 144 | 145 | this.checkFlyoutVisibility(); 146 | } 147 | 148 | handleLostFocusFlyout() { 149 | this.setState((prevState, props) => ({ 150 | showFlyout: prevState.showFlyout, 151 | cursorInTopLevelMenu: prevState.cursorInTopLevelMenu, 152 | cursorInFlyout: false, 153 | selectedTopLevelItem: prevState.selectedTopLevelItem, 154 | showTopLevelMenuItemsWhenMobile: prevState.showTopLevelMenuItemsWhenMobile, 155 | })); 156 | 157 | this.checkFlyoutVisibility(); 158 | } 159 | 160 | // Set visibility of flyout menu. Only visible if mouse is in a top-level menu or in the flyout itself. 161 | checkFlyoutVisibility() { 162 | this.setState((prevState, props) => { 163 | 164 | var showFlyout = prevState.cursorInTopLevelMenu || prevState.cursorInFlyout; 165 | 166 | return { 167 | showFlyout: showFlyout, 168 | cursorInTopLevelMenu: prevState.cursorInTopLevelMenu, 169 | cursorInFlyout: prevState.cursorInFlyout, 170 | selectedTopLevelItem: showFlyout ? prevState.selectedTopLevelItem : null, 171 | showTopLevelMenuItemsWhenMobile: prevState.showTopLevelMenuItemsWhenMobile, 172 | } 173 | }); 174 | } 175 | 176 | handleMobileMenuTouched() { 177 | this.setState((prevState, props) => { 178 | 179 | console.log("toggle"); 180 | 181 | var showTopLevelMenuItemsWhenMobile = !prevState.showTopLevelMenuItemsWhenMobile; 182 | 183 | var showFlyout = prevState.showFlyout && showTopLevelMenuItemsWhenMobile; 184 | 185 | return { 186 | showFlyout: showFlyout, 187 | cursorInTopLevelMenu: prevState.cursorInTopLevelMenu, 188 | cursorInFlyout: prevState.cursorInFlyout, 189 | selectedTopLevelItem: prevState.selectedTopLevelItem, 190 | showTopLevelMenuItemsWhenMobile: showTopLevelMenuItemsWhenMobile 191 | } 192 | }); 193 | } 194 | 195 | 196 | 197 | } 198 | -------------------------------------------------------------------------------- /provisioning/create-lists.ps1: -------------------------------------------------------------------------------- 1 | Param( 2 | [string]$siteCollectionUrl 3 | ) 4 | 5 | Connect-PnPOnline -Url $siteCollectionUrl 6 | 7 | $listNameLevel1 = "Mega Menu - Level 1" 8 | $listNameLevel2 = "Mega Menu - Level 2" 9 | $listNameLevel3 = "Mega Menu - Level 3" 10 | 11 | function Create-LinksList { 12 | 13 | Param ($listName) 14 | 15 | write-host "Creating list $listName..." -NoNewline -ForegroundColor Gray 16 | 17 | $list = New-PnPList -Title $listName -Template GenericList -EnableVersioning 18 | $field = Add-PnPField -DisplayName "Sort Order" -InternalName "SortOrder" -Type number -AddToDefaultView -List $listName 19 | $field = Add-PnPField -DisplayName "Url" -InternalName "Url" -Type URL -AddToDefaultView -List $listName 20 | $field = Add-PnPField -DisplayName "Open in New Tab" -InternalName "OpenInNewTab" -Type boolean -AddToDefaultView -List $listName 21 | $field = Add-PnPField -DisplayName "Description" -InternalName "Description" -Type note -AddToDefaultView -List $listName 22 | 23 | write-host "done." -ForegroundColor Green 24 | 25 | return $list 26 | } 27 | 28 | function Create-LookupField { 29 | Param ($listNameToAddTo, $fieldInternalName, $fieldDisplayName, $lookedUpListGuid, $lookedUpListFieldTitle) 30 | $guid = [guid]::NewGuid() 31 | $xml = "" 32 | Add-PnPFieldFromXml -FieldXml $xml -List $listNameToAddTo 33 | } 34 | 35 | # Create the lists for the 3 levels of menu. 36 | Create-LinksList -listName $listNameLevel1 37 | Create-LinksList -listName $listNameLevel2 38 | Create-LinksList -listName $listNameLevel3 39 | 40 | write-host "Creating lookup columns..." -NoNewline -ForegroundColor Gray 41 | 42 | # Add the lookup fields to the L2 and L3 lists to associate with L1 and L2 items respectively 43 | $level1List = Get-PnPList -Identity $listNameLevel1 44 | $level2List = Get-PnPList -Identity $listNameLevel2 45 | Create-LookupField -listNameToAddTo $listNameLevel2 -fieldInternalName "Level1Item" -fieldDisplayName "Level 1 Item" -lookedUpListGuid $level1List.Id -lookedUpListFieldTitle "Title" 46 | Create-LookupField -listNameToAddTo $listNameLevel3 -fieldInternalName "Level2Item" -fieldDisplayName "Level 2 Item" -lookedUpListGuid $level2List.Id -lookedUpListFieldTitle "Title" 47 | 48 | # Add the lookup field to the default view of each list 49 | $view = Get-PnPView -List $listNameLevel2 -Identity "All Items" 50 | $view.ViewFields.Add("Level1Item"); 51 | $view.Update(); 52 | $view.Context.ExecuteQuery(); 53 | 54 | $view = Get-PnPView -List $listNameLevel3 -Identity "All Items" 55 | $view.ViewFields.Add("Level2Item"); 56 | $view.Update(); 57 | $view.Context.ExecuteQuery(); 58 | 59 | write-host "done." -ForegroundColor Green 60 | 61 | # Set up new default view of each list 62 | write-host "Creating default views..." -NoNewline -ForegroundColor Gray 63 | 64 | $view = Add-PnPView -Title "Level 1 Menu Items" -List $listNameLevel1 -SetAsDefault -Fields "SortOrder","Title","Url","OpenInNewTab","Description" -Query "" -Paged 65 | $view = Add-PnPView -Title "Level 2 Menu Items" -List $listNameLevel2 -SetAsDefault -Fields "SortOrder","Title","Url","OpenInNewTab","Description" -Query "" -Paged 66 | $view = Add-PnPView -Title "Level 3 Menu Items" -List $listNameLevel3 -SetAsDefault -Fields "SortOrder","Title","Url","OpenInNewTab","Description" -Query "" -Paged 67 | 68 | write-host "done." -ForegroundColor Green 69 | 70 | # Create initial sample data 71 | write-host "Creating sample data..." -NoNewline -ForegroundColor Gray 72 | 73 | $lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." 74 | 75 | # Level 1 76 | $organisation = Add-PnPListItem -List $listNameLevel1 -Values @{"Title" = "Organisation"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Description" = $lorem} -ContentType "Item" 77 | $theManagementTeam = Add-PnPListItem -List $listNameLevel1 -Values @{"Title" = "The Management Team"; "SortOrder" = 2; "Url" = "/"; "OpenInNewTab" = "No"; "Description" = $lorem} -ContentType "Item" 78 | $resources = Add-PnPListItem -List $listNameLevel1 -Values @{"Title" = "Resources"; "SortOrder" = 3; "Url" = "/"; "OpenInNewTab" = "No"; "Description" = $lorem} -ContentType "Item" 79 | $newsAndEvents = Add-PnPListItem -List $listNameLevel1 -Values @{"Title" = "News and Events"; "SortOrder" = 4; "Url" = "/"; "OpenInNewTab" = "No"; "Description" = $lorem} -ContentType "Item" 80 | 81 | # Level 2 82 | $corporate = Add-PnPListItem -List $listNameLevel2 -Values @{"Title" = "Corporate"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Level1Item" = $organisation.Id; "Description" = $lorem} -ContentType "Item" 83 | $rd = Add-PnPListItem -List $listNameLevel2 -Values @{"Title" = "R & D"; "SortOrder" = 2; "Url" = "/"; "OpenInNewTab" = "No"; "Level1Item" = $organisation.Id; "Description" = $lorem} -ContentType "Item" 84 | $projects = Add-PnPListItem -List $listNameLevel2 -Values @{"Title" = "Projects"; "SortOrder" = 3; "Url" = "/"; "OpenInNewTab" = "No"; "Level1Item" = $organisation.Id; "Description" = $lorem} -ContentType "Item" 85 | $production = Add-PnPListItem -List $listNameLevel2 -Values @{"Title" = "Production"; "SortOrder" = 4; "Url" = "/"; "OpenInNewTab" = "No"; "Level1Item" = $organisation.Id; "Description" = $lorem} -ContentType "Item" 86 | 87 | $theBoard = Add-PnPListItem -List $listNameLevel2 -Values @{"Title" = "The Board"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Level1Item" = $theManagementTeam.Id; "Description" = $lorem} -ContentType "Item" 88 | $executive = Add-PnPListItem -List $listNameLevel2 -Values @{"Title" = "Executive"; "SortOrder" = 2; "Url" = "/"; "OpenInNewTab" = "No"; "Level1Item" = $theManagementTeam.Id; "Description" = $lorem} -ContentType "Item" 89 | 90 | $searchEngines = Add-PnPListItem -List $listNameLevel2 -Values @{"Title" = "Search Engines"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Level1Item" = $resources.Id; "Description" = $lorem} -ContentType "Item" 91 | $policies = Add-PnPListItem -List $listNameLevel2 -Values @{"Title" = "Policies"; "SortOrder" = 2; "Url" = "/"; "OpenInNewTab" = "No"; "Level1Item" = $resources.Id; "Description" = $lorem} -ContentType "Item" 92 | $procedures = Add-PnPListItem -List $listNameLevel2 -Values @{"Title" = "Procedures"; "SortOrder" = 3; "Url" = "/"; "OpenInNewTab" = "No"; "Level1Item" = $resources.Id; "Description" = $lorem} -ContentType "Item" 93 | 94 | $news = Add-PnPListItem -List $listNameLevel2 -Values @{"Title" = "News"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Level1Item" = $newsAndEvents.Id; "Description" = $lorem} -ContentType "Item" 95 | $events = Add-PnPListItem -List $listNameLevel2 -Values @{"Title" = "Events"; "SortOrder" = 2; "Url" = "/"; "OpenInNewTab" = "No"; "Level1Item" = $newsAndEvents.Id; "Description" = $lorem} -ContentType "Item" 96 | 97 | # Level 3 98 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Finance"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $corporate.Id; "Description" = $lorem} -ContentType "Item" 99 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Information Services"; "SortOrder" = 2; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $corporate.Id; "Description" = $lorem} -ContentType "Item" 100 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "HSE"; "SortOrder" = 3; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $corporate.Id; "Description" = $lorem} -ContentType "Item" 101 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Human Resources"; "SortOrder" = 4; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $corporate.Id; "Description" = $lorem} -ContentType "Item" 102 | 103 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Atomic Energy"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $rd.Id; "Description" = $lorem} -ContentType "Item" 104 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Black Hole Generation"; "SortOrder" = 2; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $rd.Id; "Description" = $lorem} -ContentType "Item" 105 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Time Travel"; "SortOrder" = 3; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $rd.Id; "Description" = $lorem} -ContentType "Item" 106 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Weaponry"; "SortOrder" = 4; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $rd.Id; "Description" = $lorem} -ContentType "Item" 107 | 108 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Terrestrial"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Projects.Id; "Description" = $lorem} -ContentType "Item" 109 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Deep Space"; "SortOrder" = 2; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Projects.Id; "Description" = $lorem} -ContentType "Item" 110 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Underwater"; "SortOrder" = 3; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Projects.Id; "Description" = $lorem} -ContentType "Item" 111 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Dimension X"; "SortOrder" = 4; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Projects.Id; "Description" = $lorem} -ContentType "Item" 112 | 113 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Moon Dust Extraction"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Production.Id; "Description" = $lorem} -ContentType "Item" 114 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Dark Matter Mining"; "SortOrder" = 2; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Production.Id; "Description" = $lorem} -ContentType "Item" 115 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Earth Core Leeching"; "SortOrder" = 3; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Production.Id; "Description" = $lorem} -ContentType "Item" 116 | 117 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Jack Jackson"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $TheBoard.Id; "Description" = $lorem} -ContentType "Item" 118 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Jane Janeson"; "SortOrder" = 2; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $TheBoard.Id; "Description" = $lorem} -ContentType "Item" 119 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Bob Bobson"; "SortOrder" = 3; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $TheBoard.Id; "Description" = $lorem} -ContentType "Item" 120 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Michelle Michelleson"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $TheBoard.Id; "Description" = $lorem} -ContentType "Item" 121 | 122 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Michael Michaelson"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Executive.Id; "Description" = $lorem} -ContentType "Item" 123 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Lee Leeson"; "SortOrder" = 2; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Executive.Id; "Description" = $lorem} -ContentType "Item" 124 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Clare Clareson"; "SortOrder" = 3; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Executive.Id; "Description" = $lorem} -ContentType "Item" 125 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Mathew Mathewson"; "SortOrder" = 4; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Executive.Id; "Description" = $lorem} -ContentType "Item" 126 | 127 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Google"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $SearchEngines.Id; "Description" = $lorem} -ContentType "Item" 128 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Bing"; "SortOrder" = 2; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $SearchEngines.Id; "Description" = $lorem} -ContentType "Item" 129 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Gargle"; "SortOrder" = 3; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $SearchEngines.Id; "Description" = $lorem} -ContentType "Item" 130 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Thingo"; "SortOrder" = 4; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $SearchEngines.Id; "Description" = $lorem} -ContentType "Item" 131 | 132 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Political"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Policies.Id; "Description" = $lorem} -ContentType "Item" 133 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Ethical"; "SortOrder" = 2; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Policies.Id; "Description" = $lorem} -ContentType "Item" 134 | 135 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Alien Lifeforms"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Procedures.Id; "Description" = $lorem} -ContentType "Item" 136 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Rifts in Spacetime"; "SortOrder" = 2; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Procedures.Id; "Description" = $lorem} -ContentType "Item" 137 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Changed the Past"; "SortOrder" = 3; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Procedures.Id; "Description" = $lorem} -ContentType "Item" 138 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Earth Core Issues"; "SortOrder" = 4; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Procedures.Id; "Description" = $lorem} -ContentType "Item" 139 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Lost in Space"; "SortOrder" = 5; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Procedures.Id; "Description" = $lorem} -ContentType "Item" 140 | 141 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Earth"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $News.Id; "Description" = $lorem} -ContentType "Item" 142 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Mars"; "SortOrder" = 2; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $News.Id; "Description" = $lorem} -ContentType "Item" 143 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Dimension X"; "SortOrder" = 3; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $News.Id; "Description" = $lorem} -ContentType "Item" 144 | 145 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Mars Family Days"; "SortOrder" = 1; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Events.Id; "Description" = $lorem} -ContentType "Item" 146 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Earth"; "SortOrder" = 2; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Events.Id; "Description" = $lorem} -ContentType "Item" 147 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Dimension X"; "SortOrder" = 3; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Events.Id; "Description" = $lorem} -ContentType "Item" 148 | $item = Add-PnPListItem -List $listNameLevel3 -Values @{"Title" = "Bottom of the Ocean"; "SortOrder" = 4; "Url" = "/"; "OpenInNewTab" = "No"; "Level2Item" = $Events.Id; "Description" = $lorem} -ContentType "Item" 149 | 150 | write-host "done." -ForegroundColor Green --------------------------------------------------------------------------------