Brings URL metadata type to core #689 and refactor it for Vue3 #794.

This commit is contained in:
mateuswetah 2024-01-25 11:54:47 -03:00
parent 815329f478
commit a186bf76b4
9 changed files with 477 additions and 1 deletions

2
package-lock.json generated
View File

@ -1647,7 +1647,7 @@
},
"@polka/url": {
"version": "1.0.0-next.24",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz",
"resolved": "https://registry.npmjs.org/@polka/tainacan-url-plugin-metadata-type/-/url-1.0.0-next.24.tgz",
"integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==",
"dev": true
},

View File

@ -43,6 +43,7 @@ class Metadata_Type_Helper {
$this->Tainacan_Metadata->register_metadata_type('Tainacan\Metadata_Types\User');
$this->Tainacan_Metadata->register_metadata_type('Tainacan\Metadata_Types\Control');
$this->Tainacan_Metadata->register_metadata_type('Tainacan\Metadata_Types\GeoCoordinate');
$this->Tainacan_Metadata->register_metadata_type('Tainacan\Metadata_Types\URL');
// the priority should see less than on function
// `load_admin_page()` of class `Admin` in file /src/views/class-tainacan-admin.php

View File

@ -0,0 +1,154 @@
<template>
<div>
<b-field
:addons="false"
:label="$i18n.getHelperTitle('tainacan-url', 'link-as-button')">
&nbsp;
<b-switch
v-model="linkAsButton"
true-value="yes"
false-value="no"
size="is-small"
@update:model-value="onUpdateLinkAsButton" />
<help-button
:title="$i18n.getHelperTitle('tainacan-url', 'link-as-button')"
:message="$i18n.getHelperMessage('tainacan-url', 'link-as-button')" />
</b-field>
<b-field
:addons="false"
:label="$i18n.getHelperTitle('tainacan-url', 'force-iframe')">
&nbsp;
<b-switch
v-model="forceIframe"
size="is-small"
true-value="yes"
false-value="no"
@update:model-value="onUpdateForceIframe" />
<help-button
:title="$i18n.getHelperTitle('tainacan-url', 'force-iframe')"
:message="$i18n.getHelperMessage('tainacan-url', 'force-iframe')" />
</b-field>
<b-field
v-if="forceIframe == 'yes'"
:addons="false">
<label class="label is-inline-block">
{{ $i18n.getHelperTitle('tainacan-url', 'iframe-min-height') }}
<help-button
:title="$i18n.getHelperTitle('tainacan-url', 'iframe-min-height')"
:message="$i18n.getHelperMessage('tainacan-url', 'iframe-min-height')" />
</label>
<b-numberinput
:model-value="iframeMinimumHeight === '' ? 0 : iframeMinimumHeight"
size="is-small"
step="1"
@update:model-value="onUpdateIframeMinimumHeight" />
</b-field>
<b-field
v-if="forceIframe == 'yes'"
:addons="false"
:label="$i18n.getHelperTitle('tainacan-url', 'iframe-allowfullscreen')">
&nbsp;
<b-switch
v-model="iframeAllowfullscreen"
size="is-small"
true-value="yes"
false-value="no"
@update:model-value="onUpdateIframeAllowfullscreen" />
<help-button
:title="$i18n.getHelperTitle('tainacan-url', 'iframe-allowfullscreen')"
:message="$i18n.getHelperMessage('tainacan-url', 'iframe-allowfullscreen')" />
</b-field>
<b-field
v-if="forceIframe == 'yes'"
:addons="false"
:label="$i18n.getHelperTitle('tainacan-url', 'is-image')">
&nbsp;
<b-switch
v-model="isImage"
size="is-small"
true-value="yes"
false-value="no"
@update:model-value="onUpdateIsImage" />
<help-button
:title="$i18n.getHelperTitle('tainacan-url', 'is-image')"
:message="$i18n.getHelperMessage('tainacan-url', 'is-image')" />
</b-field>
</div>
</template>
<script>
export default {
name: "TainacanMetadataFormTypeURL",
props: {
value: [String, Object, Array],
},
emits: [ 'update:value' ],
data() {
return {
linkAsButton: String,
forceIframe: String,
iframeMinimumHeight: [Number, String],
iframeAllowfullscreen: String,
isImage: String
};
},
created() {
this.linkAsButton = this.value && this.value['link-as-button'] ? this.value['link-as-button'] : 'no';
this.forceIframe = this.value && this.value['force-iframe'] ? this.value['force-iframe'] : 'no';
this.iframeMinimumHeight = this.value && this.value['iframe-min-height'] ? this.value['iframe-min-height'] : '';
this.iframeAllowfullscreen = this.value && this.value['iframe-allowfullscreen'] ? this.value['iframe-allowfullscreen'] : 'no';
this.isImage = this.value && this.value['is-image'] ? this.value['is-image'] : 'no';
},
methods: {
onUpdateLinkAsButton(value) {
this.$emit('update:value', {
'link-as-button': value,
'force-iframe': this.forceIframe,
'iframe-min-height': this.iframeMinimumHeight,
'iframe-allowfullscreen': this.iframeAllowfullscreen,
'is-image': this.isImage,
});
},
onUpdateForceIframe(value) {
this.$emit('update:value', {
'link-as-button': this.linkAsButton,
'force-iframe': value,
'iframe-min-height': this.iframeMinimumHeight,
'iframe-allowfullscreen': this.iframeAllowfullscreen,
'is-image': this.isImage,
});
},
onUpdateIframeMinimumHeight(value) {
this.$emit('update:value', {
'link-as-button': this.linkAsButton,
'force-iframe': this.forceIframe,
'iframe-min-height': value,
'iframe-allowfullscreen': this.iframeAllowfullscreen,
'is-image': this.isImage,
});
},
onUpdateIframeAllowfullscreen(value) {
this.$emit('update:value', {
'link-as-button': this.linkAsButton,
'force-iframe': this.forceIframe,
'iframe-min-height': this.iframeMinimumHeight,
'iframe-allowfullscreen': value,
'is-image': this.isImage,
});
},
onUpdateIsImage(value) {
this.$emit('update:value', {
'link-as-button': this.linkAsButton,
'force-iframe': this.forceIframe,
'iframe-min-height': this.iframeMinimumHeight,
'iframe-allowfullscreen': this.iframeAllowfullscreen,
'is-image': value,
});
},
},
};
</script>

View File

@ -0,0 +1,87 @@
<template>
<div v-if="itemMetadatum">
<b-input
:id="'tainacan-item-metadatum_id-' + itemMetadatum.metadatum.id + (itemMetadatum.parent_meta_id ? ('_parent_meta_id-' + itemMetadatum.parent_meta_id) : '')"
:disabled="disabled"
:model-value="value"
:placeholder="itemMetadatum.metadatum.placeholder ? itemMetadatum.metadatum.placeholder : '[link](https://url.com)'"
@update:model-value="($event) => onInput($event)"
@blur="onBlur" />
<div
v-if="itemMetadatum.item.id"
class="add-new-term">
<a
v-if="value"
class="add-link"
@click="previewHtml">
<span class="icon">
<i class="tainacan-icon has-text-secondary tainacan-icon-see" />
</span>
<span style="font-size: 0.75em">&nbsp;{{ $i18n.get('label_preview', 'tainacan') }}</span>
</a>
</div>
<transition name="filter-item">
<div
v-if="isPreviewingHtml"
v-html="singleHTMLPreview" />
</transition>
</div>
</template>
<script>
export default {
name: 'TainacanMetadataTypeURL',
props: {
itemMetadatum: Object,
value: [String, Number, Array],
disabled: false
},
emits: [ 'update:value', 'blur' ],
data() {
return {
isPreviewingHtml: false,
singleHTMLPreview: ''
}
},
methods: {
onInput(value) {
this.isPreviewingHtml = false;
this.singleHTMLPreview = '';
this.$emit('update:value', value);
},
onBlur() {
this.$emit('blur');
},
createElementFromHTML(htmlString) {
let div = document.createElement('div');
div.innerHTML = htmlString.trim();
return div;
},
previewHtml() {
// If we are going to display preview, renders it
if (!this.isPreviewingHtml) {
// Multivalued metadata need to be split as the values_as_html shows every value
if (this.itemMetadatum.metadatum.multiple == 'yes') {
const valuesAsHtml = this.createElementFromHTML(this.itemMetadatum.value_as_html);
const valuesAsArray = Object.values(valuesAsHtml.children).filter((aValue) => aValue.outerHTML != '<span class="multivalue-separator"> | </span>');
const singleValueIndex = this.itemMetadatum.value.findIndex((aValue) => aValue == this.value);
if (singleValueIndex >= 0)
this.singleHTMLPreview = valuesAsArray[singleValueIndex].outerHTML;
} else {
this.singleHTMLPreview = this.itemMetadatum.value_as_html;
}
}
// Toggle Preview view
this.isPreviewingHtml = !this.isPreviewingHtml;
}
},
};
</script>

View File

@ -0,0 +1,209 @@
<?php
namespace Tainacan\Metadata_Types;
class URL extends Metadata_Type {
use \Tainacan\Traits\Formatter_Text;
function __construct() {
parent::__construct();
// Basic options
$this->set_name( __('URL', 'tainacan') );
$this->set_description( __('An URL link, possibly with embedded content.', 'tainacan') );
$this->set_primitive_type('string');
$this->set_component('tainacan-url');
$this->set_form_component('tainacan-form-url');
$this->set_default_options([
'link-as-button' => 'no',
'force-iframe' => 'no',
'iframe-min-height' => '',
'iframe-allowfullscreen' => 'no',
'is-image' => 'no'
]);
$this->set_preview_template('
<div>
<div class="control is-clearfix">
<input type="url" placeholder="https://youtube.com/?v=abc123456" class="input">
</div>
</div>
');
}
/**
* @inheritdoc
*/
public function get_form_labels() {
return [
'link-as-button' => [
'title' => __( 'Display link as a button', 'tainacan' ),
'description' => __( 'Style the link to be displayed as a button instead of a simple textual link.', 'tainacan' ),
],
'force-iframe' => [
'title' => __( 'Force iframe', 'tainacan' ),
'description' => __( 'Force the URL to be displayed in an iframe in case the content is not embeddable by WordPress.', 'tainacan' ),
],
'iframe-min-height' => [
'title' => __( 'Forced iframe minimum height', 'tainacan' ),
'description' => __( 'If forcing the use of an iframe, sets the height attribute, in pixels. Leave it empty to be 100% of the container.', 'tainacan' ),
],
'iframe-allowfullscreen' => [
'title' => __( 'Allow fullscreen on forced iframe', 'tainacan' ),
'description' => __( 'If forcing the use of an iframe, allows it to request fullscreen to the browser.', 'tainacan' ),
],
'is-image' => [
'title' => __( 'Is link to external image', 'tainacan' ),
'description' => __( 'If you are linking directly to an external image, use this option so it can be properly embedded.', 'tainacan' ),
]
];
}
/**
* Get the value as a HTML string with links
* @return string
*/
public function get_value_as_html(\Tainacan\Entities\Item_Metadata_Entity $item_metadata) {
$value = $item_metadata->get_value();
$link_as_button = $this->get_option('link-as-button') == 'yes';
$return = '';
$return .= $link_as_button ? '<div class="wp-block-buttons">' : '';
if ( is_array($value) && $item_metadata->is_multiple() ) {
$total = sizeof($value);
$count = 0;
$prefix = $item_metadata->get_multivalue_prefix();
$suffix = $item_metadata->get_multivalue_suffix();
$separator = $item_metadata->get_multivalue_separator();
foreach ( $value as $el ) {
if ( !empty($el) ) {
$return .= $prefix;
$return .= $this->get_single_value_as_html($el);
$return .= $suffix;
$count ++;
if ($count < $total && !$link_as_button)
$return .= $separator;
}
}
} else {
$return .= $this->get_single_value_as_html($value);
}
$return .= $link_as_button ? '</div>' : '';
return $return;
}
/**
* Get the a single value as a HTML string with links
* @return string
*/
public function get_single_value_as_html($value) {
global $wp_embed;
$link_as_button = $this->get_option('link-as-button') == 'yes';
$return = '';
if ($link_as_button) {
$mkstr = preg_replace(
'/\[([^\]]+)\]\(([^\)]+)\)/',
'<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="\2" target="_blank" title="\1">\1</a></div>',
$value
);
$return = $this->make_clickable_links($mkstr);
} else {
// First, we try WordPress autoembed
$embed = $wp_embed->autoembed($value);
// If it didn't work, it will still ba a URL
if ( esc_url($embed) == esc_url($value) ) {
// Than we can force the usage of an iframe
if ( $this->get_option('force-iframe') == 'yes' ) {
// URL points to an image file
if ( $this->get_option('is-image') == 'yes' ) {
$return = sprintf('<a href="%s" target="blank"><img src="%s" /></a>', $value, $value);
// URL points to a content that is not an image
} else {
$iframeMininumHeight = '100%';
if (!empty($this->get_option('iframe-min-height')))
$iframeMininumHeight = $this->get_option('iframe-min-height');
// Creates an embed with responsive wrapper
$tainacan_embed = \Tainacan\Embed::get_instance();
$return = $tainacan_embed->add_responsive_wrapper( '<iframe src="' . $value . '" width="100%" height="' . $iframeMininumHeight . '" style="border:none;" allowfullscreen="' . ($this->get_option('iframe-allowfullscreen') == 'yes' ? 'true' : 'false') . '"></iframe>' );
}
// Or we can leave it as a link
} else {
$mkstr = preg_replace(
'/\[([^\]]+)\]\(([^\)]+)\)/',
'<a href="\2" target="_blank" title="\1">\1</a>',
$value
);
$return = $this->make_clickable_links($mkstr);
}
// If the autoembed did work, we pass the responsive wrapper to it
} else {
$tainacan_embed = \Tainacan\Embed::get_instance();
$return = $tainacan_embed->add_responsive_wrapper($embed);
}
}
return $return;
}
/**
* Checks if the value passed is a valid URL or markdown link
* @return boolean
*/
public function validate(\Tainacan\Entities\Item_Metadata_Entity $item_metadata) {
$value = $item_metadata->get_value();
//$reg_mrkd = '~\[(.+)\]\(([^ ]+)?\)~i';
$reg_url = '~^((www\.|http:\/\/www\.|http:\/\/|https:\/\/www\.|https:\/\/|ftp:\/\/www\.|ftp:\/\/|ftps:\/\/www\.|ftps:\/\/)[^"<\s]+)(?![^<>]*>|[^"]*?<\/a)$~i';
$reg_full = '~\[(.+)\]\((((www\.|http:\/\/www\.|http:\/\/|https:\/\/www\.|https:\/\/|ftp:\/\/www\.|ftp:\/\/|ftps:\/\/www\.|ftps:\/\/)[^"<\s]+)(?![^<>]*>|[^"]*?<\/a))?\)~i';
// Multivalued metadata --------------
if ( is_array($value) ) {
foreach ($value as $url_value) {
// Empty strings are valid
if ( !empty($url_value) ) {
// If this seems to be a markdown link, we check if the url inside it is ok as well
if ( !preg_match($reg_url, $url_value) && !preg_match($reg_full, $url_value) ) {
$this->add_error( sprintf( __('"%s" is invalid. Please provide a valid, full URL or a Markdown link in the form of [label](url).', 'tainacan'), $url_value ) );
return false;
}
}
}
return true;
}
// Single valued metadata --------------
// Empty strings are valid
if ( !empty($value) ) {
// If this seems to be a markdown link, we check if the url inside it is ok as well
if ( !preg_match($reg_url, $value) && !preg_match($reg_full, $value) ) {
$this->add_error( sprintf( __('"%s" is invalid. Please provide a valid, full URL or a Markdown link in the form of [label](url).', 'tainacan'), $value ) );
return false;
}
}
return true;
}
}
?>

View File

@ -44,6 +44,7 @@ import TainacanTaxonomy from '../components/metadata-types/taxonomy/TainacanTaxo
import TainacanCompound from '../components/metadata-types/compound/TainacanCompound.vue';
import TainacanUser from '../components/metadata-types/user/TainacanUser.vue';
import TainacanGeoCoordinate from '../components/metadata-types/geocoordinate/TainacanGeoCoordinate.vue'
import TainacanURL from '../components/metadata-types/url/TainacanURL.vue';
import FormText from '../components/metadata-types/text/FormText.vue';
import FormTextarea from '../components/metadata-types/textarea/FormTextarea.vue';
@ -53,6 +54,7 @@ import FormSelectbox from '../components/metadata-types/selectbox/FormSelectbox.
import FormNumeric from '../components/metadata-types/numeric/FormNumeric.vue';
import FormUser from '../components/metadata-types/user/FormUser.vue';
import FormGeoCoordinate from '../components/metadata-types/geocoordinate/FormGeoCoordinate.vue';
import FormURL from '../components/metadata-types/url/FormURL.vue';
// Term edition form must be imported here so that it is not necessary on item-submission bundle
import TermEditionForm from '../components/edition/term-edition-form.vue';
@ -231,6 +233,7 @@ export default (element) => {
app.component('tainacan-compound', TainacanCompound);
app.component('tainacan-user', TainacanUser);
app.component('tainacan-geocoordinate', TainacanGeoCoordinate);
app.component('tainacan-url', TainacanURL);
/* Metadata Option forms */
@ -243,6 +246,7 @@ export default (element) => {
app.component('tainacan-form-user', FormUser);
app.component('term-edition-form', TermEditionForm);
app.component('tainacan-form-geocoordinate', FormGeoCoordinate);
app.component('tainacan-form-url', FormURL);
/* Filter Metadata Option forms */
app.component('tainacan-filter-form-numeric', FormFilterNumeric);

View File

@ -304,6 +304,24 @@ html.is-clipped .page-container-small:not(.is-filters-menu-open) {
.taginput-container .input:hover {
border: none !important;
}
.metadata-type-ainacan_url_plugin_metadata_type .multivalue-separator,
.metadata-type-tainacan_url_plugin_metadata_type .multivalue-separator {
display: block;
max-height: 1px;
width: 80px;
background: var(--tainacan-gray3, #a5a5a5);
content: none;
color: transparent !important;
margin: 1em auto 1em 0 !important;
}
.metadata-type-ainacan_url_plugin_metadata_type .wp-block-buttons,
.metadata-type-tainacan_url_plugin_metadata_type .wp-block-buttons {
display: flex !important;
}
.metadata-type-ainacan_url_plugin_metadata_type .wp-block-buttons>.wp-block-button,
.metadata-type-tainacan_url_plugin_metadata_type .wp-block-buttons>.wp-block-button {
width: auto !important;
}
// Buefy notices (toast, snackbar...)
.notices {

View File

@ -38,6 +38,7 @@ import TainacanTaxonomy from '../../../admin/components/metadata-types/taxonomy/
import TainacanCompound from '../../../admin/components/metadata-types/compound/TainacanCompound.vue';
import TainacanUser from '../../../admin/components/metadata-types/user/TainacanUser.vue';
import TainacanGeoCoordinate from '../../../admin/components/metadata-types/geocoordinate/TainacanGeoCoordinate.vue';
import TainacanURL from '../../../admin/components/metadata-types/url/TainacanURL.vue';
// Main components
import ItemSubmissionForm from './theme.vue';
@ -176,6 +177,7 @@ export default (element) => {
VueItemSubmission.component('tainacan-compound', TainacanCompound);
VueItemSubmission.component('tainacan-user', TainacanUser);
VueItemSubmission.component('tainacan-geocoordinate', TainacanGeoCoordinate);
VueItemSubmission.component('tainacan-url', TainacanURL);
/* Others */
VueItemSubmission.component('tainacan-form-item', TainacanFormItem);

View File

@ -706,6 +706,7 @@ return apply_filters( 'tainacan-i18n', [
'label_item_edition_form_options' => __( 'Item edition form options', 'tainacan' ),
'label_item_submission_options' => __( 'Item submission options', 'tainacan' ),
'label_metadata_related_features' => __( 'Metadata related features', 'tainacan' ),
'label_preview' => __( 'Preview', 'tainacan' ),
// Instructions. More complex sentences to guide user and placeholders
'instruction_delete_selected_collections' => __( 'Delete selected collections', 'tainacan' ),