-
-
Notifications
You must be signed in to change notification settings - Fork 641
[6.x] Add Cloudflare Stream support to the video fieldtype #15459
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 6.x
Are you sure you want to change the base?
Changes from all commits
aed6a73
c44253e
862f032
f17b781
3745c70
9240993
83df089
dc360f7
56acf47
cdbcc04
81c399a
b752d12
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,26 @@ | ||||||||||||||||||||||||||||
| <template> | ||||||||||||||||||||||||||||
| <div class="flex flex-col space-y-3 p-1.5 bg-gray-100 border border-gray-300 dark:bg-gray-900 dark:border-gray-700 rounded-xl"> | ||||||||||||||||||||||||||||
| <ui-input-group> | ||||||||||||||||||||||||||||
| <ui-combobox | ||||||||||||||||||||||||||||
| :model-value="mode" | ||||||||||||||||||||||||||||
| :options="meta.providers" | ||||||||||||||||||||||||||||
| option-label="label" | ||||||||||||||||||||||||||||
| option-value="value" | ||||||||||||||||||||||||||||
| :aria-label="__('Video Provider')" | ||||||||||||||||||||||||||||
| @update:model-value="changeMode" | ||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||
|
Comment on lines
+5
to
+10
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The combobox needs I confirmed it by mounting this component with
Suggested change
|
||||||||||||||||||||||||||||
| <ui-input-group v-if="isCloudflare"> | ||||||||||||||||||||||||||||
| <ui-input-group-prepend :text="__('ID')" /> | ||||||||||||||||||||||||||||
| <ui-input | ||||||||||||||||||||||||||||
| :model-value="videoId" | ||||||||||||||||||||||||||||
| :isReadOnly="isReadOnly" | ||||||||||||||||||||||||||||
| :aria-label="__('Video ID')" | ||||||||||||||||||||||||||||
| @update:model-value="updateCloudflareId" | ||||||||||||||||||||||||||||
| @focus="$emit('focus')" | ||||||||||||||||||||||||||||
| @blur="$emit('blur')" | ||||||||||||||||||||||||||||
| input-class="border-s-0" | ||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||
| </ui-input-group> | ||||||||||||||||||||||||||||
| <ui-input-group v-else> | ||||||||||||||||||||||||||||
| <ui-input-group-prepend :text="__('URL')" /> | ||||||||||||||||||||||||||||
| <ui-input | ||||||||||||||||||||||||||||
| :model-value="value" | ||||||||||||||||||||||||||||
|
|
@@ -29,22 +49,32 @@ | |||||||||||||||||||||||||||
| <script> | ||||||||||||||||||||||||||||
| import Fieldtype from './Fieldtype.vue'; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const CLOUDFLARE = 'cloudflare'; | ||||||||||||||||||||||||||||
| const CLOUDFLARE_PREFIX = 'cloudflare:'; | ||||||||||||||||||||||||||||
| const URL_MODE = 'url'; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export default { | ||||||||||||||||||||||||||||
| mixins: [Fieldtype], | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| data() { | ||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||
| isVisible: false, | ||||||||||||||||||||||||||||
| observer: null, | ||||||||||||||||||||||||||||
| // Only consulted when there's no value; otherwise the value itself says which input to show. | ||||||||||||||||||||||||||||
| mode: this.meta.video?.provider === CLOUDFLARE ? CLOUDFLARE : URL_MODE, | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| computed: { | ||||||||||||||||||||||||||||
| shouldShowPreview() { | ||||||||||||||||||||||||||||
| return !this.isInvalid && (this.isEmbeddable || this.isVideo); | ||||||||||||||||||||||||||||
| return !this.isInvalid && (this.isCloudflare ? !!this.videoId : this.isEmbeddable || this.isVideo); | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| embedUrl() { | ||||||||||||||||||||||||||||
| if (this.isCloudflare) { | ||||||||||||||||||||||||||||
| return this.videoId ? `https://iframe.cloudflarestream.com/${this.videoId}` : null; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let embed_url = this.value || ''; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (embed_url.includes('youtube')) { | ||||||||||||||||||||||||||||
|
|
@@ -73,6 +103,10 @@ export default { | |||||||||||||||||||||||||||
| return embed_url; | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| isCloudflare() { | ||||||||||||||||||||||||||||
| return this.value?.startsWith(CLOUDFLARE_PREFIX) || (!this.value && this.mode === CLOUDFLARE); | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| isEmbeddable() { | ||||||||||||||||||||||||||||
| const url = this.value || ''; | ||||||||||||||||||||||||||||
| const isYoutube = url.includes('youtube') || url.includes('youtu.be'); | ||||||||||||||||||||||||||||
|
|
@@ -81,6 +115,8 @@ export default { | |||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| isInvalid() { | ||||||||||||||||||||||||||||
| if (this.isCloudflare) return !!this.videoId && !/^[a-zA-Z0-9]+$/.test(this.videoId); | ||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor, but when this branch fails the message rendered below is |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let htmlRegex = new RegExp(/<([A-Z][A-Z0-9]*)\b[^>]*>.*?<\/\1>|<([A-Z][A-Z0-9]*)\b[^\/]*\/>/i); | ||||||||||||||||||||||||||||
| return htmlRegex.test(this.value || ''); | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
|
|
@@ -95,6 +131,24 @@ export default { | |||||||||||||||||||||||||||
| const isVideo = url.includes('.mp4') || url.includes('.ogv') || url.includes('.mov') || url.includes('.webm'); | ||||||||||||||||||||||||||||
| return !this.isEmbeddable && isVideo; | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| videoId() { | ||||||||||||||||||||||||||||
| return this.value?.startsWith(CLOUDFLARE_PREFIX) ? this.value.slice(CLOUDFLARE_PREFIX.length) : null; | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| methods: { | ||||||||||||||||||||||||||||
| changeMode(mode) { | ||||||||||||||||||||||||||||
| if (mode === this.mode) return; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| this.mode = mode; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (this.value) this.update(null); | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| updateCloudflareId(id) { | ||||||||||||||||||||||||||||
| this.update(id ? `${CLOUDFLARE_PREFIX}${id}` : null); | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| mounted() { | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The description says "the stored value is the single source of truth for which input is shown, so the two can't disagree" — but they can, because which input renders is driven by
isCloudflare(value-derived) while which option the dropdown displays is bound tomode, which is seeded once frommeta.video.provider(server-derived).Those disagree whenever the stored
cloudflare:value is malformed:Embed::fromValue()reports providerunsupportedfor it, while the JS only checks the prefix. Mounting withvalue: 'cloudflare:ABC-123'andmeta.video.provider: 'unsupported'gives:isCloudflare→true, so the ID input renders showingABC-123mode→'url', so the dropdown reads URLThis is reachable —
isInvalidis only a visual hint and there's no server-side validation, so a malformed ID saves fine and the field comes back in that contradictory state on reload.Binding to the same source of truth fixes it in every case, since
isCloudflarealready falls back tomodewhen there's no value:With that,
modegoes back to being purely the empty-field seed, which is what the comment on it already claims.