I am implementing a customized dropdown becuase of the requirements we have, using Vue 2 and typescript (jquery is not an option).
It is working fine, when you click on the main box, it opens the options list downwards.
An improvement I am looking for is that, when at the end of the screen, the options list adds to the page height and thus causing the scrollbar to appear or increase scroll height.
What I am looking for is that, when popping up the div, if there's not enough space at the bottom of the screen, open it upwards instead. How do I achieve this?
(classes are using bootstrat 5)
Opened dropdown &
Closed dropdown
My code:
import Vue, {
PropType
} from 'vue';
import {
Validation
} from 'vuelidate';
let uidc = 0;
export default Vue.extend({
name: 'BaseDropdown',
props: {
value: {
type: [Number, String, Object],
default: () => ''
as string,
},
target: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
valueIsNumber: {
type: Boolean,
default: false,
},
options: {
type: Array,
default: null,
},
placeholder: {
type: String,
default: '',
},
required: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
validations: {
type: Object as PropType < Validation > ,
default: () => ({
$error: false,
$touch: () => undefined,
$params: {},
}) as Validation,
},
error: {
type: Boolean,
default: false,
},
trackEvent: {
type: String,
default: '',
},
trackField: {
type: String,
default: '',
},
trackPublic: {
type: Boolean,
default: false,
},
padLeft: {
type: Boolean,
default: false,
},
enforceBlackColour: {
type: Boolean,
default: false,
},
customStyled: {
type: Boolean,
default: false,
},
borderBottomWarning: {
type: Boolean,
default: false,
},
},
data(): {
selectedItem: any | null;
menuOpen: boolean;
searchText: string | null;
} {
return {
selectedItem: null,
menuOpen: false,
searchText: null,
};
},
mounted() {
const appElement = document.getElementById('app_home');
(appElement as any).addEventListener('click', this.handleDropdownClickOutside);
this.$nextTick(() => {
if (this.value) {
if (this.valueIsNumber) {
this.selectedItem = this.options.find((x: any) => x.value === Number(this.value)) || null;
} else {
this.selectedItem = this.options.find((x: any) => x.value.toString().toLowerCase() === this.value.toString().toLowerCase()) || null;
}
}
});
},
computed: {
v(): Validation | {} {
return this.validations;
},
errorMessage(): string {
// Validation must be cast to any to access validators
return Object.entries((this.v as Validation).$params).find(([k]) => !(this.v as any)[k]) ? .[1].message;
},
optgroups(): any {
return this.options.reduce((acc: any, o: any) => ({ ...acc,
[o.optgroup]: [...(acc[o.optgroup] || []), o]
}), {});
},
isRequired(): boolean {
return this.required !== false;
},
getSelectedItemText(): string | null {
return this.selectedItem ? this.selectedItem.text : this.placeholder || 'Please select an item';
},
filteredItems(): any[] {
const list: any[] = [];
for (let c = 0; c < 10; c += 1) {
list.push({
text: c,
value: c
});
}
// return this.searchText && this.searchText.length > 0 ? this.options.filter((x: any) => x.text.toLowerCase().indexOf(this.searchText!.toLowerCase()) > -1) : this.options;
return list;
},
},
methods: {
openMenu() {
this.menuOpen = !this.menuOpen;
if (this.menuOpen) {
this.searchText = null;
}
},
selectItem(item: any) {
this.selectedItem = item;
this.$emit('input', item.value);
this.menuOpen = false;
},
setSuppliedSelectedItem() {
this.$nextTick(() => {
if (this.value) {
this.selectedItem = this.options.find((x: any) => x.value === this.value) || null;
}
});
},
handleDropdownClickOutside(event: any): void {
const parent = document.getElementById(`select-${(this as any).uid}`);
const isParent = parent !== event.target && parent ? .contains(event.target);
if (!isParent) {
this.menuOpen = false;
// this.closeOpenendMenu();
// this.searchText = '';
}
},
},
beforeCreate() {
// eslint-disable-next-line no-plusplus
(this as any).uid = uidc++;
},
});
.dropdown {
font-size: 0.7rem;
img {
// float: right;
// padding-right: 10px;
// padding-top: 5px;
position: absolute;
top: 40%;
right: 10px;
}
.fade {
opacity: 0.5;
}
.search-box {
.form-control {
font-size: 12px !important;
height: 30px !important;
margin: 0 10px 5px 10px !important;
width: 95% !important;
}
}
.selected-item {
border-radius: 3px;
border: 1px solid #ced4da;
padding: 10px;
.selected-item-text {
text-overflow: ellipsis;
overflow: hidden;
width: 93%;
/* height: 1.2em; */
white-space: nowrap;
}
}
.items {
border: 1px solid rgb(236, 236, 236);
width: 100%;
z-index: 15;
max-height: 300px;
overflow-y: auto;
overflow-x: hidden;
background-color: white;
}
.item {
padding: 10px;
background-color: rgb(240, 240, 240);
cursor: pointer;
&:hover {
background-color: rgb(216, 216, 216);
}
}
}
.hidden {
opacity: 0.2;
}
.disabled {
background-color: #e9ecef;
opacity: 1;
pointer-events: none;
}
<template>
<div class="mt-2" :id="`select-${uid}`">
<label v-show="label" class="mb-2 label-grey" :class="{ 'required': isRequired }" :for="`select-${uid}`">{{ label }}</label>
<div class="dropdown noselect position-relative" :class="{'disabled': disabled}">
<div class="selected-item cursor-pointer" #click="openMenu">
<div class="selected-item-text" :class="{'fade': !selectedItem}">{{getSelectedItemText}}</div>
<img v-if="menuOpen" :src="constants.icons.arrowTop" />
<img v-else :src="constants.icons.arrowDown" />
</div>
<div class="items position-absolute" v-show="menuOpen">
<div v-if="filteredItems && filteredItems.length > 5 || searchText" class="search-box">
<input :size="'sm'" v-model="searchText" />
</div>
<div v-for="item in filteredItems" :key="item.value" #click="selectItem(item)">
<div class="item">
{{item.text}}
</div>
</div>
</div>
</div>
<span v-if="v.$error" class="text-error text-xs font-light">{{ errorMessage }}</span>
</div>
</template>
Suggest to use Floating-ui (well known as Poper)
Floating UI is a low-level library for positioning "floating" elements...intelligently keeping them in view
It's been using widely and cover a lot of edge cases you might encounter when try to create dropdown yourself
You can try with references here
creating-vue-component-dropdown-with-popper-js
floating-vue/dropdown
Related
I am trying to build a component where you can visualize on what section are you in and allows you to easily move sections on the page.
My page is structured like this
<SectionManager /> // the absolutely positioned element
<Navbar />
<Menu />
<Section1 />
<Section2 />
<Section3 />
{...}
I want to have every even numbered section to have a black background and the others to have a white background. Now I want the text inside my SectionManager component to be white when overlapping a black background and black when overlapping a white background.
Here is a photo:
My component is the one on the left. And when you scroll down to the black section I want just the about me text and the circle after that to turn white.
Sorry if this is a stupid question by I searched for hours and did not find anything. I tried mix-blend-mode but it did not work.
Here the code for my component:
const SectionManager: React.FC = () => {
const globalState = React.useContext(MyContext);
const observerCallback = (entries: IntersectionObserverEntry[]) => {
...
};
const observerOptions = React.useMemo(
...
);
React.useEffect(() => {
const observer = new IntersectionObserver(...);
globalState.currentSections.forEach((section: HTMLElement) => {
observer.observe(section);
});
}, []);
const sections = [
{
text: "Hello!",
},
{
text: "about me",
},
{
text: "work i did",
},
{
text: "contact",
},
];
return (
<div className={styles.sectionManager}>
{sections.map((section, sectionID) => (
<>
{sectionID > 0 && (
<div className={styles.sectionManager_separator}></div>
)}
<div
className={
sectionID === globalState.activeSectionId
? `${styles.sectionManager_item} ${styles.sectionManager_itemActive}`
: styles.sectionManager_item
}
>
<p>{section.text}</p>
</div>
</>
))}
</div>
);
};
export default SectionManager;
here is the scss file:
.sectionManager {
position: fixed;
z-index: 100;
right: 30px;
top: 50%;
display: flex;
flex-direction: column;
align-items: flex-end;
transform: translateY(-50%);
&_separator {
width: 1px;
height: 25px;
background: $text-secondary-dark;
margin-right: 7px;
}
&_itemActive {
&::after {
background-color: $text-primary-light !important;
transform: scale(1) !important;
}
p {
color: $text-primary-light !important;
transform: scaleX(1) !important;
}
}
&_item {
mix-blend-mode: difference;
display: flex;
align-items: center;
font-size: 1rem;
margin-top: 5px;
cursor: pointer;
background: transparent;
#include transition();
&:hover {
&::after {
background-color: $text-primary-light;
transform: scale(1);
}
p {
transform: scaleX(1);
color: $text-primary-light;
}
}
p {
margin: 0;
transform: scaleX(0);
transform-origin: right;
color: $text-secondary-light;
#include transition();
}
&::after {
content: "";
width: 15px;
height: 15px;
border-radius: 999999px;
margin-left: 10px;
transform: scale(0.9);
background: $text-secondary-dark;
#include transition();
}
}
}
And for the section background I am not doing anything fancy, I am just setting a background-color property on there.
Thank you in advance!
Edit:
I want something similar to that. The design is in figma.
I solved the issue!
I ended up getting all the sections on my page using querySelector and using an IntersectionObserver to get the section that is in viewPort and get its background color, then passing the background color to my component using data-section-bg.
Here is the whole component code:
const SectionManager: React.FC = () => {
const [currentSectionBg, setCurrentSectionsBg] =
React.useState<string>("#fff");
const globalState = React.useContext(MyContext);
const observerCallback = (entries: IntersectionObserverEntry[]) => {
// other observer ...
};
const sectionColorObserverCallback = (
entries: IntersectionObserverEntry[]
) => {
entries.forEach((entry) => {
if (entry.intersectionRatio > 0.25) {
const sectionBgColor = (
document.getElementById(entry.target.id) as HTMLElement
).style.backgroundColor;
console.log(sectionBgColor);
setCurrentSectionsBg(sectionBgColor);
}
});
};
const observerOptions = React.useMemo(
() => ({
root: null,
rootMargin: "0px",
threshold: 0.25,
}),
[]
);
React.useEffect(() => {
const observer = new IntersectionObserver(
// other observer...
);
globalState.currentSections.forEach((section: HTMLElement) => {
// other observer...
});
// Detect Section Color Observer
const allSections = document.querySelectorAll("section");
if (!allSections) return;
const sectionColorObserver = new IntersectionObserver(
sectionColorObserverCallback,
observerOptions
);
allSections.forEach((section, sectionId) => {
section.id = `SECTION_${sectionId}`;
section.style.backgroundColor = sectionId % 2 === 0 ? "#fff" : "#000";
sectionColorObserver.observe(section);
});
}, []);
const sections = [
{
text: "Hello!",
},
{
text: "about me",
},
{
text: "work i did",
},
{
text: "contact",
},
];
return (
<motion.div
initial={{ x: 150, opacity: 0 }}
animate={{
x: 0,
y: "-50%",
opacity: 1,
transition: {
duration: 1,
delay: 0.8,
ease: defaultAnimationEasing,
},
}}
className={styles.sectionManager}
>
{sections.map((section, sectionID) => (
<>
{sectionID > 0 && (
<div className={styles.sectionManager_separator}></div>
)}
<div
data-section-bg={currentSectionBg}
className={
sectionID === globalState.activeSectionId
? `${styles.sectionManager_item} ${styles.sectionManager_itemActive}`
: styles.sectionManager_item
}
>
<p>{section.text}</p>
</div>
</>
))}
</motion.div>
);
};
export default SectionManager;
Here is what I added to my scss File:
[data-section-bg="rgb(0, 0, 0)"] {
&:hover {
&::after {
background: $text-primary-dark !important;
}
p {
color: $text-primary-dark;
}
}
p {
color: $text-primary-dark;
}
}
[data-section-bg="rgb(255, 255, 255)"] {
&:hover {
&::after {
background: $text-primary-light !important;
}
p {
color: $text-primary-light;
}
}
p {
color: $text-primary-light;
}
}
&_itemActive[data-section-bg="rgb(255, 255, 255)"] {
&::after {
background-color: $text-primary-light !important;
}
}
&_itemActive[data-section-bg="rgb(0, 0, 0)"] {
&::after {
background-color: $text-primary-dark !important;
}
}
I ran into difficulty styling my dropdown : https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#Styling_with_CSS
The select element is notoriously difficult to style productively with CSS.
in summary none of the hacks you can use on the option and select tags are worth their salt even in combination.
I'd love to continue using reactive forms but I wish to purify my html and css by using only <div> tags to draw up and use my dropdown in a reactive form.
is this possible?
here is my code as it exists today.
// this.statusForm = this.fb.group({
// status: ['Delivered', Validators.required]
// });
<form [formGroup]="statusForm">
<select formControlName="status">
<option value="Delivered">Delivered</option><!---->
<option value="Cancelled">Cancelled</option><!---->
<option value="UndeliveredTechnicalissue">Undelivered/Technical issue</option><!---->
</select>
</form>
the js is just the FormBuilder hydration.
I can gather/ console.log() the value using
this.statusForm.value.status;
You create the "options" part as ul>li or div and then style it accordingly.
The trick is to hide/show this part on mouse click or keyboard interaction, but for this you can use a boolean variable (here expanded).
Here a working Stackblitz.
If you want to look at the code in just one page, have a look to the code below:
template
<div class="select-container">
<input type="text"
[id]="customId"
[placeholder]="placeholder"
[value]= "selectedValue"
[disabled]="disabled"
(click)="showOptions()"/>
<ul class="select-menu box" role="listbox" *ngIf="expanded">
<li role="option"
*ngFor="let option of options; let i = index;"
[id]="customId + '-option-' + i"
[title]="option.label"
class="option-item"
[ngClass]="{ 'selected': activeItemIndex === i }"
(click)="selectItem(option)">
<span> {{option.label}}</span>
</li>
</ul>
</div>
component
#Component({
selector: 'form-select',
templateUrl: './form-select.component.html',
styleUrls: ['./form-select.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => FormSelectComponent)
}
]
})
export class FormSelectComponent implements ControlValueAccessor{
public selectedValue = '';
public disabled = false;
public value: string;
#Input()
label: string;
#Input()
formCtrl: AbstractControl;
#Input()
pipe: { type?: string; params?: any };
#Input()
options: {key: string, label: string}[] = [];
#Input()
customId: string;
#Input()
placeholder: string;
public expanded = false;
public activeItemIndex: number;
public onChange(newVal: T) {}
public onTouched(_?: any) {}
public registerOnChange(fn: any): void {
this.onChange = fn;
}
public registerOnTouched(fn: any): void {
this.onTouched = fn;
writeValue(value: string) {
if (value && this.options) {
const match = this.options.find(
(item: { type?: string; params?: any }, index: number) => {
if (item.key === value) {
this.activeItemIndex = index;
return true;
}
}
);
this.selectedValue = match ? match.label : '';
}
}
showOptions() {
if (!this.disabled) {
this.expanded = true;
}
}
selectItem(item: {key: string, label: string}) {
this.value = item.key;
this.expanded = false;
this.selectedValue = item.label;
this.onChange(item.key);
}
}
scss styles
.select-container {
position: relative;
.input-container {
i {
position: absolute;
top: 1rem;
right: 1rem;
}
input[type='text'] {
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding-right: 2rem;
}
}
.select-menu {
width: 100%;
z-index: 100;
max-height: 17.75rem;
overflow: auto;
position: absolute;
top: -5px;
right: 0;
background-color: white;
border: 1px solid gray;
padding: 1rem;
box-sizing: border-box;
.option-item {
padding-left: 1rem;
line-height: 3rem;
color: gray;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin: 0 -1rem;
&:last-child {
margin-bottom: 0;
}
&.selected,
&:hover {
background-color: lightgray;
color: black;
}
&:focus {
outline: none;
}
}
}
}
How to use it:
In template:
<form [formGroup]="mainFormGroup">
<form-select formControlName="myControl" [options]="options">
</form-select>
</form>
in component:
const options = [{
key: key1,
label: 'value_1'
}, {
key: key2,
label: 'value_2'
}];
this.mainFormGroup = this.fb.group({
myControl: ['']
});
I am trying to replicate data in the database for a specific user. It is almost 1000 rows that should be replicated. But my request gets failed with Empty response after 2 min. you can see in the picture. I am not able to identify the issue. Guide me if anyone has already solved this kind of issue.
My database is hosted on Google Cloud Storage and I am using MySQL.
This is my controller method:
public function createDataForManyToMany (Request $request)
{
$rows = TableA::where('id_analysis',
$request->analysis_id)->get();
$files_data = TableB::Where('id_analysis',
$request->analysis_id)->get();
foreach ($rows as $row)
{
$row->input_files()->saveMany($files_data);
}
return response()->json([
'status' => 'ok',
'analysis_id' => $request->analysis_id
]);
}
My Axios post request:
axios.post('/app/create-det-infiles-data', { '_token': this.csrf_token, 'analysis_id': res.data.analysis_id})
.then( res => {
if (res.data.status === 'ok') {
this.signupTried = false
this.emailSent = true
}
Vue Component:
<template>
<div class="fullwidth-container">
<div class="login-container">
<v-card class="login_form elevation-9">
<form class="login">
<input type="hidden" name="_token" :value="csrf_token"/>
<div class="header">
<h1>PIX2MAP SignUp</h1>
</div>
<v-text-field
color="dark"
prepend-icon="person"
:label="$t('name')"
required
:error-messages="nameErrors"
v-model="name">
</v-text-field>
<v-text-field
color="dark"
prepend-icon="email"
label="Email"
required
:error-messages="emailErrors"
v-model="email">
</v-text-field>
<v-text-field
color="dark"
prepend-icon="vpn_key"
:append_icon="show_pass ? 'visibility_off' : 'visibility'"
:type="show_pass ? 'text' : 'password'"
:error-messages="passwordErrors"
required
:label="$t('password')"
v-model="password"
#click:append="show_pass = !show_pass">
</v-text-field>
<v-text-field
class="pwd-confirm"
color="dark"
:append_icon="show_pass ? 'visibility_off' : 'visibility'"
:type="show_pass2 ? 'text' : 'password'"
:error-messages="repeatPasswordErrors"
required
:label="$t('repeat_password')"
v-model="password2"
#click:append="show_pass = !show_pass">
</v-text-field>
<v-layout row style="margin: 0">
<v-flex shrink style="margin-top: 0">
<v-checkbox
v-model="termsAccepted"
:label="`I accept the `"
:error-messages="acceptAgreementError"
>
</v-checkbox>
</v-flex>
<v-flex grow text-xs-left style="margin-top: 0">
contract
</v-flex>
</v-layout>
<v-btn class="btn-signup" color="accent" #click="doSignup"
:disabled="formFilled"
>Confirm <span class="float-right" :class="{'fas':signupTried, 'fa-spinner':signupTried, 'fa-spin':signupTried}"></span>
</v-btn>
<p class="privacy">{{ $t('form_privacy') }}</p>
</form>
</v-card>
<div class="back">
<v-btn flat #click="$emit('back')"><v-icon>arrow_back</v-icon>{{ $t('back_button') }}</v-btn>
</div>
</div>
<v-dialog
v-model="dialog"
max-width="650"
>
<v-card>
<v-card-title class="headline grey lighten-2">{{ contractTitile }}
<span style="margin-left: auto"><v-icon right #click="dialog = !dialog">fas fa-times</v-icon></span>
</v-card-title>
<v-card-text >
{{ contractBody }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-dialog>
<v-snackbar
v-if="signupTried"
v-model="snackbar"
dark
:bottom="y === 'bottom'"
:timeout="timeout"
:left="x === 'left'"
:color="color"
:multi-line="mode === 'multi-line'"
:right="x === 'right'"
:top="y === 'top'"
:vertical="mode === 'vertical'"
>
{{ text }}
</v-snackbar>
<v-snackbar
v-if="emailSent"
v-model="emailSnackbar"
:bottom="y === 'bottom'"
:timeout="timeout"
:left="x === 'left'"
:multi-line="mode === 'multi-line'"
:right="x === 'right'"
:top="y === 'top'"
:color="color"
:vertical="mode === 'vertical'"
>
{{ emailSentMessage }}
</v-snackbar>
</div>
</template>
<script>
import { required, email } from 'vuelidate/lib/validators'
import {TITLE, BODY_TEXT} from '../contract'
import axios from 'axios'
export default {
props: {
csrf_token: String,
name: String,
email: String,
},
data: () => ({
contractTitile: TITLE,
contractBody: BODY_TEXT,
show_pass: false,
show_pass2: false,
name: '',
email: '',
password: '',
password2: '',
signupTried: false,
emailExist: false,
dialog: false,
termsAccepted: false,
snackbar: true,
y: 'bottom',
x: null,
mode: '',
timeout: 10000,
color: '#fff',
text: 'This process will take some time, please! be patience.',
emailSent: false,
emailSnackbar: true,
emailSentMessage: 'We have sent you instructions, please verify your account by visiting your email.'
}),
validations: {
name: {
required
},
email: {
required,
email
},
password: {
required
},
password2: {
required
},
termsAccepted: {
required
}
},
methods: {
async doSignup () {
this.signupTried = true
if (this.$v.name.required && this.$v.email.required && this.$v.email.email && this.$v.password.required && this.termsAccepted ) {
axios.post('/app/register', { '_token': this.csrf_token, 'name': this.name,'email': this.email, 'password': this.password })
.then(res => {
if (res.data.status === 'ok') {
axios.post('/app/create-in-files-data', { '_token': this.csrf_token, 'analysisIds': res.data.analysis_ids})
.then(resp => {
if (resp.data.status === 'ok') {
axios.post('/app/create-mo-files-data', { '_token': this.csrf_token, 'analysisIds': resp.data.analysis_ids})
.then( res => {
if (res.data.status === 'ok') {
// window.location = '/'
// this.signupTried = false
// this.emailSent = true
axios.post('/app/create-det-infiles-data', { '_token': this.csrf_token, 'analysis_id': res.data.analysis_id})
.then( res => {
if (res.data.status === 'ok') {
this.signupTried = false
this.emailSent = true
// await axios.post('/app/create-det-mofiles-data', { '_token': this.csrf_token, 'analysis_id': res.data.analysis_id, demo: true})
// .then(async res => {
// if (res.data.status === 'ok') {
// this.showSpinner = false
// window.location = '/'
// } else {
// console.log('Files not created')
// this.showSpinner = false
// this.$emit('forgotPw', {email: this.email});
// }
// })
// .catch(e => {
// console.log('2nd error', e)
// })
} else {
console.log('Files not created')
this.showSpinner = false
this.$emit('forgotPw', {email: this.email});
}
})
.catch(e => {
console.log('2nd error', e)
})
} else {
console.log('Signup Failed')
this.signupTried = false
this.$emit('forgotPw', {email: this.email});
}
})
.catch(e => {
console.log('2nd error', e)
})
} else {
console.log('Files not created')
this.showSpinner = false
this.$emit('forgotPw', {email: this.email});
}
})
.catch(e => {
console.log('Error: ', e)
})
} else {
this.showSpinner = false
this.$emit('forgotPw', {email: this.email});
}
})
}
},
changeState () {
this.dialog = !this.dialog
}
},
computed: {
nameErrors () {
if (this.signupTried && this.name.length == 0 && !this.$v.name.required) return [this.$t('name_required')]
},
emailErrors () {
const errors = []
if (this.signupTried && !this.$v.email.required) errors.push(this.$t('email_required'))
if (!this.$v.email.email) errors.push(this.$t('not_valid_email'))
if (this.emailExist) errors.push(this.$t('existing_email'))
return errors
},
passwordErrors () {
if (this.signupTried && !this.$v.password.required) return [this.$t('password_required')]
},
repeatPasswordErrors () {
if (this.signupTried && !this.$v.password2.required) return [this.$t('password_required')]
if (this.password2 != this.password) return [this.$t('passwords_not_equal')]
},
acceptAgreementError () {
if (this.signupTried && !this.$v.termsAccepted.required) return [this.$t('agreement_required')]
},
formFilled () {
if (!this.$v.name.required || !this.$v.email.required || !this.$v.password.required || !this.$v.password2.required || !this.termsAccepted || (this.password != this.password2))
{
return true
} else {
return false
}
}
}
}
</script>
<style scoped lang="stylus">
#import '../../stylus/theme.styl'
.fullwidth-container {
width: 100%;
height: 100%;
background: $bg-login;
position: absolute;
top: 0px;
left: 0px;
z-index: 2;
.login-container {
position: relative;
width: 500px;
top: 25px;
margin: auto;
.login_form {
padding: 16pt;
text-align: center;
height: 800px;
* {
margin-top: 24pt;
}
.header {
margin: 24pt;
}
.i-am {
display: flex;
flex-direction: row;
align-items: center;
* {
margin-top: 0pt;
}
span {
width: 8%;
}
.i-am-text {
margin-right: 20px;
}
.i-am-select {
margin-left: 5%;
}
}
.btn-signup {
margin-left: auto;
margin-right: auto;
width: 80%;
}
.privacy {
margin-top: 8pt;
font-style: italic;
}
.pwd-confirm {
margin-left: 7%;
}
.checkboxAccept {
display: inline;
}
.float-right {
margin-top: -3px;
margin-left: 5px;
color: #fff;
font-size: 18px;
}
}
}
}
</style>
This issue might be caused due to MySQL instance is reaching the time out limit for your query.
In Cloud SQL you can try to modify the flag “wait_timeout” and set a bigger value, for example 50000.
Check this link as a reference to set a flag in Cloud SQL instance.
I am create custom Gutenberg editor block to set YouTube video Id to <button> atribute data-video=""
UPDATE - Added working code to the below
My function.php code
// Include Css and js block files
function designa_youtube_block() {
// Scripts
wp_register_script(
'youtube-block-script', // Handle.
'/wp-content/themes/twentyfifteen/init/block.js',
array( 'wp-blocks', 'wp-element', 'wp-i18n' ),
time()
);
// Styles
wp_register_style(
'youtube-block-editor-style', // Handle.
'/wp-content/themes/twentyfifteen/init/editor.css',
array( 'wp-edit-blocks' ),
time()
);
// Register the block with WP using our namespacing
register_block_type( 'designa/youtube-block', array(
'editor_script' => 'youtube-block-script',
'editor_style' => 'youtube-block-editor-style',
) );
}
add_action( 'init', 'designa_youtube_block' );
My block.js code
( function( editor, components, element ) {
const el = element.createElement;
const registerBlockType = wp.blocks.registerBlockType;
const RichText = wp.editor.RichText;
const BlockControls = wp.editor.BlockControls;
const InspectorControls = wp.editor.InspectorControls;
const AlignmentToolbar = wp.editor.AlignmentToolbar;
const UrlInput = wp.editor.URLInput;
function getVideoId(url) {
if (url) {
let match = url.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/);
if (match && match[2].length == 11)
return match[2];
else
return false;
} else {
return false;
}
}
registerBlockType( 'designa/youtube-block', {
title: 'YouTube',
description: 'Видео в всплывающем окне.',
icon: 'video-alt3',
category: 'common',
attributes: {
content: {
type: 'string',
},
url: {
type: 'string',
},
alignment: {
type: 'string',
},
},
edit: function( props, isSelected ) {
let attributes = props.attributes,
url = props.attributes.url,
alignment = props.attributes.alignment;
function onChangeAlignment( newAlignment ) {
props.setAttributes( { alignment: newAlignment } );
}
if (url)
videoTitle();
async function videoTitle(){
let id = getVideoId(url);
const response = await fetch( `https://www.googleapis.com/youtube/v3/videos?id=${ id }&key=AIzaSyBUqaVKkqdXzPQnfuuP8VPa-yqOQlJwV-w&fields=items(snippet(title))&part=snippet`, {
cache: 'no-cache',
headers: {
'user-agent': 'WP Block',
'content-type': 'application/json'
},
method: 'GET',
redirect: 'follow',
referrer: 'no-referrer',
})
.then(
returned => {
if (returned.ok) return returned;
throw new Error('Network response was not ok.');
}
);
let data = await response.json();
props.setAttributes( { title: data.items[0].snippet.title} );
};
return [
el( BlockControls, { key: 'controls' },
el( AlignmentToolbar, {
value: alignment,
onChange: onChangeAlignment,
} )
),
el( InspectorControls, { key: 'inspector' },
el( components.PanelBody, {
title: 'Превью видео',
className: 'youtube-preview-block',
initialOpen: true,
},
el( 'div', {
className: 'youtube-preview',
},
el( 'iframe', {
frameborder: '0',
allowfullscreen: 'allowfullscreen',
src: url ? 'https://www.youtube.com/embed/'+getVideoId(url)+'?rel=0&showinfo=0' : ''
},
),
),
el( 'div', { className: !props.attributes.title ? 'sceleton-youtube-title' : '' },
props.attributes.title ? props.attributes.title : ''
),
),
),
el( 'div', { className: alignment ? props.className+' justify-content-'+alignment : props.className },
el( 'svg', { className: 'play-icon', width: '50', height: '50', viewBox: '0 0 70 70' },
el( 'path', { fill: '#f6155e', d: "M35 70C54.33 70 70 54.33 70 35C70 15.67 54.33 0 35 0C15.67 0 0 15.67 0 35C0 54.33 15.67 70 35 70Z" } ),
el( 'path', { fill: '#fff', d: "M48.2988 35L28.6988 44.1L28.6988 25.9L48.2988 35Z" } ),
),
el( 'div', { className: 'youtube-wrap' },
el( RichText, {
tagName: 'div',
placeholder: 'Введите текст кнопки',
keepPlaceholderOnFocus: true,
isSelected: false,
value: props.attributes.content,
onChange: function( content ) {
props.setAttributes( {
content: content,
} );
}
}
),
el( 'div', { className: 'youtube-link-wrap' },
el( 'svg', { className: 'dashicon dashicons-admin-links', width: '20', height: '20' },
el( 'path', { d: "M17.74 2.76c1.68 1.69 1.68 4.41 0 6.1l-1.53 1.52c-1.12 1.12-2.7 1.47-4.14 1.09l2.62-2.61.76-.77.76-.76c.84-.84.84-2.2 0-3.04-.84-.85-2.2-.85-3.04 0l-.77.76-3.38 3.38c-.37-1.44-.02-3.02 1.1-4.14l1.52-1.53c1.69-1.68 4.42-1.68 6.1 0zM8.59 13.43l5.34-5.34c.42-.42.42-1.1 0-1.52-.44-.43-1.13-.39-1.53 0l-5.33 5.34c-.42.42-.42 1.1 0 1.52.44.43 1.13.39 1.52 0zm-.76 2.29l4.14-4.15c.38 1.44.03 3.02-1.09 4.14l-1.52 1.53c-1.69 1.68-4.41 1.68-6.1 0-1.68-1.68-1.68-4.42 0-6.1l1.53-1.52c1.12-1.12 2.7-1.47 4.14-1.1l-4.14 4.15c-.85.84-.85 2.2 0 3.05.84.84 2.2.84 3.04 0z" } ),
),
el( UrlInput, {
tagName: 'div',
value: url,
autoFocus: false,
onChange: function( url ) {
props.setAttributes({
url: url,
title: ''
});
}
}
),
),
),
),
];
},
save: function( props ) {
let attributes = props.attributes;
return (
el( 'div', {
className: (attributes.alignment) ? 'justify-content-'+attributes.alignment : '',
},
el( 'button', {
className: 'video-btn',
'data-video': getVideoId(attributes.url),
}, attributes.content,
),
)
);
},
} );
} )(
window.wp.editor,
window.wp.components,
window.wp.element,
);
My editor.css code
.youtube-wrap {
width: calc(80% - 70px);
}
.wp-block-designa-youtube-block {
display: flex;
align-items: center;
}
.youtube-link-wrap {
display: flex;
align-items: center;
}
.youtube-link-wrap svg {
margin-right: 10px;
fill: #8f98a1;
}
.youtube-wrap {
padding: 0 20px;
}
.youtube-preview {
position: relative;
padding-bottom: 56.25%;
height: 0;
background: #e1e9ee;
margin-bottom: 14px;
}
.gutenberg__editor .youtube-preview iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.justify-content-center {
justify-content: center;
}
.justify-content-left {
justify-content: flex-start;
}
.justify-content-right {
justify-content: flex-end;
}
.sceleton-youtube-title:before, .sceleton-youtube-title:after {
content: "";
display: block;
height: 18px;
width: 100%;
background-color: #f0f4f6;
margin-bottom: 10px;
}
.sceleton-youtube-title:after {
width: 40%;
}
.components-panel__body.youtube-preview-block {
font-weight: bold;
color: #5a5e61;
letter-spacing: 0.015em;
line-height: 22px;
}
In back end I got this result:
Everything works fine, but now I need to get the title and duration of the video from YouTube by ID getId(attributes.url) and insert to InspectorControls block.
If I try to get the title and duration using Ajax, first render all block fields to InspectorControls, and then execute my ajax request. I need something like that on ajax successful push( el( 'p', {}, '**YouTube Title**') )
Tell me how to add the information obtained with Ajax to InspectorControls after rendering and on change?
I am using Highcharts to plot the graph of temperature vs time. I am having a JSON file wherein data from the backend keep updates the JSON file. I want to call ajax function such that the graphs automatically generates with respect to time. How to do that? I am new to high charts, please help me.
You can use Series.addPoint method.
http://api.highcharts.com/highcharts/Series.addPoint
Here is a example of using Highcharts with live data with GET HTTP requests.
const options = {
chart: {
type: 'spline'
},
title: {
text: 'Live Bitcoin Price'
},
xAxis: {
type: 'datetime'
},
yAxis: {
title: {
text: 'Price (USD)'
}
},
legend: {
enabled: false
},
exporting: {
enabled: false
},
series: [{
name: 'Live Bitcoint Price [USD]',
data: []
}]
}
const chart = Highcharts.chart('container', options)
// Data
const getData = () => {
setInterval(() => {
window.fetch('https://api.cryptonator.com/api/ticker/btc-usd').then((response) => {
return response.json()
}).then((data) => {
chart.series[0].addPoint({ x: data.timestamp * 1000, y: Number(data.ticker.price) })
})
}, 3000)
}
getData()
#import 'https://code.highcharts.com/css/highcharts.css';
.highcharts-background {
fill: #222;
}
.highcharts-title,
.highcharts-axis-title {
fill: #DDD;
}
.highcharts-credits,
.highcharts-credits:hover {
fill: #222;
}
body {
background-color: #222;
margin 0 !important;
}
#container {
margin: 0;
padding: 0;
border: 0;
background-color: #222;
min-height: 400px;
height:95%;
width:95%;
position:absolute;
}
<script src="https://code.highcharts.com/highcharts.js"></script>
<div id="container"></div>
Live example:
https://jsfiddle.net/xpfkx91w/