init
This commit is contained in:
38
site/plugins/embed/src/assets/css/_variables.scss
Normal file
38
site/plugins/embed/src/assets/css/_variables.scss
Normal 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;
|
||||
143
site/plugins/embed/src/assets/css/styles.scss
Normal file
143
site/plugins/embed/src/assets/css/styles.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
99
site/plugins/embed/src/components/embedField.vue
Normal file
99
site/plugins/embed/src/components/embedField.vue
Normal 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>
|
||||
88
site/plugins/embed/src/components/embedInput.vue
Normal file
88
site/plugins/embed/src/components/embedInput.vue
Normal 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>
|
||||
53
site/plugins/embed/src/components/embedPreview.vue
Normal file
53
site/plugins/embed/src/components/embedPreview.vue
Normal 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>
|
||||
21
site/plugins/embed/src/helpers/validators.js
Normal file
21
site/plugins/embed/src/helpers/validators.js
Normal 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])
|
||||
}
|
||||
13
site/plugins/embed/src/index.js
Normal file
13
site/plugins/embed/src/index.js
Normal 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,
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user