├── .gitattributes
├── .gitignore
├── src
├── tsconfig.base.json
├── tsconfig.types.json
├── types.ts
├── InternalConstants.ts
├── Model.ts
├── EventType.ts
├── MetaProperty.ts
├── Constants.ts
├── ModelClient.ts
├── EditorClient.ts
├── ModelRouter.ts
├── AuthoringUtils.ts
├── ModelStore.ts
└── PathUtils.ts
├── .editorconfig
├── .github
├── workflows
│ ├── security.yml
│ ├── ci.yml
│ ├── sonar.yml
│ └── release.yml
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── renovate.json
├── tsconfig.json
├── test
├── Setup.ts
├── data
│ ├── types.ts
│ ├── Page1Data.ts
│ ├── Page3Data.ts
│ ├── Page2Data.ts
│ ├── EditorClientData.ts
│ └── MainPageData.ts
├── ModelClient.test.ts
├── ModelRouter.test.ts
├── EditorClient.test.ts
├── AuthoringUtils.test.ts
├── ModelStore.test.ts
├── ModelManager.test.ts
└── PathUtils.test.ts
├── DEV_GUIDELINES.md
├── jest.config.js
├── .releaserc.js
├── webpack.config.js
├── CONTRIBUTING.md
├── README.md
├── package.json
├── CODE_OF_CONDUCT.md
├── CHANGELOG.md
└── LICENSE
/.gitattributes:
--------------------------------------------------------------------------------
1 | package-lock.json binary
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | .scannerwork/
4 | **/*.log
5 | *.tgz
6 |
--------------------------------------------------------------------------------
/src/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "noEmit": false,
5 | "baseUrl": ".",
6 | "paths": {
7 | "@adobe/aem-spa-page-model-manager": ["./"],
8 | "@adobe/aem-spa-page-model-manager/*": ["./*"]
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | trim_trailing_whitespace = true
7 | charset = utf-8
8 | indent_size = 2
9 | indent_style = space
10 |
11 | [*.{js,jsx,ts,tsx}]
12 | indent_size = 4
13 |
14 | [*.{json,xml}]
15 | insert_final_newline = false
16 |
17 | [package.json]
18 | insert_final_newline = true
19 |
--------------------------------------------------------------------------------
/src/tsconfig.types.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "compilerOptions": {
4 | "composite": true,
5 | "module": "es2015",
6 | "target": "esnext",
7 | "importHelpers": true,
8 | "removeComments": false,
9 | "declaration": true,
10 | "declarationMap": true,
11 | "declarationDir": "../dist",
12 | "emitDeclarationOnly": true
13 | },
14 | "exclude": [
15 | "./internal/umd.ts"
16 | ]
17 | }
--------------------------------------------------------------------------------
/.github/workflows/security.yml:
--------------------------------------------------------------------------------
1 | name: Vulnerability check
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request_target:
7 |
8 | jobs:
9 | security:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout source code
13 | uses: actions/checkout@master
14 | - name: Run Snyk to check for vulnerabilities
15 | uses: snyk/actions/node@master
16 | env:
17 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
18 | with:
19 | command: monitor
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "allowSyntheticDefaultImports": true,
5 | "esModuleInterop": true,
6 | "experimentalDecorators": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "incremental": true,
9 | "isolatedModules": true,
10 | "lib": [
11 | "dom",
12 | "dom.iterable",
13 | "es2018"
14 | ],
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "outDir": "./dist",
18 | "resolveJsonModule": true,
19 | "skipLibCheck": true,
20 | "strict": true,
21 | "target": "es2018"
22 | },
23 | "include": [
24 | "src/**/*"
25 | ]
26 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[feature] "
5 | labels: feature-request
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/test/Setup.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | import fetchMock from 'jest-fetch-mock';
14 |
15 | fetchMock.enableMocks();
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[bug]"
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **Package version**
14 | Provide a package version where the bug occurs.
15 |
16 | **To Reproduce**
17 | Steps to reproduce the behavior:
18 | 1. Go to '...'
19 | 2. Click on '....'
20 | 3. Scroll down to '....'
21 | 4. See error
22 |
23 | **Expected behavior**
24 | A clear and concise description of what you expected to happen.
25 |
26 | **Screenshots**
27 | If applicable, add screenshots to help explain your problem.
28 |
29 | **Additional context**
30 | Add any other context about the problem here.
31 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | import './ModelRouter';
14 |
15 | export { default as ModelManager } from './ModelManager';
16 | export * from './ModelClient';
17 | export * from './Model';
18 | export * from './ModelStore';
19 | export * from './PathUtils';
20 | export * from './AuthoringUtils';
21 | export { AEM_MODE, default as Constants } from './Constants';
22 |
--------------------------------------------------------------------------------
/DEV_GUIDELINES.md:
--------------------------------------------------------------------------------
1 |
2 | # Development
3 |
4 | Run npm install to get all node_modules that are necessary for development. Refer to scripts under `package.json` for more useful commands.
5 |
6 |
7 | ## Build
8 |
9 | ```sh
10 | $ npm run build
11 | ```
12 |
13 |
14 | ## Test
15 |
16 | ```sh
17 | $ npm run test
18 | ```
19 |
20 | ## Usage example
21 |
22 | This module provides the API to manage the model representation of the pages that are composing a SPA.
23 |
24 | ```
25 | // index.html
26 |
27 |
28 | ...
29 |
30 | ...
31 |
32 | ...
33 |
34 | // Bootstrap: index.js
35 | import { ModelManager } from '@adobe/aem-spa-page-model-manager';
36 |
37 | ModelManager.initialize().then((model) => {
38 | // Render the App content using the provided model
39 | render(model);
40 | });
41 |
42 | // Loading a specific portion of model
43 | ModelManager.getData("/content/site/page/jcr:content/path/to/component").then(...);
44 | ```
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "timezone": "Europe/Zurich",
3 | "packageRules": [
4 | {
5 | "groupName": "@adobe fixes",
6 | "updateTypes": ["patch", "pin", "digest", "minor"],
7 | "automerge": true,
8 | "packagePatterns": ["^@adobe/"],
9 | "schedule": ["at any time"]
10 | },
11 | {
12 | "groupName": "@adobe major",
13 | "updateTypes": ["major"],
14 | "packagePatterns": ["^@adobe/"],
15 | "automerge": false,
16 | "schedule": ["at any time"]
17 | },
18 | {
19 | "groupName": "external fixes",
20 | "updateTypes": ["patch", "pin", "digest", "minor"],
21 | "automerge": true,
22 | "schedule": ["after 1pm on Monday"],
23 | "packagePatterns": ["^.+"],
24 | "excludePackagePatterns": ["^@adobe/"]
25 | },
26 | {
27 | "groupName": "external major",
28 | "updateTypes": ["major"],
29 | "automerge": false,
30 | "packagePatterns": ["^.+"],
31 | "excludePackagePatterns": ["^@adobe/"],
32 | "schedule": ["after 1pm on Monday"]
33 | }
34 | ]
35 | }
--------------------------------------------------------------------------------
/test/data/types.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | import { Model } from '../../src/Model';
14 |
15 | export interface ResponsiveGridModel extends Model {
16 | gridClassNames: string;
17 | columnCount: number;
18 | }
19 |
20 | export interface PageModel extends Model {
21 | designPath?: string;
22 | title?: string;
23 | lastModifiedDate?: number;
24 | templateName?: string;
25 | cssClassNames?: string;
26 | language?: string;
27 | }
28 |
--------------------------------------------------------------------------------
/src/InternalConstants.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | /**
14 | * @private
15 | */
16 | export class InternalConstants {
17 | /**
18 | * Sling model selector.
19 | */
20 | public static readonly DEFAULT_SLING_MODEL_SELECTOR = 'model';
21 |
22 | /**
23 | * JSON model extension.
24 | */
25 | public static readonly DEFAULT_MODEL_JSON_EXTENSION = `.${InternalConstants.DEFAULT_SLING_MODEL_SELECTOR}.json`;
26 |
27 | private constructor() {
28 | // hide constructor
29 | }
30 | }
31 |
32 | export default InternalConstants;
33 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 | on: pull_request
3 |
4 | jobs:
5 | test:
6 | name: Build & Test
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout source code
10 | uses: actions/checkout@v2
11 | with:
12 | fetch-depth: 0
13 | - name: Setup Node.js
14 | uses: actions/setup-node@v2
15 | with:
16 | node-version: '14'
17 | - name: Install dependencies
18 | run: npm ci
19 | - name: Build the project
20 | run: npm run build:production
21 | - name: Run tests and do code coverage check
22 | run: npm run test:coverage
23 | - name: Run code linter
24 | uses: hallee/eslint-action@1.0.3
25 | if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }}
26 | with:
27 | repo-token: ${{ secrets.GITHUB_TOKEN }}
28 | - name: Upload code coverage report to workflow as an artifact
29 | uses: actions/upload-artifact@v2
30 | with:
31 | name: istanbul-code-coverage.zip
32 | path: coverage
33 | - name: Upload code coverage report to codecov.io and comment in pull request
34 | uses: codecov/codecov-action@v1
35 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | module.exports = {
14 | preset: 'ts-jest',
15 | setupFilesAfterEnv: [
16 | '/test/Setup.ts'
17 | ],
18 | testEnvironment: 'jsdom',
19 | transform: {
20 | '^.+\\.ts$': 'ts-jest'
21 | },
22 | testMatch: [
23 | '/test/*.test.ts'
24 | ],
25 | testPathIgnorePatterns: [
26 | 'node_modules/',
27 | 'dist/'
28 | ],
29 | collectCoverageFrom: [
30 | 'src/**/*.ts'
31 | ],
32 | coveragePathIgnorePatterns: [
33 | 'src/types.ts'
34 | ],
35 | coverageDirectory: 'dist/coverage',
36 | moduleFileExtensions: [
37 | 'ts',
38 | 'js'
39 | ]
40 | };
41 |
--------------------------------------------------------------------------------
/.releaserc.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | module.exports = {
14 | plugins: [
15 | '@semantic-release/commit-analyzer',
16 | '@semantic-release/release-notes-generator',
17 | [
18 | '@semantic-release/changelog', {
19 | 'changelogFile': 'CHANGELOG.md'
20 | }
21 | ],
22 | '@semantic-release/npm',
23 | [
24 | '@semantic-release/github', {
25 | 'assets': [
26 | 'package.json',
27 | 'CHANGELOG.md'
28 | ],
29 | 'message': 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}'
30 | }
31 | ],
32 | [
33 | '@semantic-release/git', {
34 | 'assets': [
35 | 'package.json',
36 | 'CHANGELOG.md'
37 | ]
38 | }
39 | ]
40 | ],
41 | branch: 'master',
42 | branches: [ 'master' ]
43 | };
44 |
--------------------------------------------------------------------------------
/src/Model.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | /**
14 | * Generic Model interface.
15 | * Defines common properties that pages / items have.
16 | */
17 | export interface Model {
18 | /**
19 | * Hierarchy type.
20 | */
21 | ':hierarchyType'?: string;
22 |
23 | /**
24 | * Path of the item/page.
25 | */
26 | ':path'?: string;
27 |
28 | /**
29 | * Child pages (only present on page's itself, not on items).
30 | */
31 | ':children'?: { [key: string]: Model };
32 |
33 | /**
34 | * Items under the page/item.
35 | */
36 | ':items'?: { [key: string]: Model };
37 |
38 | /**
39 | * Order of the items under the page/item.
40 | * Can be used as keys for the :items property to iterate items in the proper order.
41 | */
42 | ':itemsOrder'?: string[];
43 |
44 | /**
45 | * Resource type of the page/item.
46 | */
47 | ':type'?: string;
48 | }
49 |
--------------------------------------------------------------------------------
/.github/workflows/sonar.yml:
--------------------------------------------------------------------------------
1 | name: Sonar
2 | on:
3 | workflow_run:
4 | workflows: ["Continuous Integration"]
5 | types:
6 | - completed
7 | jobs:
8 | sonar:
9 | name: Sonar
10 | runs-on: ubuntu-latest
11 | if: github.event.workflow_run.conclusion == 'success'
12 | steps:
13 | - name: Checkout source code
14 | uses: actions/checkout@v2
15 | with:
16 | repository: ${{ github.event.workflow_run.head_repository.full_name }}
17 | ref: ${{ github.event.workflow_run.head_branch }}
18 | fetch-depth: 0
19 | - name: "Get PR information"
20 | uses: potiuk/get-workflow-origin@v1
21 | id: source-run-info
22 | with:
23 | token: ${{ secrets.GITHUB_TOKEN }}
24 | sourceRunId: ${{ github.event.workflow_run.id }}
25 | - name: Upload Sonar report to sonarcloud.io and comment in pull request
26 | uses: sonarsource/sonarcloud-github-action@master
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
30 | with:
31 | args: >
32 | -Dsonar.organization=adobeinc
33 | -Dsonar.projectKey=adobe_aem-spa-page-model-manager
34 | -Dsonar.sources=src
35 | -Dsonar.tests=test
36 | -Dsonar.javascript.lcov.reportPaths=dist/coverage/lcov.info
37 | -Dsonar.pullrequest.key=${{ steps.source-run-info.outputs.pullRequestNumber }}
38 | -Dsonar.pullrequest.branch=${{ steps.source-run-info.outputs.sourceHeadBranch }}
39 | -Dsonar.pullrequest.base=${{ steps.source-run-info.outputs.targetBranch }}
40 |
41 |
--------------------------------------------------------------------------------
/src/EventType.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | /**
14 | * Type of events triggered or listened by the PageModelManager and ModelRouter.
15 | * @private
16 | */
17 | export class EventType {
18 | /**
19 | * Event which indicates that the PageModelManager has been initialized
20 | */
21 | public static readonly PAGE_MODEL_INIT = 'cq-pagemodel-init';
22 |
23 | /**
24 | * Event which indicates that the PageModelManager has loaded new content
25 | */
26 | public static readonly PAGE_MODEL_LOADED = 'cq-pagemodel-loaded';
27 |
28 | /**
29 | * Event that indicates a request to update the page model
30 | */
31 | public static readonly PAGE_MODEL_UPDATE = 'cq-pagemodel-update';
32 |
33 | /**
34 | * Event which indicates that ModelRouter has identified that model route has changed
35 | */
36 | public static readonly PAGE_MODEL_ROUTE_CHANGED = 'cq-pagemodel-route-changed';
37 |
38 | private constructor() {
39 | // hide constructor
40 | }
41 | }
42 |
43 | export default EventType;
44 |
--------------------------------------------------------------------------------
/src/MetaProperty.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | /**
14 | * Meta property names associated with the PageModelProvider and ModelRouter.
15 | * @private
16 | */
17 | export class MetaProperty {
18 | /**
19 | * Meta property pointing to page model root.
20 | */
21 | public static readonly PAGE_MODEL_ROOT_URL = 'cq:pagemodel_root_url';
22 |
23 | /**
24 | * Meta property pointing to route filters.
25 | */
26 | public static readonly PAGE_MODEL_ROUTE_FILTERS = 'cq:pagemodel_route_filters';
27 |
28 | /**
29 | * Meta property pointing to model router.
30 | */
31 | public static readonly PAGE_MODEL_ROUTER = 'cq:pagemodel_router';
32 |
33 | /**
34 | * Meta property pointing to wcm mode.
35 | */
36 | public static readonly WCM_MODE = 'cq:wcmmode';
37 |
38 | /**
39 | * Meta property with a editor data type hint
40 | */
41 | public static readonly WCM_DATA_TYPE = 'cq:datatype';
42 |
43 | private constructor() {
44 | // hide constructor
45 | }
46 | }
47 |
48 | export default MetaProperty;
49 |
--------------------------------------------------------------------------------
/test/data/Page1Data.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | import { PageModel, ResponsiveGridModel } from './types';
14 |
15 | export const content_test_page1_stem_child0000 = { ':type': 'test/components/componentchild1' };
16 |
17 | export const content_test_page1_stem: ResponsiveGridModel = {
18 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12',
19 | 'columnCount': 12,
20 | ':itemsOrder': [ 'child0000' ],
21 | ':items': {
22 | 'child0000': content_test_page1_stem_child0000
23 | },
24 | ':type': 'wcm/foundation/components/responsivegrid'
25 | };
26 |
27 | export const PAGE1: PageModel = {
28 | 'designPath': '/libs/settings/wcm/designs/default',
29 | 'title': 'React sample page Custom',
30 | 'lastModifiedDate': 1512116041058,
31 | 'templateName': 'sample-template',
32 | 'cssClassNames': 'page',
33 | 'language': 'en-US',
34 | ':itemsOrder': [
35 | 'stem'
36 | ],
37 | ':items': {
38 | 'stem': content_test_page1_stem
39 | },
40 | ':path': '/content/test/page1',
41 | ':type': 'we-retail-react/components/structure/page'
42 | };
43 |
--------------------------------------------------------------------------------
/test/data/Page3Data.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | import { Model } from '../../src/Model';
14 | import { PageModel, ResponsiveGridModel } from './types';
15 |
16 | export const content_test_groot_child1000: ResponsiveGridModel = {
17 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12',
18 | 'columnCount': 12,
19 | ':type': 'wcm/foundation/components/responsivegrid'
20 | };
21 |
22 | export const content_test_groot_child1001: Model = { ':type': 'test/components/componentchild1' };
23 |
24 | export const content_test_groot: ResponsiveGridModel = {
25 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12',
26 | 'columnCount': 12,
27 | ':itemsOrder': [ 'child1000', 'child1001' ],
28 | ':items': {
29 | 'child1000': content_test_groot_child1000,
30 | 'child1001': content_test_groot_child1001
31 | },
32 | ':type': 'wcm/foundation/components/responsivegrid'
33 | };
34 |
35 | export const PAGE3: PageModel = {
36 | ':type': 'we-retail-journal/react/components/structure/page',
37 | ':path': '/content/test',
38 | ':items': {
39 | 'groot': content_test_groot
40 | },
41 | ':itemsOrder': [
42 | 'groot'
43 | ]
44 | };
45 |
--------------------------------------------------------------------------------
/test/data/Page2Data.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | import { Model } from '../../src/Model';
14 | import { PageModel, ResponsiveGridModel } from './types';
15 |
16 | export const content_test_page2_root_child2001: Model = { ':type': 'test/components/componentchild1' };
17 |
18 | export const content_test_page2_root_child2000: ResponsiveGridModel = {
19 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12',
20 | 'columnCount': 12,
21 | ':type': 'wcm/foundation/components/responsivegrid'
22 | };
23 |
24 | export const content_test_page2_root: ResponsiveGridModel = {
25 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12',
26 | 'columnCount': 12,
27 | ':itemsOrder': [ 'child2000', 'child2001' ],
28 | ':items': {
29 | 'child2000': content_test_page2_root_child2000,
30 | 'child2001': content_test_page2_root_child2001
31 | },
32 | ':type': 'wcm/foundation/components/responsivegrid'
33 | };
34 |
35 | export const PAGE2: PageModel = {
36 | ':type': 'we-retail-journal/react/components/structure/page',
37 | ':path': '/content/test/page2',
38 | ':items': {
39 | 'root': content_test_page2_root
40 | },
41 | ':itemsOrder': [
42 | 'root'
43 | ]
44 | };
45 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | branches:
5 | - master
6 |
7 | jobs:
8 | release:
9 | name: Release and publish module
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout source code
13 | uses: actions/checkout@v2
14 | with:
15 | fetch-depth: 0
16 | - name: Setup Node.js
17 | uses: actions/setup-node@v2
18 | with:
19 | node-version: 14
20 | - name: Install dependencies
21 | run: npm ci
22 | - name: Build the project
23 | run: npm run build:production
24 | - name: Run tests and do code coverage check
25 | run: npm run test:coverage
26 | - name: Upload code coverage report to codecov.io
27 | uses: codecov/codecov-action@v1
28 | - name: Upload Sonar report to sonarcloud.io
29 | uses: sonarsource/sonarcloud-github-action@master
30 | env:
31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
33 | with:
34 | args: >
35 | -Dsonar.organization=adobeinc
36 | -Dsonar.projectKey=adobe_aem-spa-page-model-manager
37 | -Dsonar.sources=src
38 | -Dsonar.tests=test
39 | -Dsonar.javascript.lcov.reportPaths=dist/coverage/lcov.info
40 | - name: Release module and publish it in github.com and npmjs.com
41 | env:
42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43 | NPM_TOKEN: ${{ secrets.ADOBE_BOT_NPM_TOKEN }}
44 | run: npm run semantic-release
45 | - name: Build documentation
46 | run: npm run docs
47 | - name: Publish documentation to github pages
48 | uses: JamesIves/github-pages-deploy-action@3.7.1
49 | with:
50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51 | BRANCH: gh-pages-documentation
52 | FOLDER: dist/docs
53 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | const path = require('path');
14 | const nodeExternals = require('webpack-node-externals');
15 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
16 |
17 | const isProduction = process.env.NODE_ENV === 'production';
18 | const mode = isProduction ? 'production' : 'development';
19 | const devtool = isProduction ? false : 'source-map';
20 |
21 | console.log('Building for:', mode);
22 |
23 | module.exports = {
24 | entry: './src/types.ts',
25 | mode,
26 | devtool,
27 | output: {
28 | globalObject: `(function(){ try{ return typeof self !== 'undefined';}catch(err){return false;}})() ? self : this`,
29 | path: path.resolve(__dirname, 'dist'),
30 | filename: 'aem-spa-page-model-manager.js',
31 | library: 'aemSpaPageModelManager',
32 | libraryTarget: 'umd'
33 | },
34 | module: {
35 | rules: [
36 | {
37 | test: /.ts$/,
38 | exclude: /(node_modules|dist)/,
39 | use: {
40 | loader: 'ts-loader'
41 | },
42 | enforce: 'post'
43 | }
44 | ]
45 | },
46 | resolve: {
47 | extensions: [ '.ts' ],
48 | fallback: {
49 | path: require.resolve('path-browserify'),
50 | url: require.resolve('url')
51 | }
52 | },
53 | externals: [ nodeExternals() ],
54 | plugins: [ new CleanWebpackPlugin() ]
55 | };
56 |
--------------------------------------------------------------------------------
/src/Constants.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | /**
14 | * Variables for interacting with AEM components.
15 | * @private
16 | */
17 | export class Constants {
18 | /**
19 | * Type of the item.
20 | */
21 | public static readonly TYPE_PROP = ':type';
22 |
23 | /**
24 | * List of child items of an item.
25 | */
26 | public static readonly ITEMS_PROP = ':items';
27 |
28 | /**
29 | * Order in which the items should be listed.
30 | */
31 | public static readonly ITEMS_ORDER_PROP = ':itemsOrder';
32 |
33 | /**
34 | * Path of an item.
35 | */
36 | public static readonly PATH_PROP = ':path';
37 |
38 | /**
39 | * Children of a hierarchical item.
40 | */
41 | public static readonly CHILDREN_PROP = ':children';
42 |
43 | /**
44 | * Hierarchical type of the item.
45 | */
46 | public static readonly HIERARCHY_TYPE_PROP = ':hierarchyType';
47 |
48 | /**
49 | * JCR content node.
50 | */
51 | public static readonly JCR_CONTENT = 'jcr:content';
52 |
53 | private constructor() {
54 | // hide constructor
55 | }
56 | }
57 |
58 | /**
59 | * AEM modes.
60 | */
61 | export enum AEM_MODE {
62 | EDIT = 'edit',
63 | PREVIEW = 'preview',
64 | DISABLED = 'disabled'
65 | }
66 |
67 | /**
68 | * Supported tag types.
69 | * @private
70 | */
71 | export enum TAG_TYPE {
72 | JS = 'script',
73 | STYLESHEET = 'stylesheet'
74 | }
75 |
76 | export default Constants;
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thanks for choosing to contribute!
4 |
5 | The following are a set of guidelines to follow when contributing to this project.
6 |
7 | ## Code Of Conduct
8 |
9 | This project adheres to the Adobe [code of conduct](../CODE_OF_CONDUCT.md). By participating,
10 | you are expected to uphold this code. Please report unacceptable behavior to
11 | [Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com).
12 |
13 | ## Have A Question?
14 |
15 | Start by filing an issue. The existing committers on this project work to reach
16 | consensus around project direction and issue solutions within issue threads
17 | (when appropriate).
18 |
19 | ## Contributor License Agreement
20 |
21 | All third-party contributions to this project must be accompanied by a signed contributor
22 | license agreement. This gives Adobe permission to redistribute your contributions
23 | as part of the project. [Sign our CLA](https://opensource.adobe.com/cla.html). You
24 | only need to submit an Adobe CLA one time, so if you have submitted one previously,
25 | you are good to go!
26 |
27 | ## Code Reviews
28 |
29 | All submissions should come in the form of pull requests and need to be reviewed
30 | by project committers. Read [GitHub's pull request documentation](https://help.github.com/articles/about-pull-requests/)
31 | for more information on sending pull requests.
32 |
33 | Lastly, please follow the [pull request template](PULL_REQUEST_TEMPLATE.md) when
34 | submitting a pull request!
35 |
36 | ## From Contributor To Committer
37 |
38 | We love contributions from our community! If you'd like to go a step beyond contributor
39 | and become a committer with full write access and a say in the project, you must
40 | be invited to the project. The existing committers employ an internal nomination
41 | process that must reach lazy consensus (silence is approval) before invitations
42 | are issued. If you feel you are qualified and want to get more deeply involved,
43 | feel free to reach out to existing committers to have a conversation about that.
44 |
45 | ## Security Issues
46 |
47 | Security issues shouldn't be reported on this issue tracker. Instead, [file an issue to our security experts](https://helpx.adobe.com/security/alertus.html).
48 |
49 | ## Developer Guidelines
50 |
51 | * [Developer Guidelines](DEV_GUIDELINES.md)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Page Model Manager
2 |
3 | [](https://github.com/adobe/aem-spa-page-model-manager/blob/master/LICENSE)
4 | [](https://www.npmjs.com/package/@adobe/aem-spa-page-model-manager)
5 | [](https://opensource.adobe.com/aem-spa-page-model-manager/)
6 |
7 |
8 | [](https://codecov.io/gh/adobe/aem-spa-page-model-manager)
9 | [](https://sonarcloud.io/dashboard?id=adobe_aem-spa-page-model-manager)
10 | [](https://snyk.io/test/github/adobe/aem-spa-page-model-manager)
11 | [](https://app.renovatebot.com/dashboard#github/adobe/aem-spa-page-model-manager)
12 |
13 | An interpreter between Adobe Experience Manager Editor and the Adobe Experience Manager Single Page Application (SPA) Editor. The SPA Editor is recommended solution for projects that use SPA framework (React or Angular).
14 | For more information please see the [documentation](https://docs.adobe.com/content/help/en/experience-manager-65/developing/headless/spas/spa-page-component.html).
15 |
16 | ## Installation
17 | ```
18 | npm install @adobe/aem-spa-page-model-manager
19 | ```
20 |
21 | ## Documentation
22 |
23 | * [SPA Editor Overview](https://www.adobe.com/go/aem6_5_docs_spa_en)
24 | * [SPA Architecture](https://docs.adobe.com/content/help/en/experience-manager-65/developing/headless/spas/spa-architecture.html)
25 | * [Getting Started with the AEM SPA Editor and Angular](https://docs.adobe.com/content/help/en/experience-manager-learn/spa-angular-tutorial/overview.html)
26 | * [Getting Started with the AEM SPA Editor and React](https://docs.adobe.com/content/help/en/experience-manager-learn/spa-react-tutorial/overview.html)
27 |
28 | ## Contributing
29 |
30 | Contributions are welcome! Read the [Contributing Guide](CONTRIBUTING.md) for more information.
31 |
32 | ## Licensing
33 |
34 | This project is licensed under the Apache V2 License. See [LICENSE](LICENSE) for more information.
35 |
36 |
--------------------------------------------------------------------------------
/src/ModelClient.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | import { Model } from './Model';
14 |
15 | export class ModelClient {
16 | private _apiHost: string | null;
17 |
18 | /**
19 | * @constructor
20 | * @private
21 | * @param [apiHost] Http host of the API.
22 | */
23 | constructor(apiHost?: string) {
24 | this._apiHost = apiHost || null;
25 | }
26 |
27 | /**
28 | * Returns http host of the API.
29 | * @returns API host or `null`.
30 | */
31 | get apiHost(): string | null {
32 | return this._apiHost;
33 | }
34 |
35 | /**
36 | * Fetches a model using given resource path.
37 | * @param modelPath Absolute path to the model.
38 | * @return Promise to page model object.
39 | */
40 | public fetch(modelPath: string): Promise {
41 | if (!modelPath) {
42 | const err = `Fetching model rejected for path: ${modelPath}`;
43 |
44 | return Promise.reject(new Error(err));
45 | }
46 |
47 | // Either the API host has been provided or we make an absolute request relative to the current host
48 | const apihostPrefix = this._apiHost || '';
49 | const url = `${apihostPrefix}${modelPath}`;
50 |
51 | // Assure that the default credentials value ('same-origin') is set for browsers which do not set it
52 | // or which are setting the old default value ('omit')
53 | return fetch(url, { credentials: 'same-origin' }).then((response) => {
54 | if ((response.status >= 200) && (response.status < 300)) {
55 | return response.json() as Promise;
56 | }
57 |
58 | throw { response };
59 | }).catch((error) => {
60 | return Promise.reject(error);
61 | });
62 | }
63 |
64 | /**
65 | * Destroys the internal references to avoid memory leaks.
66 | * @private
67 | */
68 | public destroy(): void {
69 | this._apiHost = null;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@adobe/aem-spa-page-model-manager",
3 | "version": "1.5.0",
4 | "description": "An interpreter between AEM Editor and the AEM SPA Editor.",
5 | "keywords": [
6 | "spa",
7 | "aem",
8 | "pagel-model-manager",
9 | "adobe"
10 | ],
11 | "author": "Adobe Systems Inc. ",
12 | "license": "Apache-2.0",
13 | "repository": "github:adobe/aem-spa-page-model-manager",
14 | "homepage": "https://docs.adobe.com/content/help/en/experience-manager-65/developing/headless/spas/spa-reference-materials.html",
15 | "bugs": {
16 | "url": "https://github.com/adobe/aem-spa-page-model-manager/issues"
17 | },
18 | "engines": {
19 | "npm": ">=6.14.15",
20 | "node": ">=12.16.2"
21 | },
22 | "main": "dist/aem-spa-page-model-manager.js",
23 | "types": "dist/types.d.ts",
24 | "scripts": {
25 | "build:production": "NODE_ENV=production npm run build",
26 | "build:types": "tsc -p src/tsconfig.types.json",
27 | "build": "npm run clean && npm run lint && webpack && npm run build:types",
28 | "clean": "rm -rf dist/",
29 | "docs": "npm i && npx typedoc --entryPointStrategy expand --excludePrivate ./src --out ./dist/docs",
30 | "lint": "eslint .",
31 | "semantic-release": "semantic-release",
32 | "test:coverage": "jest --clearCache && jest --coverage",
33 | "test:debug": "jest --coverage --watchAll",
34 | "test": "jest --clearCache && jest"
35 | },
36 | "dependencies": {
37 | "clone": "^2.1.2",
38 | "path-browserify": "^1.0.1",
39 | "url": "^0.11.0"
40 | },
41 | "devDependencies": {
42 | "@adobe/eslint-config-editorxp": "^1.0.3",
43 | "@semantic-release/changelog": "^6.0.1",
44 | "@semantic-release/git": "^10.0.1",
45 | "@semantic-release/github": "^8.0.4",
46 | "@types/clone": "^2.1.0",
47 | "@types/jest": "^26.0.14",
48 | "@types/node": "^14.11.5",
49 | "@typescript-eslint/eslint-plugin": "^4.4.0",
50 | "@typescript-eslint/parser": "^4.4.0",
51 | "clean-webpack-plugin": "^3.0.0",
52 | "commitizen": "^4.2.4",
53 | "cz-conventional-changelog": "^3.0.1",
54 | "eslint": "^7.10.0",
55 | "eslint-plugin-header": "^3.1.0",
56 | "eslint-plugin-json": "^2.1.2",
57 | "jest": "^26.5.2",
58 | "jest-fetch-mock": "^3.0.3",
59 | "semantic-release": "^19.0.2",
60 | "ts-jest": "^26.4.1",
61 | "ts-loader": "^8.1.0",
62 | "ts-mockito": "^2.6.1",
63 | "typedoc": "^0.23.0",
64 | "typescript": "^4.0.3",
65 | "webpack": "^5.0.0",
66 | "webpack-cli": "^4.0.0",
67 | "webpack-node-externals": "^2.5.2"
68 | },
69 | "files": [
70 | "dist/**/*.{js,ts,map}",
71 | "!**/{docs,coverage}/"
72 | ],
73 | "config": {
74 | "commitizen": {
75 | "path": "./node_modules/cz-conventional-changelog"
76 | }
77 | },
78 | "eslintConfig": {
79 | "root": true,
80 | "extends": [
81 | "@adobe/eslint-config-editorxp/typescript"
82 | ]
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Adobe Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language.
18 | * Being respectful of differing viewpoints and experiences.
19 | * Gracefully accepting constructive criticism.
20 | * Focusing on what is best for the community.
21 | * Showing empathy towards other community members.
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances.
27 | * Trolling, insulting/derogatory comments, and personal or political attacks.
28 | * Public or private harassment.
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission.
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting.
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at Grp-opensourceoffice@adobe.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [https://contributor-covenant.org/version/1/4][version].
72 |
73 | [homepage]: https://contributor-covenant.org
74 | [version]: https://contributor-covenant.org/version/1/4/
--------------------------------------------------------------------------------
/test/data/EditorClientData.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | import { PageModel, ResponsiveGridModel } from './types';
14 |
15 | const DEFAULT_PAGE_MODEL_PATH = window.location.pathname.replace(/\.htm(l)?$/, '');
16 |
17 | export const CHILD0000_MODEL_JSON: ResponsiveGridModel = {
18 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12',
19 | 'columnCount': 12,
20 | ':itemsOrder': [ 'child0010', 'child0011' ],
21 | ':items': {
22 | 'child0010': { ':type': 'test/components/componentchild0' },
23 | 'child0011': { ':type': 'test/components/componentchild1' }
24 | },
25 | ':type': 'wcm/foundation/components/responsivegrid'
26 | };
27 |
28 | export const root: ResponsiveGridModel = {
29 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12',
30 | 'columnCount': 12,
31 | ':itemsOrder': [ 'child0000', 'child0001' ],
32 | ':items': {
33 | 'child0000': CHILD0000_MODEL_JSON,
34 | 'child0001': { ':type': 'test/components/componentchild1' }
35 | },
36 | ':type': 'wcm/foundation/components/responsivegrid'
37 | };
38 |
39 | export const dummyResponsiveGrid: ResponsiveGridModel = {
40 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12',
41 | 'columnCount': 12,
42 | ':type': 'wcm/foundation/components/responsivegrid'
43 | };
44 |
45 | export const childPage1Root: ResponsiveGridModel = {
46 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12',
47 | 'columnCount': 12,
48 | ':itemsOrder': [ 'child1000', 'child1001' ],
49 | ':items': {
50 | 'child1000': dummyResponsiveGrid,
51 | 'child1001': { ':type': 'test/components/componentchild1' }
52 | },
53 | ':type': 'wcm/foundation/components/responsivegrid'
54 | };
55 |
56 | export const childPage2Root: ResponsiveGridModel = {
57 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12',
58 | 'columnCount': 12,
59 | ':itemsOrder': [ 'child2000', 'child2001' ],
60 | ':items': {
61 | 'child2000': dummyResponsiveGrid,
62 | 'child2001': { ':type': 'test/components/componentchild1' }
63 | },
64 | ':type': 'wcm/foundation/components/responsivegrid'
65 | };
66 |
67 | export const PAGE_MODEL_JSON: PageModel = {
68 | ':path': DEFAULT_PAGE_MODEL_PATH,
69 | 'designPath': '/libs/settings/wcm/designs/default',
70 | 'title': 'React sample page',
71 | 'lastModifiedDate': 1512116041058,
72 | 'templateName': 'sample-template',
73 | 'cssClassNames': 'page',
74 | 'language': 'en-US',
75 | ':itemsOrder': [
76 | 'root'
77 | ],
78 | ':items': {
79 | 'root': root
80 | },
81 | ':hierarchyType': 'page',
82 | ':children': {
83 | '/content/test/child_page_1': {
84 | ':type': 'we-retail-journal/react/components/structure/page',
85 | ':items': {
86 | 'root': childPage1Root
87 | },
88 | ':itemsOrder': [
89 | 'root'
90 | ]
91 | },
92 | '/content/test/subpage2': {
93 | ':type': 'we-retail-journal/react/components/structure/page',
94 | ':items': {
95 | 'root': childPage2Root
96 | },
97 | ':itemsOrder': [
98 | 'root'
99 | ]
100 | }
101 | },
102 | ':type': 'we-retail-react/components/structure/page'
103 | };
104 |
--------------------------------------------------------------------------------
/test/ModelClient.test.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | import * as assert from 'assert';
14 | import fetchMock from 'jest-fetch-mock';
15 | import { Model } from '../src/Model';
16 | import { ModelClient } from '../src/ModelClient';
17 | import { content_test_child_page_1, PAGE_MODEL } from './data/MainPageData';
18 |
19 | const NON_EXISTING_URL = '/content/test/undefined';
20 | const PAGE_URL = '/content/test/page';
21 | const CHILD_PAGE_URL = '/content/test/child_page_1';
22 | const CHILD_PAGE_404_URL = '/content/test/child_page_404';
23 | const myEndPoint = 'http://localhost:4523';
24 |
25 | function getJSONResponse(body: Model) {
26 | return {
27 | status: 200,
28 | body: JSON.stringify(body),
29 | headers: {
30 | 'Content-type': 'application/json'
31 | }
32 | };
33 | }
34 |
35 | function mockTheFetch() {
36 | fetchMock.doMock((req) => {
37 | switch (req.url) {
38 | case myEndPoint + CHILD_PAGE_URL:
39 | return Promise.resolve(getJSONResponse(content_test_child_page_1));
40 |
41 | case myEndPoint + PAGE_URL:
42 | return Promise.resolve(getJSONResponse(PAGE_MODEL));
43 |
44 | case myEndPoint + CHILD_PAGE_404_URL:
45 | default:
46 | return Promise.reject({ code: 404, body: 'Not found' });
47 | }
48 | });
49 | }
50 |
51 | describe('ModelClient ->', () => {
52 | beforeEach(() => {
53 | fetchMock.resetMocks();
54 | mockTheFetch();
55 | });
56 |
57 | describe('fetch ->', () => {
58 | it('should reject when the remote model endpoint is not found', () => {
59 | const modelClient = new ModelClient(myEndPoint);
60 |
61 | return modelClient.fetch(NON_EXISTING_URL).then((data) => {
62 | assert.fail(data, undefined);
63 | }).catch((error) => {
64 | expect(error.code).toEqual(404);
65 | });
66 | });
67 |
68 | it('should return the expected data', () => {
69 | const modelClient = new ModelClient(myEndPoint);
70 |
71 | return modelClient.fetch(PAGE_URL).then((data) => {
72 | assert.deepEqual(data, PAGE_MODEL);
73 |
74 | return modelClient.fetch(CHILD_PAGE_URL);
75 | }).then((data) => {
76 | assert.deepEqual(data, content_test_child_page_1);
77 |
78 | return modelClient.fetch(CHILD_PAGE_404_URL);
79 | }).catch((error) => {
80 | expect(error.code).toEqual(404);
81 | });
82 | });
83 |
84 | describe('handling incorrect parameter', () => {
85 | const modelClient = new ModelClient(myEndPoint);
86 |
87 | // failing as the undefined is passed to the PathUtils which can not handle this case
88 | // it('should resolve with Error - when no URL provided', (done) => {
89 | //
90 | // modelClient.fetch(undefined).catch((e) => {
91 | // assert.isNotNull(e);
92 | // done();
93 | // });
94 | // });
95 |
96 | it('should resolve with Error - when empty URL provided', (done) => {
97 | modelClient.fetch('').catch((error) => {
98 | expect(error.message).toBeDefined();
99 | done();
100 | });
101 | });
102 | });
103 | });
104 | });
105 |
--------------------------------------------------------------------------------
/src/EditorClient.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | import clone from 'clone';
14 | import EventType from './EventType';
15 | import { ModelManager } from './ModelManager';
16 | import { PathUtils } from './PathUtils';
17 |
18 | /**
19 | * Broadcast an event to indicate the page model has been loaded
20 | * @param model - model item to be added to the broadcast payload
21 | * @fires cq-pagemodel-loaded
22 | * @private
23 | */
24 | export function triggerPageModelLoaded(model: any): void {
25 | // Deep copy to protect the internal state of the page mode
26 | PathUtils.dispatchGlobalCustomEvent(EventType.PAGE_MODEL_LOADED, {
27 | detail: {
28 | model: clone(model)
29 | }
30 | });
31 | }
32 |
33 | /**
34 | * The EditorClient is responsible for the interactions with the Page Editor.
35 | * @private
36 | */
37 | export class EditorClient {
38 | public _modelManager: ModelManager;
39 | public _windowListener: any;
40 |
41 | constructor(modelManager: ModelManager) {
42 | this._modelManager = modelManager;
43 |
44 | this._windowListener = (event: any) => {
45 | if (!event || !event.detail || !event.detail.msg) {
46 | console.error('EditorService.js', 'No message passed to cq-pagemodel-update', event);
47 |
48 | return;
49 | }
50 |
51 | this._updateModel(event.detail.msg);
52 | };
53 |
54 | if (PathUtils.isBrowser()) {
55 | window.addEventListener(EventType.PAGE_MODEL_UPDATE, this._windowListener);
56 | }
57 | }
58 |
59 | /**
60 | * Updates the page model with the given data
61 | *
62 | * @param {Object} msg - Object containing the data to update the page model
63 | * @property {String} msg.dataPath - Relative data path in the PageModel which needs to be updated
64 | * @property {String} msg.pagePath - Absolute page path corresponding to the page in the PageModel which needs to be updated
65 | * @param {String} msg.cmd - Command Action requested via Editable on the content Node
66 | * @param {Object} msg.data - Data that needs to be updated in the PageModel at {path}
67 | * @fires cq-pagemodel-loaded
68 | * @private
69 | */
70 | public _updateModel(msg: any) {
71 | if (!msg || !msg.cmd || !msg.path) {
72 | console.error('PageModelManager.js', 'Not enough data received to update the page model');
73 |
74 | return;
75 | }
76 |
77 | // Path in the PageModel which needs to be updated
78 | const path = msg.path;
79 |
80 | // Command Action requested via Editable on the content Node
81 | const cmd = msg.cmd;
82 |
83 | // Data that needs to be updated in the page model at the given path
84 | const data = clone(msg.data);
85 |
86 | let siblingName;
87 | let itemPath;
88 | let insertBefore;
89 | const parentNodePath = PathUtils.getParentNodePath(path);
90 |
91 | switch (cmd) {
92 | case 'replace':
93 | this._modelManager.modelStore.setData(path, data);
94 | this._modelManager._notifyListeners(path);
95 | break;
96 |
97 | case 'delete':
98 | this._modelManager.modelStore.removeData(path);
99 |
100 | if (parentNodePath) {
101 | this._modelManager._notifyListeners(parentNodePath);
102 | }
103 |
104 | break;
105 |
106 | case 'insertBefore':
107 | insertBefore = true;
108 | // No break as we want both insert command to be treated the same way
109 | // eslint-disable-next-line no-fallthrough
110 |
111 | case 'insertAfter':
112 | // The logic relative to the item path and sibling between the editor and the ModelManager is reversed
113 | // Adapting the command to the ModelManager API
114 | siblingName = PathUtils.getNodeName(path);
115 |
116 | if (parentNodePath) {
117 | itemPath = parentNodePath + '/' + data.key;
118 | this._modelManager.modelStore.insertData(itemPath, data.value, siblingName, insertBefore);
119 | this._modelManager._notifyListeners(parentNodePath);
120 | }
121 |
122 | break;
123 |
124 | default:
125 | // 'replaceContent' command not supported
126 | // 'moveBefore', 'moveAfter' commands not supported.
127 | // As instead, we are replacing source and destination parents because they can contain data about the item we want to relocate
128 | console.log('EditorClient', 'unsupported command:', cmd);
129 | }
130 |
131 | triggerPageModelLoaded(this._modelManager.modelStore.dataMap);
132 | }
133 |
134 | /**
135 | * @private
136 | */
137 | public destroy() {
138 | if (PathUtils.isBrowser()) {
139 | window.removeEventListener(EventType.PAGE_MODEL_UPDATE, this._windowListener);
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/test/data/MainPageData.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | import { Model } from '../../src/Model';
14 | import { PageModel, ResponsiveGridModel } from './types';
15 |
16 | export const content_test_page_root_child0000_child0010: Model = { ':type': 'test/components/componentchild0' };
17 | export const content_test_page_root_child0000_child0011: Model = { ':type': 'test/components/componentchild1' };
18 |
19 | export const content_test_page_root_child0000: ResponsiveGridModel = {
20 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12',
21 | 'columnCount': 12,
22 | ':itemsOrder': [ 'child0010', 'child0011' ],
23 | ':items': {
24 | 'child0010': content_test_page_root_child0000_child0010,
25 | 'child0011': content_test_page_root_child0000_child0011
26 | },
27 | ':type': 'wcm/foundation/components/responsivegrid'
28 | };
29 |
30 | export const content_test_page_root_child0001: Model = { ':type': 'test/components/componentchild1' };
31 | export const content_test_page_root: ResponsiveGridModel = {
32 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12',
33 | 'columnCount': 12,
34 | ':itemsOrder': [ 'child0000', 'child0001' ],
35 | ':items': {
36 | 'child0000': content_test_page_root_child0000,
37 | 'child0001': content_test_page_root_child0001
38 | },
39 | ':type': 'wcm/foundation/components/responsivegrid'
40 | };
41 |
42 | export const content_test_child_page_1_root_child1001: Model = { ':type': 'test/components/componentchild1' };
43 |
44 | export const content_test_child_page_1_root_child1000: ResponsiveGridModel = {
45 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12',
46 | 'columnCount': 12,
47 | ':type': 'wcm/foundation/components/responsivegrid'
48 | };
49 |
50 | export const content_test_child_page_1_root: ResponsiveGridModel = {
51 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12',
52 | 'columnCount': 12,
53 | ':itemsOrder': [ 'child1000', 'child1001' ],
54 | ':items': {
55 | 'child1000': content_test_child_page_1_root_child1000,
56 | 'child1001': content_test_child_page_1_root_child1001
57 | },
58 | ':type': 'wcm/foundation/components/responsivegrid'
59 | };
60 |
61 | export const content_test_child_page_1: PageModel = {
62 | ':type': 'we-retail-journal/react/components/structure/page',
63 | ':path': '/content/test/child_page_1',
64 | ':items': {
65 | 'root': content_test_child_page_1_root
66 | },
67 | ':itemsOrder': [
68 | 'root'
69 | ]
70 | };
71 |
72 | export const content_test_subpage2_root_child2000: ResponsiveGridModel = {
73 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12',
74 | 'columnCount': 12,
75 | ':type': 'wcm/foundation/components/responsivegrid'
76 | };
77 |
78 | export const content_test_subpage2_root_child2001: Model = { ':type': 'test/components/componentchild1' };
79 |
80 | export const content_test_subpage2_root: ResponsiveGridModel = {
81 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12',
82 | 'columnCount': 12,
83 | ':itemsOrder': [ 'child2000', 'child2001' ],
84 | ':items': {
85 | 'child2000': content_test_subpage2_root_child2000,
86 | 'child2001': content_test_subpage2_root_child2001
87 | },
88 | ':type': 'wcm/foundation/components/responsivegrid'
89 | };
90 |
91 | export const content_test_subpage2: PageModel = {
92 | ':type': 'we-retail-journal/react/components/structure/page',
93 | ':items': {
94 | 'root': content_test_subpage2_root
95 | },
96 | ':itemsOrder': [
97 | 'root'
98 | ]
99 | };
100 |
101 | export const content_test_subpage2_root_page: PageModel = { ':type': 'test/components/page' };
102 |
103 | export const content_test_subpage2_rootPage: PageModel = {
104 | ':type': 'we-retail-journal/react/components/structure/page',
105 | ':items': {
106 | 'page': content_test_subpage2_root_page
107 | },
108 | ':itemsOrder': [
109 | 'page'
110 | ]
111 | };
112 |
113 | export const PAGE_MODEL: PageModel = {
114 | 'designPath': '/libs/settings/wcm/designs/default',
115 | 'title': 'React sample page',
116 | 'lastModifiedDate': 1512116041058,
117 | 'templateName': 'sample-template',
118 | 'cssClassNames': 'page',
119 | 'language': 'en-US',
120 | ':itemsOrder': [
121 | 'root'
122 | ],
123 | ':items': {
124 | 'root': content_test_page_root
125 | },
126 | ':hierarchyType': 'page',
127 | ':children': {
128 | '/content/test/child_page_1': content_test_child_page_1,
129 | '/content/test/subpage2/subpage22': content_test_subpage2_rootPage,
130 | '/content/test/subpage2': content_test_subpage2
131 | },
132 | ':path': '/content/test/page',
133 | ':type': 'we-retail-react/components/structure/page'
134 | };
135 |
136 | export const ERROR_PAGE_MODEL_404: PageModel = {
137 | 'designPath': '/libs/settings/wcm/designs/default',
138 | 'title': 'Example Error Page 404',
139 | 'lastModifiedDate': 1512116041058,
140 | 'templateName': 'sample-template',
141 | 'cssClassNames': 'page',
142 | 'language': 'en-US',
143 | ':itemsOrder': [
144 | 'root'
145 | ],
146 | ':items': {
147 | 'root': content_test_page_root
148 | },
149 | ':hierarchyType': 'page',
150 | ':children': {},
151 | ':path': '/content/test/page',
152 | ':type': 'we-retail-react/components/structure/page'
153 | };
154 |
155 | export const ERROR_PAGE_MODEL_500: PageModel = {
156 | 'designPath': '/libs/settings/wcm/designs/default',
157 | 'title': 'Example Error Page 500',
158 | 'lastModifiedDate': 1512116041058,
159 | 'templateName': 'sample-template',
160 | 'cssClassNames': 'page',
161 | 'language': 'en-US',
162 | ':itemsOrder': [
163 | 'root'
164 | ],
165 | ':items': {
166 | 'root': content_test_page_root
167 | },
168 | ':hierarchyType': 'page',
169 | ':children': {},
170 | ':path': '/content/test/page',
171 | ':type': 'we-retail-react/components/structure/page'
172 | };
173 |
--------------------------------------------------------------------------------
/src/ModelRouter.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | import EventType from './EventType';
14 | import MetaProperty from './MetaProperty';
15 | import ModelManager from './ModelManager';
16 | import { PathUtils } from './PathUtils';
17 |
18 | /**
19 | * The ModelRouter listens for HTML5 History API popstate events
20 | * and calls {@link PageModelManager#getData()} with the model path it extracted from the URL.
21 | *
22 | * Configuration
23 | *
24 | * The Model Router can be configured using meta properties located in the head section of the document.
25 | *
26 | * Meta properties
27 | *
28 | * - cq:page_model_router - default=undefined, options=disable
29 | * - cq:page_model_route_filters - default=undefined, options=RegExp<>
30 | *
31 | *
32 | * Defaults
33 | *
34 | * - The ModelRouter is enabled and uses the History API to extract the model path from the current content path
35 | *
36 | *
37 | * Examples and Usages
38 | *
39 | * Disables the page model router
40 | *
41 | * e.g. <meta property="cq:page_model_router" content="disable"\>
42 | *
43 | *
44 | * Filters paths from the model routing with the given patterns
45 | *
46 | * e.g. <meta property="cq:page_model_route_filters" content="route/not/found,^(.*)(?:exclude/path)(.*)"\>
47 | *
48 | *
49 | * @module ModelRouter
50 | */
51 |
52 | /**
53 | * Modes in which the Model Router operates.
54 | * @private
55 | */
56 | export class RouterModes {
57 | /**
58 | * Flag that indicates that the model router should be disabled.
59 | */
60 | public static readonly DISABLED = 'disabled';
61 |
62 | /**
63 | * Flag that indicates that the model router should extract the model path from the content path section of the URL.
64 | */
65 | public static readonly CONTENT_PATH = 'path';
66 |
67 | private constructor() {
68 | // hide constructor
69 | }
70 | }
71 |
72 | /**
73 | * Returns the model path. If no URL is provided the current window URL is used
74 | * @param [url] url from which to extract the model path
75 | * @private
76 | * @return
77 | */
78 | export function getModelPath(url?: string | null | URL): string | null {
79 | const localUrl = (url || (PathUtils.isBrowser() && window.location.pathname)) as string;
80 |
81 | if (localUrl) {
82 | return PathUtils.sanitize(localUrl);
83 | }
84 |
85 | return null;
86 | }
87 |
88 | /**
89 | * Returns the list of provided route filters
90 | *
91 | * @returns {string[]}
92 | *
93 | * @private
94 | */
95 | export function getRouteFilters(): string[] {
96 | const routeFilters = PathUtils.getMetaPropertyValue(MetaProperty.PAGE_MODEL_ROUTE_FILTERS);
97 |
98 | return routeFilters ? routeFilters.split(',') : [];
99 | }
100 |
101 | /**
102 | * Should the route be excluded
103 | *
104 | * @param route
105 | * @returns {boolean}
106 | *
107 | * @private
108 | */
109 | export function isRouteExcluded(route: string): boolean {
110 | const routeFilters = getRouteFilters();
111 |
112 | for (let i = 0, length = routeFilters.length; i < length; i++) {
113 | if (new RegExp(routeFilters[i]).test(route)) {
114 | return true;
115 | }
116 | }
117 |
118 | return false;
119 | }
120 |
121 | /**
122 | * Is the model router enabled. Enabled by default
123 | * @returns {boolean}
124 | * @private
125 | */
126 | export function isModelRouterEnabled(): boolean {
127 | if (!PathUtils.isBrowser()) {
128 | return false;
129 | }
130 |
131 | const modelRouterMetaType = PathUtils.getMetaPropertyValue(MetaProperty.PAGE_MODEL_ROUTER);
132 |
133 | // Enable the the page model routing by default
134 | return !modelRouterMetaType || (RouterModes.DISABLED !== modelRouterMetaType);
135 | }
136 |
137 | /**
138 | * Fetches the model from the PageModelManager and then dispatches it
139 | *
140 | * @fires cq-pagemodel-route-changed
141 | *
142 | * @param {string} [path] - path of the model to be dispatched
143 | *
144 | * @private
145 | */
146 | export function dispatchRouteChanged(path: string): void {
147 | // Triggering the page model manager to load a new child page model
148 | // No need to use a cache as the PageModelManager already does it
149 | ModelManager.getData({ path }).then((model) => {
150 | PathUtils.dispatchGlobalCustomEvent(EventType.PAGE_MODEL_ROUTE_CHANGED, {
151 | detail: {
152 | model
153 | }
154 | });
155 | });
156 | }
157 |
158 | /**
159 | * Triggers the PageModelManager to fetch data based on the current route
160 | *
161 | * @fires cq-pagemodel-route-changed - with the root page model object
162 | *
163 | * @param {string} [url] - url from which to extract the model path
164 | *
165 | * @private
166 | */
167 | export function routeModel(url?: string | undefined | null | URL): void {
168 | if (!isModelRouterEnabled()) {
169 | return;
170 | }
171 |
172 | const path = getModelPath(url);
173 |
174 | // don't fetch the model
175 | // for the root path
176 | // or when the route is excluded
177 | if (!path || ('/' === path) || isRouteExcluded(path)) {
178 | return;
179 | }
180 |
181 | dispatchRouteChanged(path);
182 | }
183 |
184 | export function initModelRouter(): void {
185 | // Activate the model router
186 | if (isModelRouterEnabled() && PathUtils.isBrowser()) {
187 | // Encapsulate the history.pushState and history.replaceState functions to prefetch the page model for the current route
188 | const pushState = window.history.pushState;
189 | const replaceState = window.history.replaceState;
190 |
191 | window.addEventListener('popstate', e => {
192 | const target = e?.target as Window;
193 |
194 | routeModel(target?.location?.pathname || null);
195 | });
196 |
197 | window.history.pushState = (state, title, url) => {
198 | routeModel(url);
199 |
200 | return pushState.apply(history, [ state, title, url ]);
201 | };
202 |
203 | window.history.replaceState = (state, title, url) => {
204 | routeModel(url || null);
205 |
206 | return replaceState.apply(history, [ state, title, url ]);
207 | };
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/test/ModelRouter.test.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | import MetaProperty from '../src/MetaProperty';
14 | import { Model } from '../src/Model';
15 | import ModelManager from '../src/ModelManager';
16 | import {
17 | dispatchRouteChanged, getModelPath, getRouteFilters, initModelRouter, isModelRouterEnabled, isRouteExcluded, routeModel, RouterModes
18 | } from '../src/ModelRouter';
19 | import { PathUtils } from '../src/PathUtils';
20 |
21 | let metaProps: { [key: string]: string } = {};
22 | const modelManagerSpy: jest.SpyInstance> = jest.spyOn(ModelManager, 'getData');
23 |
24 | describe('ModelRouter ->', () => {
25 | const DEFAULT_PAGE_MODEL_PATH = window.location.pathname.replace(/\.htm(l)?$/, '');
26 | const TEST_PATH = '/test';
27 | const TEST_MODEL = { test: 'model' };
28 | const MODEL_ROUTE_FILTERS = [ 'f1', 'f2', 'f3' ];
29 | const MODEL_ROUTE_FILTERS_STR = MODEL_ROUTE_FILTERS.join(',');
30 |
31 | beforeEach(() => {
32 | metaProps = {};
33 | jest.spyOn(PathUtils, 'getMetaPropertyValue').mockImplementation((val) => metaProps[val]);
34 | });
35 |
36 | describe('getModelPath ->', () => {
37 | it('should get the current window URL', () => {
38 | expect(getModelPath()).toEqual(DEFAULT_PAGE_MODEL_PATH);
39 | });
40 |
41 | it('should get the current window URL', () => {
42 | expect(getModelPath('/path.model.json')).toEqual('/path');
43 | });
44 |
45 | it('should get the current window URL', () => {
46 | expect(getModelPath('/zyx/abc?test=test')).toEqual('/zyx/abc');
47 | });
48 |
49 | it('should get the current window URL', () => {
50 | expect(getModelPath('/zyx/abc?date=03.10.2021')).toEqual('/zyx/abc');
51 | });
52 | it('should return null', () => {
53 | const isBrowserSpy = jest.spyOn(PathUtils, 'isBrowser').mockImplementation(() => false);
54 |
55 | expect(getModelPath()).toBeNull();
56 | isBrowserSpy.mockRestore();
57 | });
58 | });
59 |
60 | describe('dispatchRouteChanged ->', () => {
61 | beforeEach(() => {
62 | modelManagerSpy.mockResolvedValue(TEST_MODEL as Model);
63 | });
64 |
65 | afterEach(() => {
66 | modelManagerSpy.mockReset();
67 | });
68 |
69 | it('should get the current window URL', () => {
70 | dispatchRouteChanged(TEST_PATH);
71 | expect(modelManagerSpy).toHaveBeenCalledWith({ path: TEST_PATH });
72 | });
73 | });
74 |
75 | describe('routeModel ->', () => {
76 | beforeEach(() => {
77 | modelManagerSpy.mockResolvedValue(TEST_MODEL as Model);
78 | });
79 |
80 | afterEach(() => {
81 | modelManagerSpy.mockReset();
82 | });
83 |
84 | it('should route the model based on the window URL', () => {
85 | const { location } = window;
86 |
87 | // @ts-ignore
88 | delete window.location;
89 |
90 | // @ts-ignore
91 | window.location = {
92 | pathname: '/some/path/name'
93 | };
94 |
95 | routeModel();
96 | expect(modelManagerSpy).toHaveBeenCalledWith({ path: '/some/path/name' });
97 |
98 | window.location = location;
99 | });
100 |
101 | it('should route the model based on provided path', () => {
102 | routeModel(TEST_PATH);
103 | expect(modelManagerSpy).toHaveBeenCalledWith({ path: TEST_PATH });
104 | });
105 | });
106 |
107 | describe('getRouteFilters ->', () => {
108 | afterEach(() => {
109 | metaProps = {};
110 | });
111 |
112 | it('should return an empty list of route filters', () => {
113 | expect(getRouteFilters()).toEqual([]);
114 | });
115 |
116 | it('should return a list of route filters', () => {
117 | metaProps[MetaProperty.PAGE_MODEL_ROUTE_FILTERS] = MODEL_ROUTE_FILTERS_STR;
118 | expect(getRouteFilters()).toEqual(expect.arrayContaining(MODEL_ROUTE_FILTERS));
119 | });
120 | });
121 |
122 | describe('isModelRouterEnabled ->', () => {
123 | afterEach(() => {
124 | metaProps = {};
125 | });
126 |
127 | it('should return an enabled route model by default', () => {
128 | expect(isModelRouterEnabled()).toEqual(true);
129 | });
130 |
131 | it('should return a disabled route model', () => {
132 | metaProps[MetaProperty.PAGE_MODEL_ROUTER] = RouterModes.DISABLED;
133 |
134 | expect(isModelRouterEnabled()).toEqual(false);
135 | });
136 | });
137 |
138 | describe('isRouteExcluded ->', () => {
139 | afterEach(() => {
140 | metaProps = {};
141 | });
142 |
143 | it('should filter a route', () => {
144 | expect(isRouteExcluded(MODEL_ROUTE_FILTERS[0])).toEqual(false);
145 | expect(isRouteExcluded(MODEL_ROUTE_FILTERS[1])).toEqual(false);
146 | expect(isRouteExcluded(MODEL_ROUTE_FILTERS[2])).toEqual(false);
147 | });
148 |
149 | it('should filter a route', () => {
150 | metaProps[MetaProperty.PAGE_MODEL_ROUTE_FILTERS] = MODEL_ROUTE_FILTERS_STR;
151 |
152 | expect(isRouteExcluded(MODEL_ROUTE_FILTERS[0])).toEqual(true);
153 | expect(isRouteExcluded(MODEL_ROUTE_FILTERS[1])).toEqual(true);
154 | expect(isRouteExcluded(MODEL_ROUTE_FILTERS[2])).toEqual(true);
155 | });
156 | });
157 |
158 | describe('router model on history ->', () => {
159 | const originalLocation = window.location;
160 |
161 | beforeEach(() => {
162 | initModelRouter();
163 | modelManagerSpy.mockResolvedValue(TEST_MODEL as Model);
164 | });
165 |
166 | afterAll(() => {
167 | Object.defineProperty(window, 'location', { configurable: true, value: originalLocation });
168 | modelManagerSpy.mockReset();
169 | });
170 | it('should fetch model on history push', () => {
171 | window.history.pushState({}, '', '/test');
172 | expect(modelManagerSpy).toHaveBeenCalledWith({ path: '/test' });
173 | });
174 | it('should fetch model on history replace', () => {
175 | window.history.replaceState({}, '', '/test');
176 | expect(modelManagerSpy).toHaveBeenCalledWith({ path: '/test' });
177 | });
178 | it('should fetch model on history pop', () => {
179 | Object.defineProperty(window, 'location', {
180 | configurable: true,
181 | value: { pathname: '/test' }
182 | });
183 | window.dispatchEvent(new Event('popstate'));
184 | expect(modelManagerSpy).toHaveBeenCalledWith({ path: '/test' });
185 | });
186 |
187 | });
188 | });
189 |
--------------------------------------------------------------------------------
/src/AuthoringUtils.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | import { AEM_MODE } from './Constants';
14 | import { PathUtils } from './PathUtils';
15 | import MetaProperty from './MetaProperty';
16 |
17 | export class AuthoringUtils {
18 | private readonly _apiDomain: string | null;
19 |
20 | /**
21 | * Base path for editor clientlibs.
22 | */
23 | public static readonly EDITOR_CLIENTLIB_PATH = '/etc.clientlibs/cq/gui/components/authoring/editors/clientlibs/';
24 |
25 | /**
26 | * Authoring libraries.
27 | */
28 | public static readonly AUTHORING_LIBRARIES = {
29 | JS: [
30 | AuthoringUtils.EDITOR_CLIENTLIB_PATH + 'internal/messaging.js',
31 | AuthoringUtils.EDITOR_CLIENTLIB_PATH + 'utils.js',
32 | AuthoringUtils.EDITOR_CLIENTLIB_PATH + 'internal/page.js',
33 | AuthoringUtils.EDITOR_CLIENTLIB_PATH + 'internal/pagemodel/messaging.js'
34 | ],
35 | CSS: [
36 | AuthoringUtils.EDITOR_CLIENTLIB_PATH + 'internal/page.css'
37 | ],
38 | META: {
39 | [MetaProperty.WCM_DATA_TYPE]: 'JSON'
40 | }
41 | };
42 |
43 | /**
44 | * @private
45 | */
46 | constructor(domain: string | null) {
47 | this._apiDomain = domain;
48 | }
49 |
50 | /**
51 | * @private
52 | */
53 | getApiDomain(): string | null {
54 | return this._apiDomain;
55 | }
56 |
57 | /**
58 | * Generates