├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── babel.config.js ├── lib ├── base │ └── schema │ │ └── objects │ │ ├── vimeoOEmbedConfigData.js │ │ ├── vimeoOEmbedData.js │ │ └── vimeoVideo.js └── components │ └── VimeoInput │ ├── components │ ├── ConfigFieldsInput │ │ ├── components │ │ │ ├── Switch.js │ │ │ └── TextInput.js │ │ ├── constants.js │ │ └── index.js │ └── Label │ │ ├── components │ │ ├── Description.js │ │ ├── Label.js │ │ └── Title.js │ │ └── index.js │ ├── index.js │ └── styles │ └── VimeoInput.css ├── package-lock.json ├── package.json └── sanity.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime Data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Dependency Directories 13 | node_modules/ 14 | 15 | # Optional NPM Cache Directory 16 | .npm 17 | 18 | # Output of 'npm pack' 19 | *.tgz 20 | 21 | # Deployment Directories 22 | dist/ 23 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Bradley Griffith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sanity Plugin Goth Vimeo Input 2 | 3 | > ???? `sanity-plugin-vimeo-input` was taken so it's named `sanity-plugin-goth-vimeo-input`. 4 | 5 | A Sanity Plugin for Inputting Vimeo Videos by their URL and Pre-Loading oEmbed Data. 6 | 7 | Through this plugin, your users will simply be required to input a valid Vimeo video URL. The plugin will use the [Vimeo oEmbed API](https://developer.vimeo.com/api/oembed) to then verify the video and import the full oEmbed payload for the video, which includes the video's title, ID, iFrame embed code, and thumbnail URL. 8 | 9 | Additionally, the plugin allows the developer to set both defaults and available configuration options for the user to further customize the player included in the oEmbed response (e.g.; setting the video to autoplay or to use custom player control colors). 10 | 11 | ![Plugin Example](https://github.com/bradley/sanity-plugin-vimeo-input/blob/assets/images/simple-vimeo-input.gif) 12 | 13 | ## Install 14 | 15 | While in your Sanity project directory, run the following command: 16 | 17 | ``` 18 | sanity install goth-vimeo-input 19 | ``` 20 | 21 | You can read more about Sanity Plugin usage in the [official guide](https://www.sanity.io/docs/plugins). 22 | 23 | 24 | ## Usage 25 | 26 | #### Basic 27 | 28 | To have your user simply input a Vimeo URL without additional configuration, you may include the `type`, `vimeoVideo`, in your schema without additional options. 29 | 30 | ```javascript 31 | export default { 32 | name: "video", 33 | title: "Video", 34 | type: "document", 35 | fields: [ 36 | { 37 | name: "vimeoVideo", 38 | title: "Vimeo Video", 39 | type: "vimeoVideo", 40 | validation: Rule => Rule.required() 41 | }, 42 | ], 43 | preview: { 44 | select: { 45 | vimeoVideo: "vimeoVideo" 46 | }, 47 | prepare(selection) { 48 | let oEmbedData = selection.vimeoVideo 49 | ? selection.vimeoVideo.oEmbedData 50 | : {}; 51 | 52 | return { 53 | title: oEmbedData.title || "" 54 | } 55 | } 56 | } 57 | }; 58 | ``` 59 | 60 | See: https://vimeo.com/475247102 61 | 62 | #### With Configuration 63 | 64 | To include additional configuration, you may utilize either or both of the options provided with this plugin: `configurableFields` and `defaultFieldValues`. The `configurableFields` option tells the plugin which of the [Vimeo oEmbed Arguments](https://developer.vimeo.com/api/oembed/videos) to allow your user to set when loading the video, and the `defaultFieldValues` option tells the plugin the default values for any such arguments (whether configurable by the user or not). 65 | 66 | For example, the follow example will expose controls for toggling `autoplay` and `controls` on the player, and set default values for several other important oEmbed arugments: 67 | 68 | ```javascript 69 | export default { 70 | name: "video", 71 | title: "Video", 72 | type: "document", 73 | fields: [ 74 | { 75 | name: "vimeoVideo", 76 | title: "Vimeo Video", 77 | type: "vimeoVideo", 78 | options: { 79 | configurableFields: [ 80 | "autoplay", 81 | "controls" 82 | ], 83 | defaultFieldValues: { 84 | autopause: false, 85 | autoplay: true, 86 | background: false, 87 | byline: false, 88 | controls: false, 89 | dnt: true, 90 | loop: true, 91 | muted: true, 92 | portrait: false, 93 | quality: "auto", 94 | title: false, 95 | // Load a reasonably large thumbnail up front. Note this will add 96 | // relevant width/height attributes to the iframe. If youre making 97 | // your player responsive on the frontend this will be ok, but keep 98 | // it in mind. 99 | width: "960" 100 | } 101 | }, 102 | validation: Rule => Rule.required() 103 | }, 104 | ], 105 | preview: { 106 | select: { 107 | vimeoVideo: "vimeoVideo" 108 | }, 109 | prepare(selection) { 110 | let oEmbedData = selection.vimeoVideo 111 | ? selection.vimeoVideo.oEmbedData 112 | : {}; 113 | 114 | return { 115 | title: oEmbedData.title || "" 116 | } 117 | } 118 | } 119 | }; 120 | ``` 121 | 122 | See: https://vimeo.com/475247026 123 | 124 | ## Additional Cases 125 | 126 | #### User Updates to Configuration 127 | > :warning: **Important** 128 | 129 | The user will be asked to reload the video any time they make changes to configurable fields. This is because configuration options are used during the _request_ to Vimeo for oEmbed data, and as such updated configurations require an updated request to Vimeo. The plugin will alert the user to this automatically. 130 | 131 | See: https://vimeo.com/475246939 132 | 133 | #### Non-Existent Vimeo URLs / Fail Case 134 | Should the user ever attempt to load a non-existent or errant Vimeo URL, the plugin will alert the user automatically. 135 | 136 | See: https://vimeo.com/475247012 137 | 138 | ## Configuration 139 | 140 | All configurable fields exposed through the `configurableFields` option match the configuration arguments available for use with [Vimeo's oEmbed API](https://developer.vimeo.com/api/oembed/videos). To expose a field for user configuration, simply add its name to the `configurableFields` array within the `options` object when adding the `vimeoVideo` type to your schema (see "Usage" section above). Further, each configurable field exposed to the user will include the description from the matching argument in Vimeo's API. To override any default value, use `defaultFieldValues` (see "Usage" section above). 141 | 142 | This is taken directly from Vimeo's documentation: 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 |
ArgumentDefault ValueDescription
urlNoneThe URL of the video on Vimeo. This is a required value.
apitrueWhether to enable the Vimeo player SDK.
autopausetrueWhether to pause the current video when another Vimeo video on the same page starts to play.
autoplayfalseWhether to start playback of the video automatically. This feature might not work on all devices.
backgroundfalseFor videos on a Vimeo Plus account or higher: whether to hide all video controls, loop the video automatically, enable autoplay, and mute the video. The loop and autoplay behaviors can't be overridden, but the mute behavior can be; see the muted argument below.
bylinetrueWhether to display the video owner's name.
callbackNoneThe name of JavaScript function to use as the callback parameter of a JSONP call. The indicated function wraps the JSON response.
controlstrueWhether to display (true) or hide (false) all interactive elements in the player interface. To start video playback when controls are hidden, set autoplay to true or use our player API. This argument is available only for Vimeo Pro and Business accounts.
colorNoneThe hexadecimal color value of the video controls, which is normally 00ADEF.
dntfalseWhether to prevent the player from tracking session data, including cookies. Keep in mind that setting this argument to true also blocks video stats.
funtrueWhether to disable informal error messages in the player, such as Oops.
heightNoneThe height of the video in pixels.
loopfalseWhether to restart the video automatically after reaching the end.
maxheightNoneThe height of the video in pixels, where the video won't exceed its native height, no matter the value of this field.
maxwidthNoneThe width of the video in pixels, where the video won't exceed its native width, no matter the value of this field.
mutedfalseWhether to mute playback by default. The user can increase the volume manually.
player_idNoneThe unique ID for the player, which comes back with all JavaScript API responses.
playsinlinetrueWhether the video plays inline on supported mobile devices.
portraittrueWhether to display the video owner's portrait.
qualityautoFor videos on a Vimeo Plus account or higher: the playback quality of the video. Use auto for the best possible quality given available bandwidth and other factors. You can also specify 360p, 540p, 720p, 1080p, 2k, and 4k.
responsivefalseWhether to return a responsive embed code, or one that provides intelligent adjustments based on viewing conditions. We recommend this option for mobile-optimized sites.
speedfalseFor videos on a Vimeo Plus account or higher: whether to include playback speed among the player preferences.
texttrackNoneThe text track to display with the video. Specify the text track by its language code (en), the language code and locale (en-US), or the language code and kind (en.captions). For this argument to work, the video must already have a text track of the given type; see our Help Center or Working with Text Track Uploads for more information.
titletrueWhether to display the video's title.
transparenttrueWhether the background of the player area is transparent on Vimeo. When this value is false, the background of the player area is black. Depending on the video's aspect ratio, there might be visible black bars around the video.
widthNoneThe width of the video in pixels.
xhtmlfalseWhether to make the embed code XHTML-compliant.
178 | 179 | 180 | ## Payload 181 | Once loaded, a field of type `vimeoVideo` will include the following. 182 | 183 | #### `url` 184 | The Vimeo video [URL](https://www.sanity.io/docs/url-type) input by the user. 185 | 186 | #### `oEmbedDataLastFetchedAt` 187 | The last [date](https://www.sanity.io/docs/date-type) at which the user updated the oEmbed data for the video by clicking "load". 188 | 189 | #### `oEmbedDataJsonResponse` 190 | The raw JSON response, encoded as a string, returned by the Vimeo oEmbed API. 191 | 192 | #### `vimeoOEmbedData` 193 | All fields returned by the Vimeo oEmbed API, as individual Sanity fields within a sub-object on the sanity `vimeoVideo` object. See the source code [here](https://github.com/bradley/sanity-plugin-vimeo-input/blob/main/lib/base/schema/objects/vimeoOEmbedData.js) and "oEmbed response fields" [here](https://developer.vimeo.com/api/oembed/videos). 194 | 195 | #### `vimeoOEmbedConfigData` 196 | All configuration used by the plugin when the oEmbed data was last updated, as individual Sanity fields within a sub-object on the sanity `vimeoVideo` object. See the source code [here](https://github.com/bradley/sanity-plugin-vimeo-input/blob/main/lib/base/schema/objects/vimeoOEmbedConfigData.js). 197 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | 4 | const config = { 5 | plugins: [ 6 | [ 7 | "babel-plugin-module-resolver", 8 | { 9 | "root": ["./"], 10 | "alias": { 11 | "@sanity-plugin-vimeo-input/Base": "./lib/base", 12 | "@sanity-plugin-vimeo-input/Components": "./lib/components", 13 | } 14 | } 15 | ], 16 | [ 17 | "transform-imports", { 18 | "react-router": { 19 | "transform": "react-router/${member}", 20 | "preventFullImport": true 21 | } 22 | } 23 | ], 24 | "lodash", 25 | "transform-react-remove-prop-types" 26 | ], 27 | presets: [ 28 | [ 29 | "@babel/preset-env", { 30 | // Browsers with usage more than 1% global usage. 31 | "targets": "> 1%" 32 | } 33 | ], 34 | "@babel/preset-react" 35 | ] 36 | }; 37 | 38 | return config; 39 | } 40 | -------------------------------------------------------------------------------- /lib/base/schema/objects/vimeoOEmbedConfigData.js: -------------------------------------------------------------------------------- 1 | const vimeoOEmbedConfigData = { 2 | title: "Vimeo oEmbed Configuration Data", 3 | name: "vimeoOEmbedConfigData", 4 | type: "object", 5 | fields: [ 6 | { 7 | title: "api", 8 | name: "api", 9 | type: "boolean" 10 | }, 11 | { 12 | title: "autopause", 13 | name: "autopause", 14 | type: "boolean" 15 | }, 16 | { 17 | title: "autoplay", 18 | name: "autoplay", 19 | type: "boolean" 20 | }, 21 | { 22 | title: "background", 23 | name: "background", 24 | type: "boolean" 25 | }, 26 | { 27 | title: "byline", 28 | name: "byline", 29 | type: "boolean" 30 | }, 31 | { 32 | title: "callback", 33 | name: "callback", 34 | type: "string" 35 | }, 36 | { 37 | title: "controls", 38 | name: "controls", 39 | type: "boolean" 40 | }, 41 | { 42 | title: "color", 43 | name: "color", 44 | type: "string" 45 | }, 46 | { 47 | title: "dnt", 48 | name: "dnt", 49 | type: "boolean" 50 | }, 51 | { 52 | title: "fun", 53 | name: "fun", 54 | type: "boolean" 55 | }, 56 | { 57 | title: "height", 58 | name: "height", 59 | type: "string" 60 | }, 61 | { 62 | title: "loop", 63 | name: "loop", 64 | type: "boolean" 65 | }, 66 | { 67 | title: "maxheight", 68 | name: "maxheight", 69 | type: "string" 70 | }, 71 | { 72 | title: "maxwidth", 73 | name: "maxwidth", 74 | type: "string" 75 | }, 76 | { 77 | title: "muted", 78 | name: "muted", 79 | type: "boolean" 80 | }, 81 | { 82 | title: "player_id", 83 | name: "player_id", 84 | type: "string" 85 | }, 86 | { 87 | title: "playsinline", 88 | name: "playsinline", 89 | type: "boolean" 90 | }, 91 | { 92 | title: "portrait", 93 | name: "portrait", 94 | type: "boolean" 95 | }, 96 | { 97 | title: "quality", 98 | name: "quality", 99 | type: "string" 100 | }, 101 | { 102 | title: "responsive", 103 | name: "responsive", 104 | type: "boolean" 105 | }, 106 | { 107 | title: "speed", 108 | name: "speed", 109 | type: "boolean" 110 | }, 111 | { 112 | title: "texttrack", 113 | name: "texttrack", 114 | type: "string" 115 | }, 116 | { 117 | title: "title", 118 | name: "title", 119 | type: "boolean" 120 | }, 121 | { 122 | title: "transparent", 123 | name: "transparent", 124 | type: "boolean" 125 | }, 126 | { 127 | title: "width", 128 | name: "width", 129 | type: "string" 130 | }, 131 | { 132 | title: "xhtml", 133 | name: "xhtml", 134 | type: "boolean" 135 | } 136 | ] 137 | }; 138 | 139 | export default vimeoOEmbedConfigData; 140 | -------------------------------------------------------------------------------- /lib/base/schema/objects/vimeoOEmbedData.js: -------------------------------------------------------------------------------- 1 | const vimeoOEmbedData = { 2 | title: "Vimeo oEmbed Data", 3 | name: "vimeoOEmbedData", 4 | type: "object", 5 | fields: [ 6 | { 7 | title: "account_type", 8 | name: "account_type", 9 | type: "string" 10 | }, 11 | { 12 | title: "author_name", 13 | name: "author_name", 14 | type: "string" 15 | }, 16 | { 17 | title: "author_url", 18 | name: "author_url", 19 | type: "string" 20 | }, 21 | { 22 | title: "description", 23 | name: "description", 24 | type: "text" 25 | }, 26 | { 27 | title: "duration", 28 | name: "duration", 29 | type: "number" 30 | }, 31 | { 32 | title: "height", 33 | name: "height", 34 | type: "number" 35 | }, 36 | { 37 | title: "html", 38 | name: "html", 39 | type: "text" 40 | }, 41 | { 42 | title: "is_plus", 43 | name: "is_plus", 44 | type: "string" 45 | }, 46 | { 47 | title: "provider_name", 48 | name: "provider_name", 49 | type: "string" 50 | }, 51 | { 52 | title: "provider_url", 53 | name: "provider_url", 54 | type: "string" 55 | }, 56 | { 57 | title: "thumbnail_height", 58 | name: "thumbnail_height", 59 | type: "number" 60 | }, 61 | { 62 | title: "thumbnail_url", 63 | name: "thumbnail_url", 64 | type: "string" 65 | }, 66 | { 67 | title: "thumbnail_url_with_play_button", 68 | name: "thumbnail_url_with_play_button", 69 | type: "string" 70 | }, 71 | { 72 | title: "thumbnail_width", 73 | name: "thumbnail_width", 74 | type: "number" 75 | }, 76 | { 77 | title: "title", 78 | name: "title", 79 | type: "string" 80 | }, 81 | { 82 | title: "type", 83 | name: "type", 84 | type: "string" 85 | }, 86 | { 87 | title: "upload_date", 88 | name: "upload_date", 89 | type: "string" 90 | }, 91 | { 92 | title: "uri", 93 | name: "uri", 94 | type: "string" 95 | }, 96 | { 97 | title: "version", 98 | name: "version", 99 | type: "string" 100 | }, 101 | { 102 | title: "video_id", 103 | name: "video_id", 104 | type: "number" 105 | }, 106 | { 107 | title: "width", 108 | name: "width", 109 | type: "number" 110 | } 111 | ] 112 | }; 113 | 114 | export default vimeoOEmbedData; 115 | -------------------------------------------------------------------------------- /lib/base/schema/objects/vimeoVideo.js: -------------------------------------------------------------------------------- 1 | import VimeoInput from "@sanity-plugin-vimeo-input/Components/VimeoInput"; 2 | 3 | const vimeoVideo = { 4 | name: "vimeoVideo", 5 | title: "Vimeo Video", 6 | type: "object", 7 | inputComponent: VimeoInput, 8 | fields: [ 9 | { 10 | title: "Video Video URL", 11 | name: "url", 12 | type: "url" 13 | }, 14 | { 15 | title: "oEmbed Data Last Fetched At", 16 | name: "oEmbedDataLastFetchedAt", 17 | type: "date" 18 | }, 19 | { 20 | title: "oEmbed Data JSON Response", 21 | name: "oEmbedDataJsonResponse", 22 | type: "text" 23 | }, 24 | { 25 | title: "oEmbed Data", 26 | name: "oEmbedData", 27 | type: "vimeoOEmbedData" 28 | }, 29 | { 30 | title: "Configuration Data", 31 | name: "oEmbedConfigData", 32 | type: "vimeoOEmbedConfigData" 33 | } 34 | ] 35 | }; 36 | 37 | export default vimeoVideo; 38 | -------------------------------------------------------------------------------- /lib/components/VimeoInput/components/ConfigFieldsInput/components/Switch.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DefaultSwitch from "part:@sanity/components/toggles/switch"; 3 | 4 | const Switch = (props, ref) => { 5 | const { checked, description, disabled, label, name, onChange } = props; 6 | 7 | const handleChange = ({ target }) => { 8 | onChange(target.checked); 9 | }; 10 | 11 | return ( 12 | 20 | ); 21 | }; 22 | 23 | export default Switch; 24 | -------------------------------------------------------------------------------- /lib/components/VimeoInput/components/ConfigFieldsInput/components/TextInput.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { uniqueId } from "lodash"; 3 | import DefaultTextInput from "part:@sanity/components/textinputs/default"; 4 | 5 | import { Description, Label, Title } from "../../Label"; 6 | 7 | const TextInput = (props, ref) => { 8 | const { 9 | description, 10 | disabled, 11 | fieldId, 12 | label, 13 | name, 14 | onChange, 15 | value 16 | } = props; 17 | 18 | const handleChange = ({ target }) => { 19 | onChange(target.value); 20 | }; 21 | 22 | const inputId = uniqueId(`${ fieldId }__text-field`); 23 | 24 | return ( 25 |
26 | 34 | 41 |
42 | ); 43 | }; 44 | 45 | export default TextInput; 46 | -------------------------------------------------------------------------------- /lib/components/VimeoInput/components/ConfigFieldsInput/constants.js: -------------------------------------------------------------------------------- 1 | export const CONFIGURATION_FIELD_TYPES = { 2 | BOOLEAN: "boolean", 3 | STRING: "string" 4 | }; 5 | 6 | export const CONFIGURATION_FIELDS = [ 7 | { 8 | argument: "api", 9 | default: true, 10 | description: "Whether to enable the Vimeo player SDK.", 11 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 12 | }, 13 | { 14 | argument: "autopause", 15 | default: true, 16 | description: "Whether to pause the current video when another Vimeo video on the same page starts to play.", 17 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 18 | }, 19 | { 20 | argument: "autoplay", 21 | default: false, 22 | description: "Whether to start playback of the video automatically. This feature might not work on all devices.", 23 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 24 | }, 25 | { 26 | argument: "background", 27 | default: false, 28 | description: "For videos on a Vimeo Plus account or higher: whether to hide all video controls, loop the video automatically, enable autoplay, and mute the video. The loop and autoplay behaviors can't be overridden, but the mute behavior can be; see the muted argument below.", 29 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 30 | }, 31 | { 32 | argument: "byline", 33 | default: true, 34 | description: "Whether to display the video owner's name.", 35 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 36 | }, 37 | { 38 | argument: "callback", 39 | default: null, 40 | description: "The name of JavaScript function to use as the callback parameter of a JSONP call. The indicated function wraps the JSON response.", 41 | type: CONFIGURATION_FIELD_TYPES.STRING 42 | }, 43 | { 44 | argument: "controls", 45 | default: true, 46 | description: "Whether to display (true) or hide (false) all interactive elements in the player interface. To start video playback when controls are hidden, set autoplay to true or use our player API. This argument is available only for Vimeo Pro and Business accounts.", 47 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 48 | }, 49 | { 50 | argument: "color", 51 | default: null, 52 | description: "The hexadecimal color value of the video controls, which is normally 00ADEF.", 53 | type: CONFIGURATION_FIELD_TYPES.STRING 54 | }, 55 | { 56 | argument: "dnt", 57 | default: false, 58 | description: "Whether to prevent the player from tracking session data, including cookies. Keep in mind that setting this argument to true also blocks video stats.", 59 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 60 | }, 61 | { 62 | argument: "fun", 63 | default: true, 64 | description: "Whether to disable informal error messages in the player, such as Oops.", 65 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 66 | }, 67 | { 68 | argument: "height", 69 | default: null, 70 | description: "The height of the video in pixels.", 71 | type: CONFIGURATION_FIELD_TYPES.STRING 72 | }, 73 | { 74 | argument: "loop", 75 | default: false, 76 | description: "Whether to restart the video automatically after reaching the end.", 77 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 78 | }, 79 | { 80 | argument: "maxheight", 81 | default: null, 82 | description: "The height of the video in pixels, where the video won't exceed its native height, no matter the value of this field.", 83 | type: CONFIGURATION_FIELD_TYPES.STRING 84 | }, 85 | { 86 | argument: "maxwidth", 87 | default: null, 88 | description: "The width of the video in pixels, where the video won't exceed its native width, no matter the value of this field.", 89 | type: CONFIGURATION_FIELD_TYPES.STRING 90 | }, 91 | { 92 | argument: "muted", 93 | default: false, 94 | description: "Whether to mute playback by default. The user can increase the volume manually.", 95 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 96 | }, 97 | { 98 | argument: "player_id", 99 | default: null, 100 | description: "The unique ID for the player, which comes back with all JavaScript API responses.", 101 | type: CONFIGURATION_FIELD_TYPES.STRING 102 | }, 103 | { 104 | argument: "playsinline", 105 | default: true, 106 | description: "Whether the video plays inline on supported mobile devices.", 107 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 108 | }, 109 | { 110 | argument: "portrait", 111 | default: true, 112 | description: "Whether to display the video owner's portrait.", 113 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 114 | }, 115 | { 116 | argument: "quality", 117 | default: "auto", 118 | description: "For videos on a Vimeo Plus account or higher: the playback quality of the video. Use auto for the best possible quality given available bandwidth and other factors. You can also specify 360p, 540p, 720p, 1080p, 2k, and 4k.", 119 | type: CONFIGURATION_FIELD_TYPES.STRING 120 | }, 121 | { 122 | argument: "responsive", 123 | default: false, 124 | description: "Whether to return a responsive embed code, or one that provides intelligent adjustments based on viewing conditions. We recommend this option for mobile-optimized sites.", 125 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 126 | }, 127 | { 128 | argument: "speed", 129 | default: false, 130 | description: "For videos on a Vimeo Plus account or higher: whether to include playback speed among the player preferences.", 131 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 132 | }, 133 | { 134 | argument: "texttrack", 135 | default: null, 136 | description: "The text track to display with the video. Specify the text track by its language code (en), the language code and locale (en-US), or the language code and kind (en.captions). For this argument to work, the video must already have a text track of the given type; see our Help Center or Working with Text Track Uploads for more information.", 137 | type: CONFIGURATION_FIELD_TYPES.STRING 138 | }, 139 | { 140 | argument: "title", 141 | default: true, 142 | description: "Whether to display the video's title.", 143 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 144 | }, 145 | { 146 | argument: "transparent", 147 | default: true, 148 | description: "Whether the background of the player area is transparent on Vimeo. When this value is false, the background of the player area is black. Depending on the video's aspect ratio, there might be visible black bars around the video.", 149 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 150 | }, 151 | { 152 | argument: "width", 153 | default: null, 154 | description: "The width of the video in pixels.", 155 | type: CONFIGURATION_FIELD_TYPES.STRING 156 | }, 157 | { 158 | argument: "xhtml", 159 | default: false, 160 | description: "Whether to make the embed code XHTML-compliant.", 161 | type: CONFIGURATION_FIELD_TYPES.BOOLEAN 162 | } 163 | ]; 164 | -------------------------------------------------------------------------------- /lib/components/VimeoInput/components/ConfigFieldsInput/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Fieldset from "part:@sanity/components/fieldsets/default"; 3 | 4 | import Switch from "./components/Switch"; 5 | import TextInput from "./components/TextInput"; 6 | 7 | import { CONFIGURATION_FIELD_TYPES, CONFIGURATION_FIELDS } from "./constants"; 8 | 9 | const ConfigFieldsInput = (props, ref) => { 10 | const { 11 | configFields, 12 | disabled, 13 | defaultFieldValues, 14 | inputId, 15 | onFieldValueChange 16 | } = props; 17 | 18 | const renderedFields = Object.entries(configFields).map(([configFieldName, configFieldValue], i) => { 19 | const configFieldSettings = CONFIGURATION_FIELDS.find(configFieldSettings => 20 | configFieldSettings.argument === configFieldName 21 | ); 22 | 23 | if (!configFieldSettings) { 24 | console.error( 25 | `Could not locate a known configuration field for name ${ configFieldName }` 26 | ); 27 | return null; 28 | } 29 | 30 | const fieldId = `${ inputId }__field-${ i }`; 31 | 32 | const fieldValue = configFieldValue == null 33 | ? ( 34 | (configFieldName in defaultFieldValues) 35 | ? defaultFieldValues[configFieldName] 36 | : configFieldSettings.default 37 | ) 38 | : configFieldValue; 39 | 40 | switch (configFieldSettings.type) { 41 | case CONFIGURATION_FIELD_TYPES.BOOLEAN: 42 | return ( 43 | { 52 | onFieldValueChange({ field: configFieldName, value: value }); 53 | }} 54 | /> 55 | ); 56 | break; 57 | case CONFIGURATION_FIELD_TYPES.STRING: 58 | return ( 59 | { 67 | onFieldValueChange({ field: configFieldName, value: value }); 68 | }} 69 | value={fieldValue} 70 | /> 71 | ); 72 | break; 73 | default: 74 | return null; 75 | } 76 | }); 77 | 78 | if (renderedFields.length === 0) { 79 | return null; 80 | } 81 | 82 | return ( 83 |
88 | { renderedFields } 89 |
90 | ); 91 | }; 92 | 93 | export default ConfigFieldsInput; 94 | -------------------------------------------------------------------------------- /lib/components/VimeoInput/components/Label/components/Description.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import formFieldStyles from "part:@sanity/components/formfields/default-style"; 3 | 4 | const Description = props => { 5 | const { children } = props; 6 | 7 | return ( 8 |
9 | { children } 10 |
11 | ); 12 | }; 13 | 14 | export default Description; 15 | -------------------------------------------------------------------------------- /lib/components/VimeoInput/components/Label/components/Label.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import formFieldStyles from "part:@sanity/components/formfields/default-style"; 3 | 4 | /* Label 5 | * 6 | * This component and its sub-components, Title and Description, essentially 7 | * mimic the current makeup of labels and descriptions for fields provided 8 | * within Sanity's `DefaultFormField`. However, due to a combination of Sanity 9 | * lacking better documentation for building complicated custom input components 10 | * and to Sanity's various existing components doing **magic** on the backend 11 | * in an attempt to hook some of its components into its own stores, it was 12 | * simply not an option to use the `DefaultFormField` in order to *just* get the 13 | * form field's label styling. Therefore, we had to cut it out ourselves. Enjoy! 14 | * 15 | * See: https://github.com/sanity-io/sanity/blob/next/packages/%40sanity/components/src/formfields/DefaultFormField.tsx 16 | */ 17 | const Label = props => { 18 | const { children, htmlFor } = props; 19 | 20 | return ( 21 | 28 | ); 29 | }; 30 | 31 | export default Label; 32 | -------------------------------------------------------------------------------- /lib/components/VimeoInput/components/Label/components/Title.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import GenericSanityLabel from "part:@sanity/components/labels/default"; 3 | import formFieldStyles from "part:@sanity/components/formfields/default-style"; 4 | 5 | const Title = props => { 6 | const { children } = props; 7 | 8 | return ( 9 |
10 | 11 | { children } 12 | 13 |
14 | ); 15 | }; 16 | 17 | export default Title; 18 | -------------------------------------------------------------------------------- /lib/components/VimeoInput/components/Label/index.js: -------------------------------------------------------------------------------- 1 | export { default as Description } from "./components/Description"; 2 | export { default as Label } from "./components/Label"; 3 | export { default as Title } from "./components/Title"; 4 | -------------------------------------------------------------------------------- /lib/components/VimeoInput/index.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | useCallback, 3 | useEffect, 4 | useImperativeHandle, 5 | useMemo, 6 | useRef, 7 | useState 8 | } from "react"; 9 | import Axios from "axios"; 10 | import { uniqueId } from "lodash"; 11 | import WarningIcon from "part:@sanity/base/warning-icon"; 12 | import Alert from "part:@sanity/components/alerts/alert"; 13 | import Button from "part:@sanity/components/buttons/default"; 14 | import TextInput from "part:@sanity/components/textinputs/default"; 15 | import FormField from "part:@sanity/components/formfields/default"; 16 | import Fieldset from "part:@sanity/components/fieldsets/default"; 17 | import { 18 | FormBuilderInput, 19 | PatchEvent, 20 | patches, 21 | withDocument 22 | } from "part:@sanity/form-builder"; 23 | 24 | import ConfigFieldsInput from "./components/ConfigFieldsInput"; 25 | import { Description, Label, Title } from "./components/Label"; 26 | import styles from "./styles/VimeoInput.css"; 27 | 28 | const O_EMBED_CONFIG_DATA_FIELD_NAME = "oEmbedConfigData"; 29 | const O_EMBED_DATA_FIELD_NAME = "oEmbedData"; 30 | 31 | const client = Axios.create({ 32 | baseURL: "https://vimeo.com/api", 33 | }); 34 | 35 | const VimeoInput = React.forwardRef((props, ref) => { 36 | const { focusPath, level, markers, onChange, type, value } = props; 37 | 38 | const configFieldsInputId = 39 | useMemo(() => uniqueId("video-o-embed-data__config-fields-input"), []); 40 | const urlSearchInputId = 41 | useMemo(() => uniqueId("video-o-embed-data__url-search-input"), []); 42 | const videoIdInputId = 43 | useMemo(() => uniqueId("video-o-embed-data__video-id-input"), []); 44 | const videoTitleInputId = 45 | useMemo(() => uniqueId("video-o-embed-data__video-title-input"), []); 46 | 47 | const oEmbedConfigDataField = 48 | type.fields.find(field => field.name === O_EMBED_CONFIG_DATA_FIELD_NAME); 49 | const oEmbedDataField = 50 | type.fields.find(field => field.name === O_EMBED_DATA_FIELD_NAME); 51 | 52 | const initialUrl = (value && value.url) ? value.url : ""; 53 | 54 | const [ 55 | didErrorFetching, 56 | setDidErrorFetching 57 | ] = useState(false); 58 | const [fetching, setFetching] = useState(false); 59 | const [fetchTrigger, setFetchTrigger] = useState(null); 60 | // `resolvedUrl` is used for keeping track of the initial url and updates to 61 | // the url that take place upon patch events (e.g.; when the search URL form 62 | // is submitted). 63 | const [resolvedUrl, setResolvedUrl] = useState(initialUrl); 64 | // `url` is used for keeping track of the url as represented in the search URL 65 | // form as the user types within it. 66 | const [url, setUrl] = useState(initialUrl); 67 | // `configIsDirty` is used to keep track of the saved state of the config 68 | // against the `resolvedUrl`. This is important because the returned `oEmbed` 69 | // data we get back for the `resolvedUrl` will have details that differ 70 | // according to the passed configuration payload, and we want to know when 71 | // our config values change so that we can prompt the user to re-resolve the 72 | // search URL. 73 | const [configIsDirty, setConfigIsDirty] = useState(false); 74 | // `configFieldValues` is used to hold the transient states of configuration 75 | // values, initially set from the persisted `oEmbedConfigDataField`. We dont 76 | // actually update the `oEmbedConfigDataField` until _after_ the search URL 77 | // is re-resolved so that we can always prompt our user to changes while 78 | // accurately showing config values that reflect the saved `oEmbed` data 79 | // (e.g.; a page refresh should refresh config values to their saved state 80 | // until the search URL is re-resolved (searched)). 81 | const [configFieldValues, setConfigFieldValues] = useState(() => { 82 | const initialConfigFieldValues = 83 | (value && value[O_EMBED_CONFIG_DATA_FIELD_NAME]) 84 | ? value[O_EMBED_CONFIG_DATA_FIELD_NAME] 85 | : {}; 86 | 87 | let configFieldValues = {}; 88 | 89 | if (type.options && type.options.configurableFields) { 90 | configFieldValues = type.options.configurableFields 91 | .reduce((obj, configurableField) => { 92 | obj[configurableField] = 93 | initialConfigFieldValues[configurableField]; 94 | 95 | if ( 96 | obj[configurableField] == null && 97 | type.options.defaultFieldValues[configurableField] != null 98 | ) { 99 | obj[configurableField] = 100 | type.options.defaultFieldValues[configurableField]; 101 | } 102 | 103 | return obj; 104 | }, {}); 105 | } 106 | 107 | return configFieldValues; 108 | }); 109 | 110 | const urlRef = useRef(url); 111 | const searchUrlInputRef = useRef(null); 112 | 113 | useImperativeHandle(ref, () => ({ 114 | focus: focusSearchUrlInput 115 | })); 116 | 117 | useEffect(() => { 118 | urlRef.current = url; 119 | }, [url]); 120 | 121 | useEffect(() => { 122 | if (!fetchTrigger) { return; } 123 | 124 | const cancelSource = Axios.CancelToken.source(); 125 | let artificialTimeout; // Used to add a little tactile feeling to button. 126 | 127 | const fetch = () => { 128 | const url = urlRef.current; 129 | 130 | let configuration = {}; 131 | 132 | if (type.options && type.options.defaultFieldValues) { 133 | configuration = type.options.defaultFieldValues; 134 | } 135 | 136 | if (configFieldValues) { 137 | configuration = { ...configuration, ...configFieldValues }; 138 | } 139 | 140 | configuration.background = !configuration.controls; 141 | 142 | client.get("/oembed.json", { 143 | params: { 144 | url, 145 | ...configuration 146 | }, 147 | options: { 148 | cancelToken: cancelSource.cancelToken 149 | } 150 | }) 151 | .then(res => handleFetchSuccess(url, configuration, res)) 152 | .catch(handleFetchError) 153 | .finally(() => { 154 | setFetching(false); 155 | }) 156 | }; 157 | 158 | setFetching(true); 159 | artificialTimeout = setTimeout(fetch, 500); 160 | 161 | return () => { 162 | cancelSource.cancel(); 163 | clearTimeout(artificialTimeout); 164 | } 165 | }, [fetchTrigger]); 166 | 167 | const handleFetchSuccess = useCallback( 168 | (url, configuration, { data, status }) => { 169 | if (!status || status !== 200) { 170 | setDidErrorFetching(true); 171 | return; 172 | } 173 | 174 | const doc = { 175 | _type: type.name, 176 | url: url, 177 | oEmbedDataLastFetchedAt: new Date().toISOString(), 178 | oEmbedDataJsonResponse: JSON.stringify(data) 179 | }; 180 | 181 | // Set values for oEmbedConfigData. 182 | doc[O_EMBED_CONFIG_DATA_FIELD_NAME] = 183 | Object.entries(configuration).reduce( 184 | (obj, [configFieldName, configFieldValue]) => { 185 | const configSchemaField = 186 | oEmbedConfigDataField.type.fields.find( 187 | field => field.name === configFieldName 188 | ); 189 | 190 | if (!configSchemaField) { 191 | console.log("throwing away %s", configFieldName); 192 | return obj; 193 | } 194 | 195 | if (configSchemaField.type.jsonType === "number") { 196 | configFieldValue = Number(configFieldValue); 197 | } 198 | 199 | obj[configFieldName] = configFieldValue; 200 | 201 | return obj; 202 | }, {} 203 | ); 204 | 205 | // Set values for oEmbedData. 206 | doc[O_EMBED_DATA_FIELD_NAME] = 207 | oEmbedDataField.type.fields.reduce( 208 | (obj, field) => { 209 | let fieldValue = data[field.name]; 210 | 211 | if (!fieldValue) { 212 | return obj; 213 | } 214 | 215 | if (field.type.jsonType === "number") { 216 | fieldValue = Number(fieldValue); 217 | } 218 | 219 | obj[field.name] = fieldValue; 220 | 221 | return obj; 222 | }, {} 223 | ); 224 | 225 | onChange(PatchEvent.from(patches.set(doc))); 226 | setResolvedUrl(url); 227 | setConfigIsDirty(false); 228 | setDidErrorFetching(false); 229 | }, 230 | [oEmbedDataField] 231 | ); 232 | 233 | const handleFetchError = useCallback((err) => { 234 | console.error(err); 235 | 236 | onChange(PatchEvent.from(patches.unset())); 237 | setResolvedUrl(""); 238 | setDidErrorFetching(true); 239 | }, []); 240 | 241 | const handleConfigFieldValueChange = ({ field, value }) => { 242 | setConfigFieldValues({ ...configFieldValues, [field]: value }); 243 | setConfigIsDirty(true); 244 | }; 245 | 246 | const handleUrlSearchChange = ({ target: { value } }) => { 247 | setUrl(value); 248 | }; 249 | 250 | const handleFetchClicked = () => { 251 | setFetchTrigger(+new Date()); 252 | }; 253 | 254 | const focusSearchUrlInput = () => { 255 | searchUrlInputRef.current.focus(); 256 | }; 257 | 258 | return ( 259 |
264 | 275 | { 276 | configIsDirty && ( 277 |
278 | 283 | Changing configuration data requires that you re-load the Vimeo 284 | video. If you are using the same video as before and just 285 | updating configuration values, you just need to click "Load" 286 | again below. 287 | 288 |
289 | ) 290 | } 291 |
292 | 302 |
303 |
304 | 313 |
314 | 323 |
324 | { 325 | didErrorFetching && ( 326 |
327 | 332 | The Vimeo video URL you attempted to load is either invalid or 333 | could not be found. 334 | 335 |
336 | ) 337 | } 338 |
339 | { 340 | resolvedUrl && ( 341 | 342 |
343 | 348 | 359 |
360 |
361 | 366 | {"Video 375 |
376 |
377 | ) 378 | } 379 |
380 | ); 381 | }); 382 | 383 | export default VimeoInput; 384 | -------------------------------------------------------------------------------- /lib/components/VimeoInput/styles/VimeoInput.css: -------------------------------------------------------------------------------- 1 | @import "part:@sanity/base/theme/variables-style"; 2 | 3 | .search { 4 | width: 100%; 5 | display: flex; 6 | align-items: stretch; 7 | } 8 | 9 | .input { 10 | flex-grow: 1; 11 | min-width: 0; 12 | } 13 | 14 | .button { 15 | margin-left: var(--extra-small-padding); 16 | } 17 | 18 | .warningMessage { 19 | margin-top: var(--small-padding); 20 | } 21 | 22 | .thumbnail { 23 | display: block; 24 | height: auto; 25 | vertical-align: bottom; 26 | width: 100%; 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sanity-plugin-goth-vimeo-input", 3 | "description": "A Sanity Plugin for Inputting Vimeo Videos by their URL and Pre-Loading oEmbed Data.", 4 | "version": "1.0.3", 5 | "authors": [ 6 | "(bradley )" 7 | ], 8 | "license": "MIT", 9 | "main": "package.json", 10 | "keywords": [ 11 | "sanity", 12 | "sanity-plugin", 13 | "vimeo", 14 | "oEmbed" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git@github.com:bradley/sanity-plugin-vimeo-input.git" 19 | }, 20 | "scripts": { 21 | "build": "npm run build:clean && npm run build:dist", 22 | "build:clean": "rm -rf dist/", 23 | "build:dist": "npx babel --config-file ./babel.config.js lib/ --out-dir dist --copy-files" 24 | }, 25 | "dependencies": { 26 | "axios": "^1.3.4", 27 | "lodash": "^4.17.21" 28 | }, 29 | "peerDependencies": { 30 | "@sanity/base": "^2.0.1", 31 | "@sanity/components": "^2.0.1", 32 | "@sanity/core": "^2.0.1", 33 | "react": "^17.0.0", 34 | "react-dom": "^17.0.0" 35 | }, 36 | "devDependencies": { 37 | "@babel/cli": "^7.21.0", 38 | "@babel/core": "^7.21.0", 39 | "@babel/node": "^7.20.7", 40 | "@babel/preset-env": "^7.20.2", 41 | "@babel/preset-react": "^7.18.6", 42 | "babel-plugin-lodash": "^3.3.4", 43 | "babel-plugin-module-resolver": "^5.0.0", 44 | "babel-plugin-transform-imports": "^2.0.0", 45 | "babel-plugin-transform-react-remove-prop-types": "^0.4.24", 46 | "babel-preset-es2015": "^6.24.1", 47 | "babel-preset-react": "^6.24.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "paths": { 3 | "source": "./lib", 4 | "compiled": "./dist" 5 | }, 6 | "parts": [ 7 | { 8 | "implements": "part:@sanity/base/schema-type", 9 | "path": "base/schema/objects/vimeoOEmbedConfigData" 10 | }, 11 | { 12 | "implements": "part:@sanity/base/schema-type", 13 | "path": "base/schema/objects/vimeoOEmbedData" 14 | }, 15 | { 16 | "implements": "part:@sanity/base/schema-type", 17 | "path": "base/schema/objects/vimeoVideo" 18 | } 19 | ] 20 | } 21 | --------------------------------------------------------------------------------