├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── cfn
├── configs
│ ├── customembed
│ │ └── us-east-1
│ │ │ └── config.yml
│ └── tags
│ │ ├── global.yml
│ │ └── prod.yml
├── manifest.yml
└── templates
│ └── static-site.yml
├── deployspec.yml
├── docs
├── getting-started.md
├── reference.md
└── testing.md
├── package-lock.json
├── package.json
├── public
├── audio
│ ├── audio0.mp3
│ ├── audio1.mp3
│ ├── audio2.mp3
│ ├── audio3.mp3
│ ├── cover0.png
│ ├── cover1.jpg
│ ├── cover2.jpg
│ └── cover3.jpg
├── audioEditApi.html
├── audioSearchApi.html
├── audioViewApi.html
├── blank
│ ├── edit.html
│ ├── search.html
│ └── view.html
├── css
│ ├── all.min.css
│ └── app.css
├── editApi.html
├── favicon.ico
├── img
│ ├── list-play-hover.png
│ ├── list-play-light.png
│ ├── mute.svg
│ ├── now-playing.svg
│ ├── pause.svg
│ ├── play.svg
│ └── volume.svg
├── index.html
├── searchApi.html
├── viewApi.html
└── webfonts
│ ├── fa-brands-400.eot
│ ├── fa-brands-400.svg
│ ├── fa-brands-400.ttf
│ ├── fa-brands-400.woff
│ ├── fa-brands-400.woff2
│ ├── fa-regular-400.eot
│ ├── fa-regular-400.svg
│ ├── fa-regular-400.ttf
│ ├── fa-regular-400.woff
│ ├── fa-regular-400.woff2
│ ├── fa-solid-900.eot
│ ├── fa-solid-900.svg
│ ├── fa-solid-900.ttf
│ ├── fa-solid-900.woff
│ └── fa-solid-900.woff2
├── src
├── App.css
├── App.test.js
├── App.tsx
├── EditPanel.tsx
├── Header.tsx
├── IframeHost.tsx
├── SearchPanel.tsx
├── SettingsInput.tsx
├── ViewPanel.tsx
├── index.css
├── index.tsx
├── logo.svg
└── react-app-env.d.ts
└── tsconfig.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | sourceType: 'module',
5 | // https://github.com/typescript-eslint/typescript-eslint/issues/251#issuecomment-463943250
6 | tsconfigRootDir: __dirname,
7 | project: 'tsconfig.json',
8 | ecmaFeatures: {
9 | jsx: true
10 | }
11 | },
12 | extends: [
13 | 'react-app',
14 | 'plugin:@typescript-eslint/recommended',
15 | 'plugin:react/recommended',
16 | 'plugin:jsx-a11y/recommended',
17 | // prettier configs should come last to avoid rule conflicts
18 | // https://github.com/prettier/eslint-config-prettier#installation
19 | 'plugin:prettier/recommended',
20 | 'prettier/@typescript-eslint',
21 | 'prettier/standard',
22 | 'prettier/react'
23 | ],
24 | plugins: [
25 | 'react-hooks',
26 | '@typescript-eslint',
27 | 'prettier',
28 | 'react',
29 | 'standard'
30 | ],
31 | env: {
32 | browser: true,
33 | jest: true
34 | },
35 | rules: {
36 | 'react-hooks/rules-of-hooks': 'error',
37 | 'react-hooks/exhaustive-deps': 'warn',
38 | '@typescript-eslint/interface-name-prefix': 'off',
39 | '@typescript-eslint/explicit-function-return-type': 'off'
40 | },
41 | overrides: [
42 | {
43 | files: ['*locale.js'],
44 | rules: {
45 | camelcase: 'off',
46 | '@typescript-eslint/camelcase': 'off'
47 | }
48 | }
49 | ],
50 | settings: {
51 | react: {
52 | version: 'detect'
53 | }
54 | },
55 | globals: {
56 | $: 'readonly'
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
63 |
64 | # dependencies
65 | /node_modules
66 | /.pnp
67 | .pnp.js
68 |
69 | # testing
70 | /coverage
71 |
72 | # production
73 | /build
74 |
75 | # misc
76 | .DS_Store
77 | .env.local
78 | .env.development.local
79 | .env.test.local
80 | .env.production.local
81 |
82 | npm-debug.log*
83 | yarn-debug.log*
84 | yarn-error.log*
85 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false
4 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Custom Embed Reference Implementation and Testing Tool
2 |
3 | ## Overview
4 |
5 | This project contains:
6 | * several reference implementations of Arc Custom Embeds
7 | * a "blank" custom embed implementation for quick-start
8 | * a host application for testing without using Composer
9 |
10 | ## Documentation
11 |
12 | For Arc users with PageBuilder Fusion running, we recommend walking through the process of building an embed first.
13 |
14 | [Getting Started with Custom Embeds](./docs/getting-started.md)
15 |
16 | Whether you're using Fusion or not, it may help to look at our starter code or reference implementations.
17 |
18 | [Starter Code](./public/blank)
19 |
20 | [Reference Implementations](./public/)
21 |
22 | For all users, we suggest running the testing tool:
23 |
24 | [Testing Tool Docs](./docs/testing.md)
25 |
26 | For more information, see the complete embed specification:
27 |
28 | [Custom Embed Specification](./docs/reference.md)
29 |
30 | ## Bugfixes
31 |
32 | A previous version of this example contained a bug that would result in spaces being decoded as plus (+) symbols. This
33 | was corrected on Sept 26th, 2019, but any work built off of an earlier version of this repo should be reviewed and
34 | modified if necessary. See [the following pull request](https://github.com/washingtonpost/arc-custom-embed/pull/6) for
35 | the changes applied.
36 |
--------------------------------------------------------------------------------
/cfn/configs/customembed/us-east-1/config.yml:
--------------------------------------------------------------------------------
1 | template: cfn/templates/static-site.yml
2 | stack_name: static-site-customembed-prod
3 | tag_file:
4 | - cfn/configs/tags/global.yml
5 | - cfn/configs/tags/prod.yml
6 | region: us-east-1
7 | context:
8 | name: customembed
9 | environment: prod
10 |
11 | site:
12 | # Required, Root domain name for the website.)
13 | domain: customembed.ellipsis.aws.arc.pub
14 |
15 | # Required, Domain name to lookup Route53 Hosted Zone. (can be equal to domain)
16 | zone_apex: ellipsis.aws.arc.pub
17 |
18 | # Not required, Subdomain name of the website, usually includes environment name
19 | #subdomain: prod
20 |
21 | # Required, don't deploy static sites without https
22 | certificate: arn:aws:acm:us-east-1:192577119545:certificate/e8ffe5a3-46f9-4720-9e92-c17af4d7342a
23 |
--------------------------------------------------------------------------------
/cfn/configs/tags/global.yml:
--------------------------------------------------------------------------------
1 | App: arc-custom-embed
--------------------------------------------------------------------------------
/cfn/configs/tags/prod.yml:
--------------------------------------------------------------------------------
1 | Env: Prod
--------------------------------------------------------------------------------
/cfn/manifest.yml:
--------------------------------------------------------------------------------
1 | templates:
2 | shared:
3 | source: v1/cfn/shared
4 | version: 1a61d8026fb21b105382c7dc24529c431434e8d3
5 | plugins:
6 | static-site:
7 | cfn/templates/static-site.yml: 1a61d8026fb21b105382c7dc24529c431434e8d3
8 | deployspec.yml: 1a61d8026fb21b105382c7dc24529c431434e8d3
9 | cfn/configs/customembed/us-east-1/config.yml: 1a61d8026fb21b105382c7dc24529c431434e8d3
10 | cfn/configs/customembed/config.yml: 1a61d8026fb21b105382c7dc24529c431434e8d3
11 | cfn/templates/pipeline.template.yml: 1a61d8026fb21b105382c7dc24529c431434e8d3
12 |
--------------------------------------------------------------------------------
/cfn/templates/static-site.yml:
--------------------------------------------------------------------------------
1 | ---
2 | Metadata:
3 | Stackjack:
4 | CanonicalTemplate: v1/cfn/shared/static-site/static-site.template.yml
5 | AllowDestroy:
6 | - CloudFrontDistribution
7 |
8 | Parameters:
9 | RootDomainName:
10 | Description: Root domain name for the website.
11 | Type: String
12 | Default: {{ site.domain }}
13 | ZoneApex:
14 | Description: Domain name to lookup Route53 Hosted Zone.
15 | Type: String
16 | Default: {{ site.zone_apex }}
17 | SiteDomainName:
18 | Description: FQDN of the website.
19 | Type: String
20 | Default: {% if site.subdomain is defined %}{{ site.subdomain }}.{% endif %}{{ site.domain }}
21 | CertificateARN:
22 | Type: String
23 | Description: the Amazon Resource Name (ARN) of an AWS Certificate Manager (ACM) certificate.
24 | Default: {{ site.certificate }}
25 | AllowedPattern: "arn:aws:acm:.*"
26 |
27 | Resources:
28 | # Common resources for all environments
29 | OriginAccessIdentity:
30 | Type: "AWS::CloudFront::CloudFrontOriginAccessIdentity"
31 | Properties:
32 | CloudFrontOriginAccessIdentityConfig:
33 | Comment: This configuration blocks direct access to the S3 bucket so that all traffic goes through CloudFront
34 |
35 | SiteBucket:
36 | Type: AWS::S3::Bucket
37 | Properties:
38 | BucketName: !Ref SiteDomainName
39 | AccessControl: PublicRead
40 | WebsiteConfiguration:
41 | IndexDocument: "index.html"
42 | ErrorDocument: "404.html"
43 | Tags:
44 | - Key: Name
45 | Value: !Ref SiteDomainName
46 |
47 | BucketPolicy:
48 | Type: AWS::S3::BucketPolicy
49 | Properties:
50 | PolicyDocument:
51 | Id: SiteBucketPolicy
52 | Version: 2012-10-17
53 | Statement:
54 | - Sid: PublicReadForGetBucketObjects
55 | Effect: Allow
56 | Principal:
57 | CanonicalUser: !GetAtt OriginAccessIdentity.S3CanonicalUserId
58 | Action: 's3:GetObject'
59 | Resource: !Join ['', [!GetAtt SiteBucket.Arn, '/*']]
60 | Bucket: !Ref SiteBucket
61 |
62 | CloudFrontDistribution:
63 | Type: AWS::CloudFront::Distribution
64 | DependsOn:
65 | - SiteBucket
66 | - OriginAccessIdentity
67 | Properties:
68 | DistributionConfig:
69 | Origins:
70 | - DomainName: !GetAtt SiteBucket.DomainName
71 | Id: S3Origin
72 | S3OriginConfig:
73 | OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${OriginAccessIdentity}
74 | Enabled: true
75 | HttpVersion: 'http2'
76 | DefaultRootObject: index.html
77 | Aliases:
78 | - !Ref SiteDomainName
79 | DefaultCacheBehavior:
80 | AllowedMethods:
81 | - GET
82 | - HEAD
83 | Compress: true
84 | TargetOriginId: S3Origin
85 | ForwardedValues:
86 | QueryString: true
87 | Cookies:
88 | Forward: none
89 | ViewerProtocolPolicy: redirect-to-https
90 | PriceClass: {{ site.price_class|default("PriceClass_All") }}
91 | ViewerCertificate:
92 | AcmCertificateArn: !Ref CertificateARN
93 | SslSupportMethod: sni-only
94 | {% if site.custom_error_responses is defined %}
95 | CustomErrorResponses:
96 | {% for error_response in site.custom_error_responses %}
97 | - ErrorCachingMinTTL: {{ error_response.caching_min_ttl|default(300) }}
98 | ErrorCode: {{ error_response.error_code }}
99 | ResponseCode: {{ error_response.response_code }}
100 | ResponsePagePath: "{{ error_response.page_path }}"
101 | {% endfor %}
102 | {% endif %}
103 |
104 | CloudFrontDomainRecord:
105 | Type: AWS::Route53::RecordSetGroup
106 | Properties:
107 | HostedZoneName: !Sub ${ZoneApex}.
108 | Comment: Zone apex alias.
109 | RecordSets:
110 | - Name: !Ref SiteDomainName
111 | Type: A
112 | AliasTarget:
113 | # Hard-coded per https://docs.aws.amazon.com/Route53/latest/APIReference/API_AliasTarget.html
114 | HostedZoneId: Z2FDTNDATAQYW2
115 | DNSName: !GetAtt CloudFrontDistribution.DomainName
116 |
117 | Outputs:
118 |
119 | CloudFrontDistributionId:
120 | Description: Cloudfront distribution id
121 | Value: !Ref CloudFrontDistribution
122 | Export:
123 | Name: !Sub ${AWS::StackName}-cloudfront-distribution-id
124 |
--------------------------------------------------------------------------------
/deployspec.yml:
--------------------------------------------------------------------------------
1 | version: 0.2
2 | env:
3 | variables:
4 | ENVIRONMENT: 'prod'
5 | phases:
6 | build:
7 | commands:
8 | - npm install && npm run build
9 | - echo Deployment started on `date`
10 | - aws s3 sync build/ s3://customembed.ellipsis.aws.arc.pub --exclude index.html --cache-control max-age=31536000
11 | - aws s3 cp build/index.html s3://customembed.ellipsis.aws.arc.pub/index.html --metadata-directive REPLACE --cache-control "max-age=60,s-maxage=31536000"
12 | post_build:
13 | commands:
14 | - DIST_ID=ETKFQ9SXRF4F9
15 | # $(aws cloudformation list-exports --region us-east-1 --output text --query 'Exports[?Name==`static-site-customembed-${ENVIRONMENT}-cloudfront-distribution-id`]' | cut -d$'\t' -f3)
16 | - echo DIST_ID=$DIST_ID
17 | - aws cloudfront create-invalidation --distribution-id $DIST_ID --paths "/*"
18 | - echo Deployment completed on `date`
19 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | # Composer Custom Embed: Getting Started
2 |
3 | ## Use Cases
4 |
5 | Custom Embeds is a feature for Arc Composer that allows developers to build content elements in stories that store an embedded link to a piece of content that lives outside Arc.
6 |
7 | ## Requirements
8 |
9 | To follow along with this guide, you will need:
10 |
11 | * Basic knowledge of HTML, Javascript, and the web
12 | * Basic knowledge of Arc Composer and [ANS](https://www.github.com/washingtonpost/ans-schema)
13 | * Basic experience with React components and content sources in PageBuilder Fusion
14 | * Permissions to administer and configure both Composer and PageBuilder Fusion in your Arc environment.
15 | * A working Fusion environment, complete with origin and public internet domain name. (I.e., you can see your fusion code running at www.mysite.com)
16 |
17 | ## Goal
18 |
19 | This guide will demonstrate how to build a "Movie" custom embed using [OMDB](https://www.omdbapi.com/).
20 |
21 | At the end of this guide, you will know how to:
22 | * Create a content source in Fusion that proxies to their external content
23 | * Create a search panel in Composer for users to find external content
24 | * Create a view panel in Composer for users to see external content embedded in their story
25 | * Create an edit panel in Composer for users to configure presentation options for the embed
26 | * Render a custom embed in Fusion so that readers can see the content inline
27 |
28 | ## Limitations and Warnings
29 |
30 | The Custom Embeds workflow is made up of "panels." Currently, these panels must reside on the *public internet*. This means that they are accessible by anyone in the world with internet access. Therefore we ask that you adhere to a few key restrictions:
31 |
32 | * The content exposed in these panels should only include published, public content. You should not search against unpublished content, data that is private to an organization or an individual, or content with legal restrictions placed on it.
33 | * None of these panels should write or modify the data they expose in the data's original source. The panels function only by sending and receiving data from Composer via the user's browser. All edited data should be saved within the Composer ANS document, and nowhere else.
34 |
35 | ## Steps
36 |
37 | ### 1. Identify or create endpoints for your external data.
38 |
39 | Your external data source should provide public GET endpoints as specified above for fetching and searching for content.
40 | These may already exist, or in some cases you may be able to create them yourself.
41 |
42 | This guide will use the [OMDB API](https://www.omdbapi.com/) as an example data source.
43 |
44 | Here's an example endpoint for fetching content by ID:
45 |
46 | * https://www.omdbapi.com/?apikey=YOUR_API_KEY_HERE&plot=full&i=tt3969158
47 |
48 | Here's an example endpoint for search for content by name:
49 |
50 | * https://www.omdbapi.com/?apikey=YOUR_API_KEY_HERE&s=Jurassic
51 |
52 | > **What about *my* content?**
53 | >
54 | > To implement an Arc Composer Custom Embed for your external content, your data source will need to:
55 | >
56 | > * ...expose an HTTP GET endpoint for fetching content metadata by id
57 | > * ...expose an HTTP GET endpoint for querying for content in your content source as a Composer user would
58 | > * ...**without** exposing confidential, unpublished or otherwise private content or data
59 | > * ...and **without** submitting new contnet or modifying content in the external data source
60 | >
61 | > These last two restrictions are in place because your endpoints will be indirectly exposed on the **public internet**,
62 | > so it important that they do not expose anything that is not already discoverable on the web.
63 |
64 |
65 |
66 | ### 2. Build a content source in PageBuilder Fusion
67 |
68 | We'll need two content sources set up in Fusion.
69 |
70 | The first content source will be used for rendering a single embed on the website as well as the view and edit panels. It will need to fetch a piece of content by id and return its metadata.
71 |
72 | Let's modify the existing movie-find content source from the Fusion recipes to accept an `id` parameter in addition to the `title` parameter.
73 |
74 | ```javascript
75 | /* /content/sources/movie-find.js */
76 |
77 | import { OMDB_API_KEY } from 'fusion:environment'
78 |
79 | const resolve = (query) => {
80 | const requestUri = `https://www.omdbapi.com/?apikey=${OMDB_API_KEY}&plot=full`
81 |
82 | if (query.hasOwnProperty('movieTitle')) {
83 | return `${requestUri}&t=${query.movieTitle}`
84 | } else if (query.hasOwnProperty('imdbID')) {
85 | return `${requestUri}&i=${query.imdbID}`
86 | }
87 |
88 | throw new Error('movie-find content source requires a movieTitle or imdbID')
89 | }
90 |
91 | export default {
92 | resolve,
93 | params: {
94 | movieTitle: 'text',
95 | imdbID: 'text'
96 | }
97 | }
98 | ```
99 |
100 | This will allow us to make client-side calls like the following in our Composer Custom Embed:
101 | `/pf/api/v3/content/fetch/movie-find?query={"imdbID":"tt0107290"}`
102 |
103 |
104 | We'll need one additional content source for our search panel to find a list of movies that match a certain title. Let's create a new content source `movie-search`:
105 |
106 | ```javascript
107 | /* /content/sources/movie-search.js */
108 |
109 | import { OMDB_API_KEY } from 'fusion:environment'
110 |
111 | const resolve = (query) => {
112 | const requestUri = `https://www.omdbapi.com/?apikey=${OMDB_API_KEY}&plot=full`
113 |
114 | let query_string = ''
115 |
116 | if (query.hasOwnProperty('text')) {
117 | query_string = `${requestUri}&s=${query.text}&type=movie`
118 | }
119 | else {
120 | throw new Error('movie-search content source requires text')
121 | }
122 |
123 | if (query.hasOwnProperty('year')) {
124 | query_string += `&y={query.year}`
125 | }
126 |
127 | return query_string
128 | }
129 |
130 | export default {
131 | resolve,
132 | params: {
133 | text: 'text',
134 | year: 'text'
135 | }
136 | }
137 |
138 | ```
139 |
140 |
141 |
142 |
143 | > **Why do this? Why not use the public endpoints?**
144 | >
145 | > * It allows our external content to leverage Fusion's object caching, preventing your external content from getting overwhelmed with requests.
146 | > * It prevents your API key from being exposed client-side.
147 | > * It allows you to do your entire rendering of the external content server-side, or client-side, as you'll soon see.
148 |
149 | ### 3. Create a search panel for your custom embed.
150 |
151 | We can get started by copying the [starter search code](https://raw.githubusercontent.com/washingtonpost/arc-custom-embed/master/public/starter/search.html). The best practice is to save static html files to /resources/plugins/composer/embeds/movie/search.html
152 |
153 | Note: It is required that all static html files to be saved in the `/resources/plugins/composer` directory. If saved outside of this folder, a deployment version parameter (d) will be requested, and will have to be updated with every deployment, making it unmaintainable.
154 |
155 | Unfortunately, this base embed doesn't do very much. We'll need to add functionality.
156 |
157 | First, let's define our search form. Our users only really want to find movies by name, so we'll stick to a single text field and a Search button.
158 |
159 | Edit the section under `` to be the following:
160 |
161 | ```html
162 |
163 |
164 | Movie Finder
165 |
166 |
167 | Let's go to the movies
168 |
169 |
170 |
171 |
174 |
175 | ```
176 |
177 | That's straightforward. But it still doesn't do very much. So let's go implement handleSearch().
178 |
179 | Here's where things get interesting. We can now call the content source we built in Step 2 to retrieve data for our search results. We can use the Fusion HTTP endpoint for fetching data from a content source. Let's check the data first to make sure we know what the data from our content source looks like. Hit this URL in your browser:
180 |
181 | http://localhost/pf/api/v3/content/fetch/movie-search?query={%22text%22:%22Jurassic%22}
182 |
183 | This gives us back data like:
184 |
185 | ```json
186 | {"Search":[{"Title":"Jurassic Park","Year":"1993","imdbID":"tt0107290","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMjM2MDgxMDg0Nl5BMl5BanBnXkFtZTgwNTM2OTM5NDE@._V1_SX300.jpg"},{"Title":"Jurassic World","Year":"2015","imdbID":"tt0369610","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BNzQ3OTY4NjAtNzM5OS00N2ZhLWJlOWUtYzYwZjNmOWRiMzcyXkEyXkFqcGdeQXVyMTMxODk2OTU@._V1_SX300.jpg"},{"Title":"The Lost World: Jurassic Park","Year":"1997","imdbID":"tt0119567","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMDFlMmM4Y2QtNDg1ZS00MWVlLTlmODgtZDdhYjY5YjdhN2M0XkEyXkFqcGdeQXVyNTI4MjkwNjA@._V1_SX300.jpg"},{"Title":"Jurassic Park III","Year":"2001","imdbID":"tt0163025","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BZDMyZGJjOGItYjJkZC00MDVlLWE0Y2YtZGIwMDExYWE3MGQ3XkEyXkFqcGdeQXVyNDYyMDk5MTU@._V1_SX300.jpg"},{"Title":"Jurassic World: Fallen Kingdom","Year":"2018","imdbID":"tt4881806","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BNzIxMjYwNDEwN15BMl5BanBnXkFtZTgwMzk5MDI3NTM@._V1_SX300.jpg"},{"Title":"Jurassic Shark","Year":"2012","imdbID":"tt2071491","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BODI1ODAyODgtZDYzZS00ZTM2LTg5MzMtZjNjMDFjMzlkZGQ2XkEyXkFqcGdeQXVyMTg0MTI3Mg@@._V1_SX300.jpg"},{"Title":"Jurassic City","Year":"2015","imdbID":"tt2905674","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMjM1MzUyMTk5MV5BMl5BanBnXkFtZTgwOTc2NzA0NDE@._V1_SX300.jpg"},{"Title":"The Jurassic Games","Year":"2018","imdbID":"tt6710826","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BZWJkMzE4ZTAtOTY1ZS00YmZjLWI2MzQtZTg4MzdiN2U4NmUyXkEyXkFqcGdeQXVyMTUwMzY1MDM@._V1_SX300.jpg"},{"Title":"Jurassic Prey","Year":"2015","imdbID":"tt3469284","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMTQ4MDg1NDkyNl5BMl5BanBnXkFtZTgwNTY3MTM2NDE@._V1_SX300.jpg"},{"Title":"The Making of 'Jurassic Park'","Year":"1995","imdbID":"tt0256908","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMjlhY2Y5NGYtZDdlMS00YzhhLWJhNzQtNWYzNTQzZDJjNGU2XkEyXkFqcGdeQXVyODY0NzcxNw@@._V1_SX300.jpg"}],"totalResults":"98","Response":"True","_id":"8e7343a11b4e2286a9ec0b93495aa3f3c618deeba43fbc45f0f92919f3853ade"}
187 | ```
188 |
189 | Alright, so we know what the data looks like and how to retrieve it. It's straightforward from there to write a client-side call to this endpoint.
190 |
191 | Edit the handleSearch function to be:
192 |
193 | ```javascript
194 |
195 | const handleSearch = () => ({
196 | // 1. Make an Ajax call to content source
197 | // 2. Set data based on response
198 | // 3. Re-render search results
199 | const searchTerm = document.getElementById('searchTitle').value;
200 |
201 | superagent
202 | .get('/pf/api/v3/content/fetch/movie-search')
203 | .query({ query: JSON.stringify({"text":searchTerm})})
204 | .set('Accept', 'application/json')
205 | .then(res => {
206 | data = res.body.Search
207 | render()
208 | });
209 | })
210 |
211 | ```
212 |
213 | This will make the Ajax call back to Fusion to retrieve our movie data. But we still need to show the results! So let's fill out that template and render function to include a movie image, id, title and year. (the .Poster, .imdbID, .Title and .Year fields, respectively, from our data source.)
214 |
215 | Set the search result template to be:
216 |
217 | ```html
218 |
219 |
220 |
221 |
222 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 | ```
237 |
238 | And we'll tweak the `render()` function just a bit to insert the data into this template:
239 |
240 | ```javascript
241 | const render = () => {
242 | // Show search results to user
243 | const template = document.getElementById('content_template').innerHTML
244 | document.getElementById('search_content').innerHTML = '';
245 |
246 | for (i = 0; i < data.length; i++) {
247 | const html = template
248 | .replace('%item_id%', 'row-' + data[i].imdbID)
249 | .replace('%image_id%', data[i].Poster)
250 | .replace('%text%', data[i].Title)
251 | .replace('%year%', data[i].Year)
252 | const element = document.createElement('div')
253 | document.getElementById('search_content').appendChild(element)
254 | element.outerHTML = html
255 | document
256 | .getElementById('row-' + data[i].imdbID)
257 | .addEventListener('click', handleClick(i))
258 | }
259 | }
260 | ```
261 |
262 | The search button shows results now. All that's left to do is let the user select one and send the data back to Composer.
263 |
264 | Change handleClick to be:
265 |
266 | ```javascript
267 | const handleClick = index => event => {
268 | // Send message back to Composer about selected item
269 | // message must contain:
270 | // {
271 | // "id": (content item id - string)
272 | // "url": (content source identifier - string)
273 | // "config": (contextual metadata - object)
274 | // }
275 |
276 | const ansCustomEmbed = {
277 | id: data[index]['imdbID'],
278 | url: 'https://www.imdb.com/title/',
279 | config: {
280 | "show_poster": true,
281 | "caption": "No caption specified"
282 | }
283 | }
284 |
285 | sendMessage('data', ansCustomEmbed)
286 | }
287 | ```
288 |
289 | The data format we are returning here is based part of the ans [custom embed element](https://github.com/washingtonpost/ans-schema/blob/master/src/main/resources/schema/ans/0.10.3/story_elements/custom_embed.json) schema.
290 |
291 | Finally, we can test our whole flow by using the online custom embed testing tool. Visit the testing tool, select the config options, and set the search page to be http://localhost/pf/resources/plugins/composer/embeds/movie/search.html.
292 |
293 |
294 | ### 4. Create an edit panel for your custom embed.
295 |
296 | The search panel is working, but we still need to let users attach some contextual metadata to an embed. For that, we need to define an edit panel.
297 |
298 | Copy [starter edit code](https://raw.githubusercontent.com/washingtonpost/arc-custom-embed/master/public/starter/edit.html) to /resources/plugins/composer/embeds/movie/edit.html
299 |
300 | Note: It is required that all static html files to be saved in the `/resources/plugins/composer` directory. If saved outside of this folder, a deployment version parameter (d) will be requested, and will have to be updated with every deployment, making it unmaintainable.
301 |
302 | For our movie embeds, we'll let users control two things: whether or not to display the movie poster image, and to set an optional caption or tagline about the movie.
303 |
304 | We can start by building a form to set those options.
305 |
306 | ```html
307 |
308 |
309 |
310 |
313 |
314 |
315 |
316 |
320 |
324 |
325 |
326 |
329 |
332 |
333 |
334 | ```
335 |
336 | We'll also need to show a rendering of the content with the configuration options factored in. This requires fetching the data and rendering it alongside the configuration options.
337 |
338 | We can update `fetchData` to retrieve the content based on the content id passed in via query parameters. Note that this time we're pulling from the movie-find content source instead of movie-search.
339 |
340 | ```javascript
341 | // Retrieve the content data based on parameters
342 | const fetchData = (ansCustomEmbed) => {
343 | superagent
344 | .get('/pf/api/v3/content/fetch/movie-find')
345 | .query({ query: JSON.stringify({"imdbID":ansCustomEmbed.id})})
346 | .set('Accept', 'application/json')
347 | .then(res => {
348 | content = res.body
349 | render(content, data.config)
350 | });
351 | }
352 | ```
353 |
354 | That render function doesn't do anything, so let's make a template and use it.
355 |
356 | Edit the content template to be:
357 |
358 | ```html
359 |
360 |
361 |
363 |
370 |
371 |
372 | %text%
373 |
374 |
375 | %year%
376 |
377 |
378 | "%caption%"
379 |
380 |
381 |
382 |
383 |
384 |
JSON Response to Composer
385 |
%data%
386 |
387 |
388 | ```
389 |
390 | And pull it all together in `render`:
391 |
392 | ```javascript
393 | // Render the content data and contextual configuration together
394 | const render = (content, config) => {
395 |
396 | // Setup Element Preview
397 | const template = document.getElementById('content_template').innerHTML
398 | const html = template
399 | .replace('%item_id%', 'row-' + content.imdbID)
400 | .replace('%image_id%', content.Poster)
401 | .replace('%text%', content.Title)
402 | .replace('%year%', content.Year)
403 | .replace('%caption%', (config.caption ? config.caption : ""))
404 | .replace('%data%', JSON.stringify(data, null, 2))
405 |
406 | const element = document.createElement('div')
407 | document.getElementById('content_holder').innerHTML = ''
408 | document.getElementById('content_holder').appendChild(element)
409 | element.outerHTML = html
410 |
411 | if (config.show_poster && config.show_poster === false) {
412 | document.getElementById('content_card').removeChild(
413 | document.getElementById('content_image')
414 | )
415 | }
416 |
417 | // Update form state
418 | document.getElementById('poster_yes').checked = (!!config.show_poster)
419 | document.getElementById('poster_no').checked = (!config.show_poster)
420 |
421 | document.getElementById('caption').value = (config.caption ? config.caption : "")
422 | }
423 |
424 | ```
425 |
426 | Finally, we need to make the Apply Changes button send the data back to Composer. It should read the form state, update the config object and send the whole data package back.
427 |
428 |
429 | ```javascript
430 | // Update config based on form changes, submit back to Composer and re-render
431 | const applyChanges = () => {
432 | data.config.show_poster = document.getElementById('poster_yes').checked
433 | data.config.caption = document.getElementById('caption').value
434 |
435 | // Update Composer and re-render form
436 | sendMessage('data', data)
437 | render(content, data.config)
438 | }
439 |
440 | ```
441 |
442 |
443 | That's it for editing! We can test it using the same test tool we used for search.
444 |
445 |
446 | ### 5. Create a view panel for your custom embed.
447 |
448 | Phew! The hardest part is over. The search and edit panels are working. But we still need to tell Composer how to display the embed to writers, editors and content producers when it's embedded in a document. The view panel controls how the embed displays when it's at rest.
449 |
450 | Copy the [starter view code](https://raw.githubusercontent.com/washingtonpost/arc-custom-embed/master/public/starter/view.html) to /resources/plugins/composer/embeds/movie/view.html
451 |
452 | Note: It is required that all static html files to be saved in the `/resources/plugins/composer` directory. If saved outside of this folder, a deployment version parameter (d) will be requested, and will have to be updated with every deployment, making it unmaintainable.
453 |
454 | This one's a little easier -- a lot can be borrowed from the edit panel. Instead of a form, we just need to tell our view panel how to fetch and render content by id. We'll use the movie-find content source again, along with the config object passed in.
455 |
456 | Fetch data looks the same as in edit panel:
457 |
458 | ```javascript
459 | const fetchData = (ansCustomEmbed) => {
460 | superagent
461 | .get('/pf/api/v3/content/fetch/movie-find')
462 | .query({ query: JSON.stringify({"imdbID":ansCustomEmbed.id})})
463 | .set('Accept', 'application/json')
464 | .then(res => {
465 | data = res.body
466 | render(data, ansCustomEmbed.config)
467 | });
468 | }
469 | ```
470 |
471 | And the only remaining steps are adding the template and render function.
472 |
473 | ```html
474 |
475 |
478 |
485 |
486 |
487 | %text%
488 |
489 |
490 | %year%
491 |
492 |
493 | "%caption%"
494 |
495 |
496 |
497 | ```
498 |
499 | ```javascript
500 | const render = (data, config) => {
501 | const template = document.getElementById('content_template').innerHTML
502 | const html = template
503 | .replace('%item_id%', 'row-' + data.imdbID)
504 | .replace('%image_id%', data.Poster)
505 | .replace('%text%', data.Title)
506 | .replace('%data%', JSON.stringify(data, null, 2))
507 | .replace('%year%', data.Year)
508 | .replace('%caption%', (config.caption ? config.caption : ""))
509 |
510 | const element = document.createElement('div')
511 | document.getElementById('search_content').appendChild(element)
512 | element.outerHTML = html
513 |
514 | if (config && (config.show_poster === false)) {
515 | document.getElementById('content_card').removeChild(
516 | document.getElementById('content_image')
517 | )
518 | }
519 | }
520 |
521 | ```
522 |
523 | Once again, we can test using the test tool.
524 |
525 |
526 | ### 6. Deploy code to a development environment.
527 |
528 | To wire these panels up in Composer, you'll need to host them on a public domain. Just zip up a bundle like you usually would and deploy to a test environment.
529 |
530 | Once they're uploaded, you can wire up Composer to use the panels in the settings page. See [these instructions](https://redirector.arcpublishing.com/alc/arc-products/composer/user-docs/composer-custom-embed-power-ups-for-rich-third-party-content/#Configuring-a-Custom-Embed-Integration).
531 |
532 | Save a few documents with the embed in them, then go look at those documents published on the web.
533 |
534 | ### 7. Create an appropriate feature component in PageBuilder Fusion.
535 |
536 | Wait...there's nothing there? Ah, geeze, we forgot to write a Fusion feature for actually rendering these things!
537 |
538 | Fortunately it's not too hard to add. We already have the right content sources configured, after all.
539 |
540 | Let's update our movie-detail component (from the Fusion recipes) to utilize our content source and configuration options.
541 |
542 | ```javascript
543 | @Consumer
544 | class MovieDetail extends Component {
545 | constructor (props) {
546 | super(props)
547 | this.state = {
548 | movie: {}
549 | }
550 | this.fetch = this.fetch.bind(this)
551 | this.fetch()
552 | }
553 |
554 | fetch() {
555 | const { movie } = this.state
556 | const { imdbID, caption, show_poster } = this.props
557 | this.fetchContent({
558 | movie: {
559 | source: 'movie-find',
560 | query: { imdbID: imdbID },
561 | transform: (data) => {
562 | return Object.assign(
563 | {},
564 | data,
565 | {
566 | Poster: show_poster ? data.Poster : null,
567 | caption: caption
568 | }
569 | )
570 | }
571 | }
572 | })
573 | this.render()
574 | }
575 |
576 | render () {
577 |
578 | const { Actors, Director, Plot, Poster, Rated, Title, Writer, Year, caption } = this.state.movie || {}
579 |
580 | return (
581 |
582 |
583 | {Poster && Title &&
}
584 |
585 |
589 | {Title &&
{Title}
}
590 | {Year &&
Year: {Year}
}
591 | {caption &&
"{caption}"
}
592 |
593 |
594 |
595 |
596 | )
597 | }
598 | }
599 |
600 | MovieDetail.label = 'Movie Detail'
601 |
602 | export default MovieDetail
603 |
604 | ```
605 |
606 | A few things have changed here. Our component now takes `imdbID`, `caption` and `show_poster` as props. It no longer uses the global content source. The `caption` and `show_poster` fields are factored into the render function. But most importantly, `imdbID` is used as an argument to our content source. This is pretty cool!
607 |
608 |
609 | We're not quite done, though. We'll need to tell our feature pack that custom embeds are a valid content element to appear in an article body. So let's find our article body feature (the location and code varies by project) and add this to our content element switch statement:
610 |
611 | ```javascript
612 | case 'custom_embed':
613 | return
614 | ```
615 |
616 | We'll also need to import the component at the top, perhaps like:
617 |
618 | ```
619 | import CustomEmbedBody from './_children/custom-embed'
620 | ```
621 |
622 | And we'll need to implement the custom embed component as well.
623 |
624 | ```javascript
625 | 'use strict'
626 |
627 | /* Third party libs */
628 | import React, { Component } from 'react'
629 |
630 | /* Components */
631 | import MovieDetail from '../../../movies/movie-detail'
632 |
633 | /* Other JS */
634 |
635 | /* Non-JS resources */
636 |
637 |
638 | class CustomEmbedBody extends Component {
639 |
640 | render() {
641 |
642 | return (
643 |
644 |
649 |
650 |
651 | )
652 | }
653 | }
654 |
655 |
656 | export default CustomEmbedBody
657 | ```
658 |
659 | This component is pretty basic for now, but it would be a good place in the future to `switch` between multiple custom embed types, once we have them. In the meantime, it extracts the relevant fields from our custom embed and passes them into the updated movie detail component.
660 |
661 | And voila! Our movies appear inline in our articles!
662 |
663 |
664 | ### 9. Deploy the whole thing to sandbox and production, and enable the configs.
665 |
666 | One benefit of implementing the custom embed panels in Fusion is that they become part of our deployment bundle! So you can change the Composer config options, the content source, and the reader rendering all at once, without any gap time.
667 |
668 | Just zip up the bundle and deploy to each environment as you normally would. Remember that the first time you deploy to each environment, you'll also need to enable the appropriate Ellipsis configs.
669 |
670 | That's it for now. We can't wait to see what you do with this!
671 |
--------------------------------------------------------------------------------
/docs/reference.md:
--------------------------------------------------------------------------------
1 | # Custom Embed Specification
2 |
3 | All communication goes through browser postMessage API. There are 3 types of plugin:
4 |
5 | - Search Plugin
6 | - View Plugin
7 | - Edit Plugin
8 |
9 | ## Search integration with Composer
10 |
11 | When user add a Custom Embed element into Composer, Search Modal should appear. Depending on which Custom Embed subtype being added, a correspondent searchApi should be used to load search user experience.
12 | Search integration may receive any arbitrary data through URL. However, Custom Embed subtype is the only piece of information required.
13 |
14 | ## View integration with Composer
15 |
16 | Composer may require display Custom Embed type from the editor itself, related content or featured media panel. In order to do that Composer will create an iframe and provide iframe src with the link to the preconfigured viewApi endpoint. Composer will make any necessary substitution to the viewApi URL from ANS to make proper URL.
17 | note: View integration should not have Edit controls. Composer should initiate editing.
18 |
19 | ## Edit integration with Composer
20 |
21 | Pretty much the same as view integration, except Composer will be waiting for the submit or cancel message from the iframe content. Submit message should have well-formed Custom Embed ANS in it with all the updates. Edit integration may have a cancel button along with Submit button for the better UI. However, Composer may cancel editing at any time just by removing that iframe. No message will be sent to the Edit integration.
22 | note: Integration developers encouraged not to save any data in the underlying systems at all, since all information should be carried over by Custom Embed ANS. If Edit integration designed to save some of the data to the underlying system, it should not assume that Composer will inform integration in any way if user cancelled editing.
23 |
24 | ## Iframe communication protocol outline
25 |
26 | Search, view and edit integration should send a handshake postMessage to the parent window as soon as it is loaded and ready to receive commands or interact with the user. If Composer does not receive initial handshake message, Composer will display an error information with the Retry button.
27 |
28 | Search integration expected to return a configuration JSON on success search. Configuration JSON is a subject of validation, see below.
29 |
30 | View integration is expected to send only handshake message. View integration receives configuration in a form of a query string. (base64/encoded, tbd)
31 |
32 | Edit integration receives configuration in a form of a query string. (base64/encoded, tbd). Edit integration is expected to send handshake message and the configuration JSON on changes submit. If user discard changes, a cancel message should be send. Configuration JSON is a subject of validation, see below.
33 |
34 | Each integration should be supplied with a `k` query string argument. This key is suppose to be used as simple cookie value and returned back with any message in the `key` field. This helps Composer to identify which integration responds.
35 |
36 | # Validation
37 |
38 | Custom Embed configuration has the following limitations:
39 |
40 | - Configuration should be a valid JSON
41 | - JSON size should be no more than 2048 bytes length (TBD)
42 | - JSON should not have type, version or referent fields at any level. Composer will strip that out
43 |
44 | Iframe communication protocol specification
45 |
46 | Composer communicate with plugins though query string data. This data is arbitrary and servers a purpose of plugin configuration. Custom embed data is passed as URL encoded data in a p parameter in a form of JSON.
47 |
48 | Example:
49 |
50 | editApi.html?k=j0a&p=%7B%22id%22%3A%2217b3224337d2d3%22%2C%22url%22%3A%22https%3A%2F%2Fmy.content.com%2Fdata%2F5%22%2C%22config%22%3A%7B%22id%22%3A5%2C%22text%22%3A%22Brunch%20raclette%20vexillologist%20post-ironic%20glossier%20ennui%20XOXO%20mlkshk%20godard%20pour-over%20blog%20tumblr%20humblebrag.%22%2C%22image_id%22%3A30%7D%7D
51 |
52 | Plugins should response back through browser postMessage mechanism. Each message should be a JSON object and contain the following fields:
53 |
54 | - source - always equal to custom_embed
55 | - action - can be ready, data, cancel
56 | - key - a value from `k` query string parameter. Should be passed as-is
57 | - data - contain custom embed data or content height of the iframe
58 |
59 | ## Ready message
60 |
61 | Ready message should be send by a plugins as soon as it renders their content and content height is known.
62 |
63 | {"source":"custom_embed","action":"ready","key":"j0a","data":{"height":908}}
64 |
65 | If ready message will not be sent within ~10 seconds, Composer will render a timeout error and plugin content will be discarded.
66 |
67 | ### Request additional Story ANS
68 |
69 | Customers can obtain addition ANS data from Composer via `postMessage`. It can be done with **Search**, **View**, and **Edit** integration. Composer will send back the data with the additional fields of:
70 |
71 | 1. Headlines
72 | 2. Subheadlines
73 | 3. Taxonomy
74 |
75 | To do this, add an additional flag `isAnsRequired` set to `true` in your integration where `postMessage()` is being called and when `action` is `'ready'`:
76 | ```javascript
77 | const sendMessage = function(action, data) {
78 | const messagePayload = {
79 | ...,
80 | action,
81 | data,
82 | }
83 | // Composer only accepts `isAnsRequired` flag when `action = 'ready'`
84 | if (action === 'ready') {
85 | messagePayload.isAnsRequired = false // set to `true`
86 | }
87 | window.parent.postMessage(JSON.stringify(messagePayload), '*')
88 | }
89 |
90 | sendMessage('action', data)
91 | ```
92 | Which will send out a message to Composer:
93 | ```javascript
94 | {source: "custom_embed", action: "ready", data: {…}, key: "1234567890", isAnsRequired: true}
95 | ```
96 | Composer will then `postMessage` back with additional Story ANS data:
97 | ```javascript
98 | MessageEvent
99 | {
100 | "message":"ans_data",
101 | "data":{
102 | "headlines":{
103 | "basic":"Test Headline",
104 | "mobile":"",
105 | "native":"",
106 | ...
107 | },
108 | "subheadlines":{ "basic":"Test Sub Headline"},
109 | "taxonomy":{
110 | "sites":[{"_id":"/about-us", "type":"site", ...}],
111 | "tags":[],
112 | "sections":[],
113 | ...
114 | }
115 | }
116 | }
117 | ```
118 | To test if the message is received properly, use the following example:
119 | ```javascript
120 | window.addEventListener('message', (e) => {
121 | console.log(e)
122 | }, false)
123 | ```
124 |
125 | For reference, there is an example already set up in [audioSearchApi](../public/audioSearchApi.html#L234-L245).
126 |
127 | ## Data message
128 |
129 | Data message should be send by search plugin when an item has been selected by user or edit plugin when editing is done. Data message should contain an embed data structure. Feel free to check the schema.
130 |
131 | {
132 | "source":"custom_embed",
133 | "action":"data",
134 | "key":"j0a",
135 | "data":{
136 | "id":"b0bc95dc11919",
137 | "url":"https://my.content.com/data/2",
138 | "config":{
139 | "id":2,
140 | "text":"Some Text",
141 | "image_id":1
142 | }
143 | }
144 | }
145 |
146 | A data field should contain id, url and config fields. Those are required. Config object might have any arbitrary data structure. The only requirement is that it should not have referent, type and version fields. Config object should have as few fields as possible to properly configure the custom embed object. Please do not put large objects here. Instead, use data.id to identify internal resource and data.config to store configuration properties only.
147 |
148 | ## Cancel message
149 |
150 | Cancel message is used to notify Composer that user wants to cancel search or discard any editing changes.
151 |
152 | {"source":"custom_embed","action":"cancel","key":"j0a"}
153 |
154 | Cancel message notify Composer to close the UI. It does nothing for the view integration and should not be used there.
155 |
156 | Composer can close search or edit iframe by itself without notifying iframe content. Please consider this behavior and do not persist any changes in the system. The only proper way to persist changes is to send data through data message back to the ellipsis.
157 |
158 | ## Message communication example
159 |
160 | | Composer | Plugin |
161 | | ---------------------------------- | --------------------------------------------------------------------------------------------- |
162 | | Composer loads Search Integration | |
163 | | | Integration loads and send back Ready message |
164 | | | ← {"source":"custom_embed","action":"ready","key":"j0a","data":{"height":908}} |
165 | | | User select necessary media |
166 | | | ← {"source":"custom_embed","action":"data","key":"j0a","data":{…}} |
167 | | Composer loads View Integration | |
168 | | viewApi.html?p=…. | View integration renders it’s content and send back ready message with the content height. |
169 | | | ← {"source":"custom_embed","action":"ready","key":"j0a","data":{"height":480}} |
170 | | Composer loads Edit Integration | |
171 | | editApi.html?p=…. | Edit integration renders it’s content and send back ready message with the content height. |
172 | | | ← {"source":"custom_embed","action":"ready","key":"j0a","data":{"height":890}} |
173 | | | User accepted changes |
174 | | | ← {"source":"custom_embed","action":"data","key":"j0a","data":{…}} |
175 | | | /or/ User cancelled changes |
176 | | | ← {"source":"custom_embed","action":"cancel","key":"j0a"} |
177 | | Composer Sends Story ANS | |
178 | | | User requesting additional Story ANS |
179 | | | ← {"source":"custom_embed","action":"ready",data: {…}, key: "j0a", isAnsRequired: true} |
180 |
--------------------------------------------------------------------------------
/docs/testing.md:
--------------------------------------------------------------------------------
1 | # Custom Embed Test Application
2 |
3 | > This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.
15 | You will also see any lint errors in the console.
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "arc-custom-embed",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/jest": "^24.0.11",
7 | "@types/node": "^11.13.0",
8 | "@types/react": "^16.8.11",
9 | "@types/react-dom": "^16.8.3",
10 | "react": "^16.8.6",
11 | "react-dom": "^16.8.6",
12 | "react-scripts": "2.1.8",
13 | "typescript": "^3.4.1"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "eslintConfig": {
22 | "extends": "react-app"
23 | },
24 | "browserslist": [
25 | ">0.2%",
26 | "not dead",
27 | "not ie <= 11",
28 | "not op_mini all"
29 | ],
30 | "devDependencies": {
31 | "@typescript-eslint/eslint-plugin": "^1.6.0",
32 | "eslint-config-prettier": "^4.1.0",
33 | "eslint-plugin-prettier": "^3.0.1",
34 | "eslint-plugin-react": "^7.12.4",
35 | "eslint-plugin-react-hooks": "^1.6.0",
36 | "eslint-plugin-standard": "^4.0.0",
37 | "prettier": "^1.16.4"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/public/audio/audio0.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/washingtonpost/arc-custom-embed/f33df0813b194b0139098003d7453c37d69fb54f/public/audio/audio0.mp3
--------------------------------------------------------------------------------
/public/audio/audio1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/washingtonpost/arc-custom-embed/f33df0813b194b0139098003d7453c37d69fb54f/public/audio/audio1.mp3
--------------------------------------------------------------------------------
/public/audio/audio2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/washingtonpost/arc-custom-embed/f33df0813b194b0139098003d7453c37d69fb54f/public/audio/audio2.mp3
--------------------------------------------------------------------------------
/public/audio/audio3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/washingtonpost/arc-custom-embed/f33df0813b194b0139098003d7453c37d69fb54f/public/audio/audio3.mp3
--------------------------------------------------------------------------------
/public/audio/cover0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/washingtonpost/arc-custom-embed/f33df0813b194b0139098003d7453c37d69fb54f/public/audio/cover0.png
--------------------------------------------------------------------------------
/public/audio/cover1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/washingtonpost/arc-custom-embed/f33df0813b194b0139098003d7453c37d69fb54f/public/audio/cover1.jpg
--------------------------------------------------------------------------------
/public/audio/cover2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/washingtonpost/arc-custom-embed/f33df0813b194b0139098003d7453c37d69fb54f/public/audio/cover2.jpg
--------------------------------------------------------------------------------
/public/audio/cover3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/washingtonpost/arc-custom-embed/f33df0813b194b0139098003d7453c37d69fb54f/public/audio/cover3.jpg
--------------------------------------------------------------------------------
/public/audioEditApi.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Audio Edit API plugin
5 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
26 |
27 |
28 |
29 |
76 | Search implementation have to provide a UI for the user to search
77 | and select required media. This reference implementation does not
78 | have search logic, however, user might click on any tile below to
79 | emulate search result accepted.
80 |
81 |
82 | Click on any tile below to emulate selection has been accepted
83 |