Input Text auto height (dynamic textarea)
This commit is contained in:
parent
86694f41e8
commit
7a1fac7a36
9 changed files with 842 additions and 674 deletions
|
|
@ -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
1285
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
97
resources/ts/components/bo/InputTextAutoComponent.vue
Normal file
97
resources/ts/components/bo/InputTextAutoComponent.vue
Normal 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>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
<div class="wordCounter">
|
||||
<span class="words"
|
||||
><u>{{ __("Mots") }}</u
|
||||
> : <b>{{ words }}</b></span
|
||||
> : <b>{{ _words }}</b></span
|
||||
>
|
||||
<span class="chars"
|
||||
><u>{{ __("Caractères") }}</u
|
||||
> : <b>{{ chars }}</b></span
|
||||
> : <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(" ");
|
||||
|
|
|
|||
5
resources/ts/components/shims-vue.d.ts
vendored
Normal file
5
resources/ts/components/shims-vue.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue