Input Text auto height (dynamic textarea)

This commit is contained in:
Jérémy Munsch 2022-06-27 15:02:26 +02:00
parent 86694f41e8
commit 7a1fac7a36
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
9 changed files with 842 additions and 674 deletions

View file

@ -44,7 +44,7 @@ class StoreNewsRequest extends FormRequest
return [
'title' => 'required|min:6|max:255',
'slug' => 'required|max:255',
'seo_title' => 'required|min:53|max:60',
'seo_title' => 'required|min:45|max:70',
'seo_description' => 'required|min:50|max:160',
'content' => 'required|string|min:15',
'picture' => 'sometimes|image|dimensions:width=300,height=300|mimes:png,jpg,jpeg,gif',

1285
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -27,7 +27,7 @@ export default defineComponent({
}
},
mounted() {
const json = String(this.$attrs.json ?? ""),
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
this.name = data.name;
this.internalText = data.text;

View file

@ -539,25 +539,25 @@ export default defineComponent({
},
reset() {
if (!(this._cropper instanceof Cropper)) {
throw new Error("cropped not initialized");
return;
}
this._cropper.reset();
},
zoom(v: number) {
if (!(this._cropper instanceof Cropper)) {
throw new Error("cropped not initialized");
return;
}
this._cropper.zoom(v);
},
move(x: number, y: number) {
if (!(this._cropper instanceof Cropper)) {
throw new Error("cropped not initialized");
return;
}
this._cropper.move(x, y);
},
scale(horizontal: boolean) {
if (!(this._cropper instanceof Cropper)) {
throw new Error("cropped not initialized");
return;
}
if (horizontal) {
this._scaleX = -1 / this._scaleX;
@ -691,7 +691,7 @@ export default defineComponent({
actualImage = this.$refs.actualImage as HTMLInputElement,
container = new DataTransfer();
if (!(self._cropper instanceof Cropper)) {
throw new Error("cropped not initialized");
return;
}
if (!actualImage.files) {
throw new Error();

View file

@ -0,0 +1,97 @@
<template>
<WordCounter :id="_id" />
<textarea
ref="textarea"
:id="_id"
:class="`inputTextAutoComponent ${_class}`"
:name="_name"
:aria-describedby="_ariaDescribedby"
:placeholder="_placeholder"
:required="_required"
v-on:input="onInput"
:json="'{}'"
>{{ _value }}</textarea
>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from "vue";
import trans from "../../modules/trans";
import WordCounter from "./WordCounterComponent.vue";
const textarea = ref<HTMLTextAreaElement | null>(null);
export default defineComponent({
inheritAttrs: false,
mixins: [trans],
components: {
WordCounter,
},
setup() {
return { textarea };
},
beforeMount() {
if (!this.$attrs.json) {
throw new Error("this component requires json");
}
},
mounted() {
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
this._id = String(data.id);
this._class = String(data.class);
this._name = String(data.name);
this._placeholder = String(data.placeholder);
this._ariaDescribedby = String(data.ariaDescribedby);
this._value = String(data.value);
if (data.required) {
this._required = true;
}
if (this.textarea) {
// Default input init
this.textarea.setAttribute("style", `height: 1rem;overflow-y: hidden;`);
}
// * Init input
this.$nextTick(this.onInput);
},
data(): {
_id: string;
_class: string;
_name: string;
_placeholder: string;
_ariaDescribedby: string;
_required: boolean;
_value: string;
_textarea: HTMLTextAreaElement | null;
} {
return {
_id: "",
_class: "",
_name: "",
_placeholder: "",
_ariaDescribedby: "",
_required: false,
_value: "",
_textarea: null,
};
},
methods: {
onInput() {
const el = textarea.value;
if (!el) {
throw new Error("undefined textarea element");
}
// * Remove every new line char
el.value = el.value.replace(/[\n\r]/, "");
el.style.height = "auto";
el.style.height = `${el.scrollHeight}px`;
},
},
});
</script>
<style lang="scss">
.inputTextAutoComponent {
resize: none;
}
</style>

View file

@ -35,7 +35,7 @@ export default defineComponent({
}
},
mounted() {
const json = String(this.$attrs.json ?? ""),
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
this.name = data.name;
this.tags = this.parseTags(data.value);

View file

@ -2,11 +2,11 @@
<div class="wordCounter">
<span class="words"
><u>{{ __("Mots") }}</u
>&nbsp;:&nbsp;<b>{{ words }}</b></span
>&nbsp;:&nbsp;<b>{{ _words }}</b></span
>
<span class="chars"
><u>{{ __("Caractères") }}</u
>&nbsp;:&nbsp;<b>{{ chars }}</b></span
>&nbsp;:&nbsp;<b>{{ _chars }}</b></span
>
</div>
</template>
@ -17,63 +17,81 @@ import trans from "../../modules/trans";
export default defineComponent({
mixins: [trans],
beforeMount() {
if (!this.$attrs.json) {
throw new Error("this component requires json");
mounted() {
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
this._id = String(this.id);
if (data.id) {
this._id = data.id;
}
},
mounted() {
const json = String(this.$attrs.json ?? ""),
data = JSON.parse(json);
this.id = data.id;
props: {
id: {
type: String,
default: null,
},
},
data(): {
id: string;
words: number;
chars: number;
input: HTMLInputElement | null;
_id: string;
_words: number;
_chars: number;
_input: HTMLInputElement | HTMLTextAreaElement | null;
} {
return {
id: "",
words: 0,
chars: 0,
input: null,
_id: "",
_words: 0,
_chars: 0,
_input: null,
};
},
watch: {
id() {
if (!this.id) {
throw new Error("Missing ID");
}
const input = document.getElementById(this.id);
if (!input || !(input instanceof HTMLInputElement)) {
throw new Error(`Input ${this.id} could not be found`);
}
this.input = input;
this._id = this.id;
},
input() {
if (!this.input) {
throw new Error("input has to be set");
_id() {
if (!this._id) {
return;
}
if (this.input && !(this.input instanceof HTMLInputElement)) {
throw new Error("input as to be HTMLInputElement");
this.$nextTick(() => {
const input = document.getElementById(this._id);
if (
!input ||
!(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)
) {
throw new Error(`${this._id} is not a input or textarea`);
}
this._input = input;
});
},
_input() {
if (
!this._input ||
!(
this._input instanceof HTMLInputElement ||
this._input instanceof HTMLTextAreaElement
)
) {
throw new Error(`${this._id} is not a input or textarea`);
}
this.input.addEventListener("input", (e) => {
this._input.addEventListener("input", (e) => {
const el = e.target;
if (!el || !(el instanceof HTMLInputElement)) {
throw new Error();
if (
!el ||
!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)
) {
throw new Error(`${this._id} is not a input or textarea`);
}
this.updateValues(el.value);
});
this.updateValues(this.input.value);
this.updateValues(this._input.value);
},
},
methods: {
updateValues(string: string) {
let chars, words;
[words, chars] = this.calculateValues(string);
this.words = words;
this.chars = chars;
this._words = words;
this._chars = chars;
},
calculateValues(string: string) {
const arr = string.split(" ");

View file

@ -0,0 +1,5 @@
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

View file

@ -44,18 +44,29 @@
<label for="newsModelSeoTitle" class="col-form-label"><b>{{ __('Titre seo') }}</b></label>
<div class="word-counter" data-json='@json(['id' => 'newsModelSeoTitle'])'></div>
<input name="seo_title" type="text" value="{{ old('seo_title', $newsModel->seo_title ?? '') }}" class="form-control"
id="newsModelSeoTitle" aria-describedby="newsModelSeoTitleHelp" placeholder="{{ __('Un titre en 60 caractères maximum') }}" required>
id="newsModelSeoTitle" aria-describedby="newsModelSeoTitleHelp" placeholder="{{ __('Un titre en 70 caractères maximum') }}" required>
@include('back.modules.input-error', [
'inputName' => 'seo_title', 'helperId' => 'newsModelSeoTitleHelp' , 'helper' => __('Doit décrire précisément l\'article, requiert entre 53 et 60 caratères, balise <title>')
'inputName' => 'seo_title', 'helperId' => 'newsModelSeoTitleHelp' , 'helper' => __('Doit décrire précisément l\'article, recommandation pour un positionnement optimal dans les moteurs de recherche : entre 53 et 60 caractères, balise <title> ')
])
</div>
<div class="col-12 col-md-6 form-group">
<label for="newsModelSeoDescription" class="col-form-label"><b>{{ __('Description seo') }}</b></label>
<div class="word-counter" data-json='@json(['id' => 'newsModelSeoDescription'])'></div>
<input name="seo_description" type="text" value="{{ old('seo_description', $newsModel->seo_description ?? '') }}" class="form-control"
id="newsModelSeoDescription" aria-describedby="newsModelSeoDescriptionHelp" placeholder="{{ __('Une description de l\'article pour les moteurs de recherche') }}" required>
@php
$data = [
'id' => 'newsModelSeoDescription',
'class' => 'form-control',
'name' => 'seo_description',
'placeholder' => __('Une description de l\'article pour les moteurs de recherche'),
'ariaDescribedby' => 'newsModelSeoDescriptionHelp',
'value' => old('tags', $newsModel->tags ?? []),
'tags' => $tags,
'required' => true,
'value' => old('seo_description', $newsModel->seo_description ?? '')
];
@endphp
<div id="input-text-auto" data-json='@json($data)'></div>
@include('back.modules.input-error', [
'inputName' => 'seo_description', 'helperId' => 'newsModelSeoDescriptionHelp' , 'helper' => __('Doit décrire précisément l\'article, requiert entre 50 et 160 caratères, balise <meta> keywords')
'inputName' => 'seo_description', 'helperId' => 'newsModelSeoDescriptionHelp' , 'helper' => __(' Doit décrire précisément l\'article, requiert entre 50 et 160 caractères, balise <meta> description')
])
</div>
</div>