1
0
This commit is contained in:
Philip Wagner
2024-08-31 10:01:49 +02:00
commit 78b6c0d381
1169 changed files with 235103 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
/* Colors
---------------------------------*/
$color-white: #fff;
$color-light: #efefef;
$color-light-grey: #999;
$color-dark: #16171a;
$color-dark-grey: #777;
$color-background: $color-light;
$color-border: #ccc;
$color-focus: #4271ae;
$color-focus-border: $color-focus;
/* Pattern
---------------------------------*/
$pattern: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXR0ZXJuIGlkPSJhIiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHBhdHRlcm5Vbml0cz0idXNlclNwYWNlT25Vc2UiPjxwYXRoIGZpbGw9InJnYmEoMCwgMCwgMCwgMC4yKSIgZD0iTTAgMGgxMHYxMEgwem0xMCAxMGgxMHYxMEgxMHoiLz48L3BhdHRlcm4+PHJlY3QgZmlsbD0idXJsKCNhKSIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIvPjwvc3ZnPg==";
/* Breakpoint
---------------------------------*/
$breakpoint-medium: 65em;
/* Input size
---------------------------------*/
$field-input-padding: .5rem;
$field-input-height: 2.25rem;
$field-input-line-height: 1.25rem;
/* Font size
---------------------------------*/
$font-size-small: 0.875rem;

View File

@@ -0,0 +1,143 @@
@import '_variables.scss';
.k-embed-field {
.k-input-icon {
width: auto;
.k-embed-infos {
display: flex;
align-items: center;
line-height: 1;
.k-embed-status {
margin-right: 3px;
&-loading {
display: inline-block;
.loader {
display: inline-block;
overflow: hidden;
height: 1.3em;
margin-top: -0.3em;
line-height: 1.5em;
font-size: 1rem;
vertical-align: text-bottom;
&::after {
content: "⠋\A⠙\A⠹\A⠸\A⠼\A⠴\A⠦\A⠧\A⠇\A⠏";
display: inline-table;
white-space: pre;
text-align: left;
animation: spin10 0.8s steps(10) infinite;
}
}
@keyframes spin10 { to { transform: translateY(-15.0em); } }
}
&-synced, &-failed {
font-size: 0.8rem;
padding: 2px 5px 2px 6px;
display: inline-block;
border-radius: 3px;
}
&-synced {
background: var(--color-green-300);
display: flex;
align-items: center;
.checkmark {
position: relative;
width: 20px;
height: 20px;
&:before {
content: "";
position: absolute;
top: 5px;
left: 7px;
width: 9px;
height: 6px;
transform: rotate(-45deg);
background: none;
border: solid black;
border-width: 0 0 1px 1px;
}
}
}
&-failed {
background: var(--color-red-300);
display: flex;
align-items: center;
.cross {
position: relative;
width: 20px;
height: 20px;
&:before, &:after {
content: "";
position: absolute;
left: 11px;
top: 5px;
height: 10px;
width: 1px;
background-color: black;
}
&:before {
transform: rotate(45deg);
}
&:after {
transform: rotate(-45deg);
}
}
}
}
}
&-button {
width: 2.5em;
}
}
}
.preview {
position: relative;
margin-bottom: 0.5rem;
&-content {
padding: 1rem;
position: relative;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
iframe {
max-width: 100% !important;
}
img {
width: auto !important;
height: 30vh !important;
}
}
&-background {
position: absolute;
top: 0;
left: 0;
z-index: 0;
width: 100%;
height: 100%;
background: $color-light url($pattern);
opacity: 0.45;
}
&[data-provider=youtube],
&[data-provider=vimeo] {
.preview-content {
padding: 0;
iframe {
width: 100%;
aspect-ratio: 16 / 9;
height: auto;
}
}
}
}
.k-embed-field-preview {
padding: 0.375rem var(--table-cell-padding);
&-inner {
overflow: hidden;
}
.k-embed-icon {
--fit: cover;
--back: var(--pattern);
}
}

View File

@@ -0,0 +1,99 @@
<template>
<k-field :input="_uid" v-bind="$props" class="k-embed-field k-url-field k-field">
<div class="preview" v-if="hasMedia" :data-provider="providerName">
<div class="preview-content" v-html="media.code"></div>
<div class="preview-background"></div>
</div>
<k-input ref="input" :id="_uid" v-bind="$props" :value="inputValue" :media="media" theme="field" v-on="$listeners" @setMedia="setMedia" @startLoading="startLoading">
<div class="k-embed-infos" slot="icon">
<div class="k-embed-status">
<span v-if="loading" class="k-embed-status-loading"><span class="loader"></span></span>
<span v-else-if="hasMedia" class="k-embed-status-synced">{{ $t('embed.synced') }} <span class="checkmark"></span></span>
<span v-else-if="syncFailed" class="k-embed-status-failed">{{ $t('embed.failed') }} <span class="cross"></span></span>
</div>
<k-button v-if="link"
:icon="icon"
:link="inputValue"
:tooltip="$t('open')"
class="k-input-icon-button"
tabindex="-1"
target="_blank"
rel="noopener" />
</div>
</k-input>
</k-field>
</template>
<script>
import { isUrl, matchProvider } from '../helpers/validators.js'
export default {
extends: 'k-url-field',
data() {
return {
media: Object,
loading: false,
}
},
props: {
provider: String,
},
created() {
if(this.value && this.value.media && this.hasLength(this.value.media)) {
this.media = this.value.media
}
},
watch: {
inputValue() {
if(this.value && this.value.media && this.hasLength(this.value.media)) {
this.media = this.value.media
}
else {
this.media = {}
}
}
},
computed: {
hasMedia() {
return this.hasLength(this.media) && this.media.code
},
providerName() {
return this.hasMedia && this.media.providerName ? this.media.providerName.toLowerCase() : null
},
syncFailed() {
return this.inputValue != '' && this.isEmbeddableUrl(this.inputValue) && !this.hasMedia
},
inputValue() {
return this.value && this.value.input ? this.value.input : ''
}
},
methods: {
setMedia(media) {
this.media = media
this.stopLoading()
},
hasLength(obj) {
return Object.keys(obj).length
},
startLoading() {
this.loading = true
},
stopLoading() {
this.loading = false
},
isEmbeddableUrl(value) {
if(!isUrl(value)) return false
if(this.provider && !matchProvider(value, this.provider)) return false
return true
}
}
};
</script>
<style lang="scss">
@import '../assets/css/styles.scss'
</style>

View File

@@ -0,0 +1,88 @@
<script>
import { isUrl, matchProvider } from '../helpers/validators.js'
export default {
extends: 'k-url-input',
props: {
provider: String,
media: Object,
},
mounted() {
this.loadEmbedScripts()
},
methods: {
onInput(value) {
if(value == '' || !this.isEmbeddableUrl(value)) {
this.media = {}
this.emitInput(value)
return false;
}
if(value.includes('https://www.instagram.com')) {
value = value.split('?')[0].replace(/\/$/, "");
}
this.$emit('startLoading')
this.$api
.get('kirby-embed/get-data', { url: value })
.then(response => {
if(response['status'] == 'success' && response['data']) {
if(response['data']['providerName'] == 'Vimeo') {
let iframe = response['data']['code']
iframe = this.htmlToElement(iframe)
iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin')
response['data']['code'] = iframe.outerHTML
}
this.media = response['data']
}
else {
this.media = {}
}
this.emitInput(value)
})
.catch(error => {
this.media = {}
this.emitInput(value)
})
},
emitInput(value) {
this.$emit("input", { input: value, media: this.media });
this.$emit("setMedia", this.media)
this.loadEmbedScripts()
},
loadEmbedScripts() {
if (window.twttr) {
window.twttr.widgets.load();
}
else if (this.media && Object.keys(this.media).length && this.media.providerName.toLowerCase() == 'twitter') {
const embed = document.createElement('script');
embed.src = 'https://platform.twitter.com/widgets.js';
document.body.appendChild(embed);
}
if (window.instgrm) {
window.instgrm.Embeds.process();
}
else if (this.media && Object.keys(this.media).length && this.media.providerName.toLowerCase() == 'instagram') {
const embed = document.createElement('script');
embed.src = 'https://www.instagram.com/embed.js';
document.body.appendChild(embed);
}
},
isEmbeddableUrl(value) {
if(!isUrl(value)) return false
if(this.provider && !matchProvider(value, this.provider)) return false
return true
},
htmlToElement(html) {
let template = document.createElement('template')
html = html.trim()
template.innerHTML = html
return template.content.firstChild
}
},
};
</script>

View File

@@ -0,0 +1,53 @@
<template>
<div class="k-embed-field-preview">
<div class="k-embed-field-preview-inner k-bubble" data-has-text="true">
<div class="k-embed-icon k-frame">
<img :src="iconSrc">
</div>
<div class="k-embed-url">
{{ url }}
</div>
</div>
</div>
</template>
<script>
export default {
props: {
value: String,
field: Object
},
computed: {
url() {
return this.value.input.replace(/^\/\/|^.*?:\/\//, '')
},
isSynced() {
return Object.keys(this.value.media).length && this.value.media.code !== null
},
iconSrc() {
return this.$panel.$urls.site +'/media/plugins/sylvainjule/embed/svg/'+ this.providerIcon;
},
providerIcon() {
if(this.isSynced) {
if(this.field.icons) {
let provider = this.value.media.providerName
let icons = ['Vimeo', 'Flickr', 'Instagram', 'SoundCloud', 'Spotify', 'Twitter', 'YouTube'];
if(icons.includes(provider)) {
return 'embed-icon-'+ provider.toLowerCase() +'.svg'
}
else {
return 'embed-icon-synced.svg';
}
}
else {
return 'embed-icon-synced.svg'
}
}
else {
return 'embed-icon-failed.svg'
}
}
},
};
</script>

View File

@@ -0,0 +1,21 @@
export function isUrl(value) {
const pattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/g
const regex = new RegExp(pattern)
return !value || value.match(regex)
}
export function matchProvider(value, provider) {
const patterns = {
'youtube': /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/,
'vimeo': /vimeo\.com\/([0-9]+)/,
'flickr': /^.*(flickr\.com)\/(.*)/,
'soundcloud': /^.*(soundcloud\.com|snd\.sc)\/(.*)/,
'twitter': /^.*(twitter\.com)\/(.*)/,
'instagram': /^.*(instagram\.com)\/(.*)/
};
const patternKeys = Object.keys(patterns)
if (patternKeys.indexOf(provider) == -1) return false
return value.match(patterns[provider])
}

View File

@@ -0,0 +1,13 @@
import embedField from './components/embedField.vue'
import embedInput from './components/embedInput.vue'
import embedPreview from './components/embedPreview.vue'
panel.plugin('sylvainjule/embed', {
fields: {
embed: embedField,
},
components: {
'k-embed-input': embedInput,
'k-embed-field-preview': embedPreview,
}
});