Merge branch 'issues' into 'develop'

Issues

See merge request ldc-web/projets/volena!11
This commit is contained in:
Vincent BRIDIER 2024-06-26 10:22:23 +00:00
commit 3a1a7e5fce
30 changed files with 1496 additions and 908 deletions

View file

@ -7,6 +7,7 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Sections\UpdateSectionRequest;
use App\Models\Section;
use App\Models\StaticPage;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
@ -88,7 +89,7 @@ class SectionController extends Controller
* @param \App\Models\Section $section
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateSectionRequest $request, Section $section): \Illuminate\Http\RedirectResponse
public function update(UpdateSectionRequest $request, Section $section): RedirectResponse
{
return DB::transaction(function () use ($request, $section) {
$section->fill(array_replace_recursive(
@ -109,4 +110,38 @@ class SectionController extends Controller
]));
});
}
/**
* Section add PartPiece
*
* @param \App\Models\Section $section
* @return \Illuminate\Http\RedirectResponse
*/
public function addPartPiece(Section $section): RedirectResponse
{
$section->addPartPiece();
$section->saveOrFail();
return response()->redirectToRoute('bo.sections.edit', $section)
->with('success', trans('Partie ajouté'));
}
/**
* Section remove PartPiece
*
* @param \App\Models\Section $section
* @param integer $index
* @return \Illuminate\Http\RedirectResponse
*/
public function removePartPiece(Section $section, int $index): RedirectResponse
{
$count = $section->removePartPiece($index);
$section->saveOrFail();
$response = response()->redirectToRoute('bo.sections.edit', $section);
if ($count) {
$response->with('error', trans('Partie enlevé'));
} else {
$response->with('info', trans('Aucune partie enlevé'));
}
return $response;
}
}

View file

@ -28,7 +28,7 @@ class RedirectIfAuthenticated
return \redirect()->route('fo.home.show');
case 'backend':
if (!$request->routeIs('bo.*')) {
continue;
continue 2;
}
return \redirect()->route('bo.dashboard');
default:

View file

@ -133,15 +133,10 @@ abstract class UpdateMiniCrudRequest extends FormRequest
case InputType::url->value:
$rules = array_merge($rules, [
"data.$iptName.value" => [
'required', 'url',
$this->compileValidate('url', $input),
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
function ($attribute, $value, $fail) {
try {
ToolboxHelper::assertNoInternalUrls($attribute, $value);
ToolboxHelper::assertNoInternalBoUrls($attribute, $value);
} catch (ValidationException $e) {
$fail(collect($e->errors())->first()[0]);
}
$this->validateUrl($attribute, $value, $fail);
}
]
]);
@ -184,18 +179,32 @@ abstract class UpdateMiniCrudRequest extends FormRequest
}
}
$test = \collect($inputTypeValidate)->all();
$test = \collect($inputTypeValidate)->sort(function ($valueA, $valueB) {
$valueA = $this->ruleHasToComeFirst($valueA) ? 0 : 2;
$valueB = $this->ruleHasToComeFirst($valueB) ? 1 : 2;
return $valueA - $valueB;
})->unique()->all();
return \collect($inputTypeValidate)->unique()->sort(function ($valueA, $valueB) {
$valueA = $this->ruleHasToComeFirst($valueA) ? 0 : 2;
$valueB = $this->ruleHasToComeFirst($valueB) ? 1 : 2;
return $valueA - $valueB;
})->all();
})->values()->all();
}
/**
* Validate URl
*
* @param string $attribute
* @param mixed $url
* @param \Closure $fail
* @return void
*/
private function validateUrl(string $attribute, mixed $url, \Closure $fail): void
{
try {
if (!is_null($url)) {
$url = is_string($url) ? $url : '';
ToolboxHelper::assertNoInternalUrls($attribute, $url);
ToolboxHelper::assertNoInternalBoUrls($attribute, $url);
}
} catch (ValidationException $e) {
$fail(collect($e->errors())->first()[0]);
}
}
/**

View file

@ -17,7 +17,7 @@ use Illuminate\Support\Str;
* @property \App\Enums\Sections\PageSectionEnum $section Slider that contains the slide.
* @property string $h2_title Title.
* @property string $text WYSIWYG - Content.
* @property string $data Minicrud content.
* @property array $data Minicrud content.
* @property-read \Carbon\Carbon $created_at Created date.
* @property-read \Carbon\Carbon $updated_at Updated date.
*
@ -53,7 +53,7 @@ class Section extends Model
*/
protected $casts = [
'section' => PageSectionEnum::class,
'data' => 'object',
'data' => 'array',
];
/**
@ -138,7 +138,7 @@ class Section extends Model
bool $required = false,
string $name = "URL à ouvrir",
): array {
$data["bloc_link"] = [
$data[sprintf('bloc_%s', Str::of($name)->snake()->ascii()->remove(['\'', '-']))] = [
'label' => $name,
'inputType' => InputType::url->value,
'value' => $url,
@ -265,6 +265,160 @@ class Section extends Model
return $data;
}
/**
* Add part piece
*
* @return void
*/
public function addPartPiece(): void
{
$pieceTemplateLinesCount = self::countPartPieces();
$pieceToAdd = self::getAddablePartPieceBlock(
$this->section,
$this->countPartPieces() + 1
);
$linesToSkip = 1;
$data = collect($this->data);
$groupedPieces = $data->skip($linesToSkip)->splitIn($data->count() / ($pieceTemplateLinesCount ?: 1))
->concat([$pieceToAdd])
->sort(function ($aCollect, $bCollect) {
if ($this->section->equals(PageSectionEnum::produce_rhd)) {
return strcmp(
strrev(collect(collect($aCollect)->first())->first()),
strrev(collect(collect($bCollect)->first())->first())
);
}
return 0;
});
$groupedPieces->prepend([$data->keys()->first() => $data->first()]);
$this->data = $groupedPieces->collapse()->all();
}
/**
* Remove part piece
*
* @param integer $index
* @return integer Elements count removed.
*/
public function removePartPiece(int $index): int
{
$pieceTemplate = self::getPartPieces();
$pieceTemplateLinesCount = self::countPartPieces();
if (!$pieceTemplateLinesCount) {
return 0;
}
$linesToSkip = 1;
$data = collect($this->data);
$groupedPieces = $data->skip($linesToSkip)->splitIn($data->count() / count($pieceTemplate));
$removedGroup = $groupedPieces->splice($index - 1, 1);
$this->data = $groupedPieces->collapse()
->prepend($data->first(), $data->keys()->first())
->all();
return $removedGroup->count();
}
/**
* Count part pieces
*
* @return integer
*/
public function countPartPieces(): int
{
$pieceTemplate = self::getPartPieces();
$data = collect($this->data);
$templateKeys = collect($pieceTemplate)->keys();
$dataCount = $data->count();
$repetitionCount = 0;
for ($i = 1; $i <= $dataCount; $i++) {
$indexKeys = $templateKeys->map(fn ($key) => str_replace('__index', strval($i), $key));
if ($indexKeys->every(fn ($key) => $data->has($key))) {
$repetitionCount++;
}
}
return $repetitionCount;
}
/**
* Get part pieces
*
* @return array
*/
private function getPartPieces(): array
{
$pieceTemplate = self::getAddablePartPieceBlockTemplate($this->section);
return is_null($pieceTemplate) ? [] : $pieceTemplate;
}
/**
* Addable part piece block
*
* @param \App\Enums\Sections\PageSectionEnum $sectionEnum
* @param integer $index
* @return array
*/
public static function getAddablePartPieceBlock(PageSectionEnum $sectionEnum, int $index = 1): array|null
{
$data = null;
$pieceTemplate = self::getAddablePartPieceBlockTemplate($sectionEnum);
if (!is_null($pieceTemplate)) {
$data = json_decode(Str::of(json_encode($pieceTemplate))->replace('__index', strval($index)), true);
/** @var array $data */
}
return $data;
}
/**
* Addable part piece block
*
* @param \App\Enums\Sections\PageSectionEnum $sectionEnum
* @return array
*/
public static function getAddablePartPieceBlockIndexes(PageSectionEnum $sectionEnum): array|null
{
$data = self::getAddablePartPieceBlockTemplate($sectionEnum);
if (!is_null($data)) {
return array_keys($data);
}
return $data;
}
/**
* @param \App\Enums\Sections\PageSectionEnum $sectionEnum
* @return array|null
*/
private static function getAddablePartPieceBlockTemplate(PageSectionEnum $sectionEnum): array|null
{
$data = null;
switch ($sectionEnum) {
case PageSectionEnum::produce_export:
$data = array_merge(
self::emptyWysiwygBloc(
[],
"accompagnant l'image __index de la partie",
''
),
self::emptyPictureBloc([], '', '', '', 100, 100, true, "Image __index de la partie"),
self::emptyLinkBloc([], '', false, "Lien de l'image__index de la partie")
);
break;
case PageSectionEnum::produce_rhd:
$data = array_merge(
self::emptyWysiwygBloc(
[],
"accompagnant l'image __index de la partie 1",
''
),
self::emptyPictureBloc([], '', '', '', 100, 100, true, "Image __index de la partie 1"),
self::emptyLinkBloc([], '', false, "Lien de l'image__index de la partie 1")
);
break;
default:
} //end switch
return $data;
}
// * RELATIONS
/**

View file

@ -3,6 +3,7 @@
namespace App\Policies;
use App\Enums\Users\RoleEnum;
use App\Models\Section;
use App\Models\User;
use App\Policies\Rules\UserStaticRules;
@ -30,4 +31,32 @@ class SectionPolicy
// phpcs:enable
return UserStaticRules::atLeastRole($authUser, RoleEnum::admin);
}
/**
* Can add part piece
*
* @param \App\Models\User $authUser
* @param \App\Models\Section $section
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
*/
public function addPartPiece(User $authUser, Section $section): bool
{
// phpcs:enable
return Section::getAddablePartPieceBlock($section->section) !== null;
}
/**
* Can remove part piece
*
* @param \App\Models\User $authUser
* @param \App\Models\Section $section
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
*/
public function removePartPiece(User $authUser, Section $section): bool
{
// phpcs:enable
return Section::getAddablePartPieceBlock($section->section) !== null;
}
}

View file

@ -168,7 +168,9 @@ trait MiniCrudModel
case InputType::string->value:
case InputType::text->value:
case InputType::url->value:
ToolboxHelper::assertNoInternalBoUrls('content', $input['value']);
if (!is_null($input['value'])) {
ToolboxHelper::assertNoInternalBoUrls('content', $input['value']);
}
break;
default:
throw new \ErrorException(sprintf(

BIN
database/assets/schema.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -0,0 +1,118 @@
<?php
use App\Enums\Sections\PageSectionEnum;
use App\Models\Section;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Collection;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Collection::macro('insertAfter', function ($target, $value, $new_key = null) {
if (is_int($target)) {
$this->items = array_merge(array_slice($this->items, 0, $target + 1), [$value], array_slice($this->items, $target + 1));
return;
}
foreach ($this->items as $key => $old) {
$new[$key] = $old;
if ($key === $target && is_null($new_key)) {
$new[] = $value;
} elseif ($key === $target) {
$new[$new_key] = $value;
}
}
$this->items = $new;
});
$export = Section::query()->where('section', PageSectionEnum::produce_export)->first();
if ($export) {
$partsAmount = 3;
$data = collect($export->data);
for ($i = 1; $i <= $partsAmount; $i++) {
$bloc = collect(Section::emptyLinkBloc(
[],
'',
false,
"Lien de l'image {$i} de la partie"
))->first();
$data->insertAfter(
"bloc_image{$i}_de_la_partie",
$bloc,
"bloc_lien_de_limage{$i}_de_la_partie"
);
}
$export->data = $data->all();
$export->saveQuietly();
}
$rhd = Section::query()->where('section', PageSectionEnum::produce_rhd)->first();
if ($rhd) {
$getEmptyLinkBlock = fn ($i, $k) => collect(Section::emptyLinkBloc(
[],
'',
false,
"Lien de l'image {$i} de la partie {$k}"
))->first();
$data = collect($rhd->data);
$data->insertAfter(
'bloc_image1_de_la_partie1',
$getEmptyLinkBlock(1, 1),
'bloc_lien_de_limage1_de_la_partie1'
);
$data->insertAfter(
'bloc_image2_de_la_partie1',
$getEmptyLinkBlock(2, 1),
'bloc_lien_de_limage2_de_la_partie1'
);
$data->insertAfter(
'bloc_image3_de_la_partie1',
$getEmptyLinkBlock(3, 1),
'bloc_lien_de_limage3_de_la_partie1'
);
$data->insertAfter(
'bloc_image1_de_la_partie2',
$getEmptyLinkBlock(1, 2),
'bloc_lien_de_limage1_de_la_partie2'
);
$rhd->data = $data->all();
$rhd->saveQuietly();
} //end if
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
$export = Section::query()->where('section', PageSectionEnum::produce_export)->first();
if ($export) {
$partsAmount = 3;
$data = collect($export->data);
for ($i = 1; $i <= $partsAmount; $i++) {
$data->forget("bloc_lien_de_limage{$i}_de_la_partie");
}
$export->data = $data->all();
$export->saveQuietly();
}
$rhd = Section::query()->where('section', PageSectionEnum::produce_rhd)->first();
if ($rhd) {
$data = collect($rhd->data);
$data->forget('bloc_lien_de_limage1_de_la_partie1');
$data->forget('bloc_lien_de_limage2_de_la_partie1');
$data->forget('bloc_lien_de_limage3_de_la_partie1');
$data->forget('bloc_lien_de_limage1_de_la_partie2');
$rhd->data = $data->all();
$rhd->saveQuietly();
} //end if
}
};

View file

@ -0,0 +1,71 @@
<?php
use App\Enums\Sections\PageSectionEnum;
use App\Lib\Helpers\FileStorageHelper;
use App\Models\Section;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Collection::macro('insertAfter', function ($target, $value, $new_key = null) {
if (is_int($target)) {
$this->items = array_merge(array_slice($this->items, 0, $target + 1), [$value], array_slice($this->items, $target + 1));
return;
}
foreach ($this->items as $key => $old) {
$new[$key] = $old;
if ($key === $target && is_null($new_key)) {
$new[] = $value;
} elseif ($key === $target) {
$new[$new_key] = $value;
}
}
$this->items = $new;
});
$quality = Section::query()->where('section', PageSectionEnum::produce_quality)->first();
if ($quality) {
$data = collect($quality->data);
$data->insertAfter(
'bloc_text_de_la_partie2_content',
collect(Section::emptyPictureBloc([], FileStorageHelper::storeFile(
new Section(),
new \SplFileInfo(\resource_path(
'../database/assets/schema.png'
))
), fake()->sentence(), fake()->sentence(), 1208, 374))->first(),
'bloc_image_de_la_partie2_content'
);
$quality->data = $data->all();
$quality->saveQuietly();
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
$rhd = Section::query()->where('section', PageSectionEnum::produce_rhd)->first();
if ($rhd) {
$data = collect($rhd->data);
$data->forget('bloc_lien_de_limage1_de_la_partie1');
$rhd->data = $data->all();
$rhd->saveQuietly();
} //end if
}
};

View file

@ -465,10 +465,6 @@ class SectionSeeder extends Seeder
'../database/factories/assets/sections/default-picture-100x100.png'
))
), fake()->sentence(), fake()->sentence(), 200, 200);
$data = array_merge(
$data,
$section->emptyWysiwygBloc([], 'accompagnant l\'image', clean(\fake()->randomHtml()))
);
$data = array_merge($data, $section->emptyWysiwygBloc([], 'text', clean(\fake()->randomHtml())));
return $data;
}

View file

@ -24,6 +24,11 @@
@include media-breakpoint-up(md) {
hyphens: initial;
}
h2,h3,h4,.h2,.h3,.h4 {
color: $color-sixt !important;
font-weight: 600 !important;
}
}
/*

View file

@ -30,4 +30,28 @@ window.addEventListener("DOMContentLoaded", () => {
return false;
});
}
/**
* Popup delete item.
*/
const elementsLinks = document.getElementsByClassName(
"confirmDeleteByLinkTS"
) as HTMLCollectionOf<Element>;
for (const element of elementsLinks) {
element.addEventListener("click", function (e) {
e.preventDefault();
const el = e.target;
sweetalert.methods.confirm(
"Êtes-vous sûr ?",
"Toutes les données vont être perdu",
el as HTMLElement,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function (response: SweetAlertResult<any>) {
window.location.href = (el as HTMLAnchorElement).href;
},
{ icon: "warning" }
);
return false;
});
}
});

View file

@ -67,9 +67,9 @@ import type VueTagsInput from "@sipec/vue3-tags-input";
import { VueTagsInput as VueTagsInputCls } from "@sipec/vue3-tags-input";
import slugify from "slugify";
import { defineComponent } from "vue";
import parseLaravelErrorMessages from "./../../modules/parseLaravelErrorMessages";
import route from "./../../modules/route";
import trans from "./../../modules/trans";
import parseLaravelErrorMessages from "../../modules/parseLaravelErrorMessages";
import route from "../../modules/route";
import trans from "../../modules/trans";
export default defineComponent({
name: "BelongsToManyDropdownComponent",

File diff suppressed because it is too large Load diff

View file

@ -1,108 +1,103 @@
<template>
<WordCounter
:id="intId"
:id="id"
class="ck-word-count"
/>
<textarea
ref="textarea"
:id="intId"
:class="`input-text-auto-component ${intClass}`"
:name="intName"
:aria-describedby="intAriaDescribedby"
:placeholder="intPlaceholder"
:required="intRequired"
@input="onInput"
:id="id"
:class="`input-text-auto-component ${classPath}`"
:name="name"
:aria-describedby="ariaDescribedby"
:placeholder="placeholder"
:required="required"
@input="styleElement(checkInput())"
@blur="onBlur"
v-model="intValue"
v-model="value"
/>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import trans from "./../../modules/trans";
<script lang="ts" setup>
import { computed, nextTick, onBeforeMount, onMounted, ref, useAttrs } from "vue";
import WordCounter from "./WordCounterComponent.vue";
const textarea = ref<HTMLTextAreaElement | null>(null);
export default defineComponent({
name: "InputTextAutoComponent",
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.intId = String(data.id);
this.intClass = String(data.class);
this.intName = String(data.name);
this.intPlaceholder = String(data.placeholder);
this.intAriaDescribedby = String(data.ariaDescribedby);
this.intValue = String(data.value);
if (data.required) {
this.intRequired = true;
}
if (this.textarea) {
// Default input init
this.textarea.setAttribute("style", "height: 1rem;overflow-y: hidden;");
}
// * Init input
this.$nextTick(this.onInput);
},
data(): {
intId: string;
intClass: string;
intName: string;
intPlaceholder: string;
intAriaDescribedby: string;
intRequired: boolean;
intValue: string;
} {
return {
intId: "",
intClass: "",
intName: "",
intPlaceholder: "",
intAriaDescribedby: "",
intRequired: false,
intValue: "",
};
},
methods: {
onBlur() {
const el = textarea.value;
if (!el) {
throw new Error("undefined textarea element");
}
// * Remove every new line char
el.value = el.value.replace(/[\n\r]/g, "");
this.$nextTick(() => {
el.style.height = "auto";
el.style.height = `${el.scrollHeight}px`;
});
},
onInput() {
const el = textarea.value;
if (!el) {
throw new Error("undefined textarea element");
}
el.style.height = "auto";
el.style.height = `${el.scrollHeight}px`;
},
},
defineOptions({
name: "InputTextAutoComponent"
});
// * ATTRIBUTES
const attrs = useAttrs();
// * REFS
const textarea = ref<HTMLTextAreaElement|null>(null);
// * DATA
/** Hack to set the id for wordCounterComponent. */
const id = ref<string>(JSON.parse(String(attrs.json ?? "{}")).id);
const classPath = ref<string>("");
const name = ref<string>("");
const placeholder = ref<string>("");
const ariaDescribedby = ref<string>("");
const value = ref<string>("");
const required = ref<boolean>(true);
// * COMPUTED
const isUsedWithProps = computed<boolean>(() => attrs.json === undefined);
onBeforeMount(() => {
if (isUsedWithProps.value) {
throw new Error("this component requires json");
}
});
// * LIFECYCLE
onMounted(() => {
const json = String(attrs.json ?? "{}"),
data = JSON.parse(json);
if (!isUsedWithProps.value) {
id.value = String(data.id);
classPath.value = String(data.class);
name.value = String(data.name);
placeholder.value = String(data.placeholder);
ariaDescribedby.value = String(data.ariaDescribedby);
value.value = String(data.value);
required.value = data.required;
textarea.value?.setAttribute("style", "height:1rem;overflow-y:hidden;");
}
nextTick(() => {
styleElement(checkInput());
});
});
// * METHODS
/** When the textarea get the focus. */
function onBlur() {
const el = checkInput();
/** Remove every new line char */
el.value = el.value.replace(/[\n\r]/g, "");
nextTick(() => {
styleElement(el);
});
}
/** Check if the element is an HTMLTextAreaElement. */
function checkInput(): HTMLTextAreaElement {
const el = textarea.value;
if (!el) {
throw new Error("undefined textarea element");
}
return el;
}
/** Style the textarea element. */
function styleElement(el: HTMLTextAreaElement): void {
el.style.height = "auto";
el.style.height = `${el.scrollHeight}px`;
}
</script>
<style lang="scss">
<style lang="scss" scopped>
.input-text-auto-component {
resize: none;
}

View file

@ -1,26 +1,36 @@
<template>
<button
ref="passwordVisibility"
@click.prevent="toggleVisibility()"
@keydown="keydown"
:class="`btn btn-primary rounded-0 rounded-end password-visibility-${inputId} h-100`"
:class="`btn btn-primary rounded-0 rounded-end password-visibility password-visibility-${id} h-100 border-0 p-0 overflow-hidden`"
:aria-pressed="visible"
>
<span
class="text-bg-primary reveal d-flex justify-content-center align-items-center h-100"
:class="{
'text-bg-primary reveal d-flex justify-content-center align-items-center h-100': true,
'd-none': visible
}"
type="button"
:title="visible ? __('vue.PasswordVisibility.hide') : __('vue.PasswordVisibility.show')"
:title="trans.methods.__('vue.PasswordVisibility.hide')"
data-bs-toggle="tooltip"
>
<FontAwesomeIcon
v-if="visible"
icon="fas fa-eye-slash"
:aria-hidden="true"
/>
</span>
<span
ref="passwordVisibilitySpan"
:class="{
'text-bg-primary reveal d-flex justify-content-center align-items-center h-100': true,
'd-none': !visible
}"
type="button"
:title="trans.methods.__('vue.PasswordVisibility.show')"
data-bs-toggle="tooltip"
>
<FontAwesomeIcon
v-else
icon="fas fa-eye"
:aria-hidden="true"
/>
@ -28,84 +38,85 @@
</button>
</template>
<script lang="ts">
<script lang="ts" setup>
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { defineComponent } from "vue";
import bsTooltips from "./../../modules/bsTooltips";
import trans from "./../../modules/trans";
import { onMounted, ref, useAttrs } from "vue";
import { Tooltips } from "../../modules/bsTooltips";
import trans from "../../modules/trans";
export default defineComponent({
name: "PasswordVisibility",
mixins: [bsTooltips, trans],
components: { FontAwesomeIcon },
data(): {
inputId: string;
input: HTMLInputElement;
visible: boolean;
tooltipList: HTMLButtonElement[];
} {
return {
inputId: "",
input: document.createElement("input"),
visible: false,
tooltipList: [],
};
},
computed: {},
mounted() {
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
this.inputId = data.inputId;
this.input = document.getElementById(this.inputId) as HTMLInputElement;
if (!this.input) {
throw new Error(`Cannot find input id '${this.inputId}'`);
}
if (this.input.tagName !== "INPUT") {
throw new Error(`id '${this.inputId}' is not an input`);
}
if ((this.input as HTMLInputElement).type !== "password") {
throw new Error(`id '${this.inputId}' is not an input password`);
}
this.input = this.input as HTMLInputElement;
this.$nextTick(() => {
this.initTooltips();
});
},
methods: {
keydown(e:KeyboardEvent):void {
if (
e.key === " " ||
e.code === "Space" ||
e.keyCode === 32 ||
e.key === "Enter" ||
e.code === "Enter" ||
e.keyCode === 13
) {
this.toggleVisibility();
e.preventDefault();
}
},
/**
* Update password status (visible, hidden).
*/
toggleVisibility(): void {
if (this.input.type === "password") {
this.input.type = "text";
this.visible = true;
} else {
this.input.type = "password";
this.visible = false;
}
this.$nextTick(() => {
this.initTooltips();
});
},
},
defineOptions({
name: "PasswordVisibility"
});
// * ATTRIBUTES
const attrs = useAttrs();
// * REFS
const passwordVisibility = ref<HTMLButtonElement|null>(null);
// * DATA
const id = ref<string>("");
const input = ref<HTMLInputElement>(document.createElement("input"));
const visible = ref<boolean>(true);
const tooltips = ref<Tooltips | null>(null);
// * LIFECYCLE
onMounted(() => {
const json = String(attrs.json ?? "{}"),
data = JSON.parse(json);
id.value = data.id;
input.value = document.getElementById(id.value) as HTMLInputElement;
if (!input.value) {
throw new Error(`Cannot find input id '${id.value}'`);
}
if (input.value.tagName !== "INPUT") {
throw new Error(`id '${id.value}' is not an input`);
}
if ((input.value as HTMLInputElement).type !== "password") {
throw new Error(`id '${id.value}' is not an input password`);
}
initTooltips();
});
// * METHODS
/** Set event on key down. */
function keydown(e: KeyboardEvent): void {
if (
e.key === " " ||
e.code === "Space" ||
e.keyCode === 32 ||
e.key === "Enter" ||
e.code === "Enter" ||
e.keyCode === 13
) {
toggleVisibility();
e.preventDefault();
}
}
/** Update password status (visible, hidden). */
function toggleVisibility(): void {
input.value.type = (input.value.type === "password") ? "text" : "password";
visible.value = (input.value.type === "password") ? true : false;
}
/** Initialise all tooltips in the component. */
function initTooltips(): void {
setTimeout(() => {
tooltips.value = Tooltips.make({
type: "dom",
elements: passwordVisibility.value!.querySelectorAll("[data-bs-toggle=\"tooltip\"]")
});
}, 500);
}
</script>
<style lang="scss">
.reveal {
<style lang="scss" scopped>
.password-visibility {
border-top-right-radius: 0 !important;
}
.password-visibility .reveal {
width: 52px;
}
</style>

View file

@ -11,7 +11,7 @@
<script lang="ts">
import { defineComponent } from "vue";
import trans from "./../../modules/trans";
import trans from "../../modules/trans";
export default defineComponent({
name: "WordCounterComponent",
@ -106,8 +106,6 @@ export default defineComponent({
<style lang="scss">
#word-counter + input[type="text"],
.word-counter + input[type="text"],
#word-counter + input[type="url"],
.word-counter + input[type="url"],
#word-counter + textarea,
.word-counter + textarea,
#word-counter-varchar + textarea,

View file

@ -1,6 +1,6 @@
import axios from "axios";
import * as bootstrap from "bootstrap";
import bsTooltips from "./../../modules/bsTooltips";
import { Tooltips } from "./../../modules/bsTooltips";
/**
* We'll load the axios HTTP library which allows us to easily issue requests
@ -18,4 +18,4 @@ axios.defaults.headers.common = {
window.axios = axios;
window.bootstrap = bootstrap;
bsTooltips.methods.initTooltips();
Tooltips.make();

View file

@ -1,6 +1,6 @@
import axios from "axios";
import * as bootstrap from "bootstrap";
import bsTooltips from "./../../modules/bsTooltips";
import { Tooltips } from "./../../modules/bsTooltips";
/**
* We'll load the axios HTTP library which allows us to easily issue requests
@ -18,4 +18,4 @@ axios.defaults.headers.common = {
window.axios = axios;
window.bootstrap = bootstrap;
bsTooltips.methods.initTooltips();
Tooltips.make();

View file

@ -69,6 +69,7 @@ declare global {
type LaravelModelList = ModelList;
type LaravelPaginator = Paginator;
type LaravelPaginatorLink = PaginatorLink;
type ValueOf<T> = T[keyof T];
type Enum = EnumInterface;

View file

@ -1,45 +1,114 @@
import type * as bootstrap from "bootstrap";
import { Tooltip } from "bootstrap";
let tooltips: Array<bootstrap.Tooltip> = [];
export default {
methods: {
/**
* Initialize all Bootstrap tooltips.
*/
initTooltips() {
this.disableBootstrapTooltip();
tooltips = [].slice
.call(document.querySelectorAll("[data-bs-toggle=\"tooltip\"]"))
.map(function (tooltipTriggerEl) {
return new window.bootstrap.Tooltip(tooltipTriggerEl, {
customClass: "large-tooltip",
delay: {
show: 1250,
hide: 0,
},
});
});
type ConstructorTypeParent = "parent";
type ConstructorTypeDom = "dom";
interface ParentSelectorTooltips {
type: ConstructorTypeParent
/** The parent element that contains Dom elements for which tooltips shall be created. */
parentSelector: string;
}
interface DomProvidedTooltips {
type: ConstructorTypeDom;
/** Dom elements for which tooltips shall be created. */
elements: HTMLElement | NodeListOf<HTMLElement>;
}
interface TooltipsOptions {
/** Tooltip options. */
options?: Partial<Tooltip.Options>|null|undefined;
}
export type TooltipsConstructor = TooltipsOptions & (ParentSelectorTooltips|DomProvidedTooltips);
export class Tooltips {
/** BS tooltips instances */
private tooltips: Array<Tooltip> = [];
/** The parent element that contains Dom elements for which tooltips shall be created. */
private parentSelector: string | null = null;
/** Dom elements for which tooltips shall be created. */
private elements: Array<HTMLElement> = [];
/** Tooltip default options. */
private static defaultOptions: Partial<Tooltip.Options> = {
delay: {
show: 1250,
hide: 0,
},
/**
* Disable all Bootstrap tooltips.
*/
disableBootstrapTooltip() {
tooltips.forEach((tooltip) => {
tooltip.disable();
tooltip.hide();
});
},
/**
* Close all Bootstrap tooltips.
*/
closeBootstrapTooltip() {
[].slice
.call(document.querySelectorAll("[data-bs-toggle=\"tooltip\"]"))
.forEach((tooltip: HTMLElement) => {
tooltip.blur();
window.bootstrap.Tooltip.getInstance(tooltip)?.hide();
});
},
},
};
};
/** Tooltip options. */
private options: Partial<Tooltip.Options> = {};
/**
* @param parentSelector
* @param options Tooltip options.
*/
constructor(params: TooltipsConstructor) {
if (params.type === "parent") {
this.parentSelectorConstructor(params);
}
if (params.type === "dom") {
this.domPrividedTooltipsContructor(params);
}
}
public static make(params: TooltipsConstructor = { type: "parent", parentSelector: "body"}
) {
const fullParams = { ...{ type: "parent", options: {} }, ...params };
return new Tooltips(fullParams);
}
private parentSelectorConstructor({ parentSelector, options }: TooltipsOptions & ParentSelectorTooltips) {
this.parentSelector = parentSelector;
this.options = { ...Tooltips.defaultOptions, ...options };
this.initTooltips();
}
private domPrividedTooltipsContructor({ elements, options }: TooltipsOptions & DomProvidedTooltips) {
if (elements instanceof HTMLElement) {
this.elements = [elements];
} else {
this.elements = Array.from(elements);
}
this.options = { ...Tooltips.defaultOptions, ...options };
this.initTooltips();
}
/**
* Close all Bootstrap tooltips and reinstanciate them.
*/
public refreshTooltips(): void {
this.closeBootstrapTooltip();
this.initTooltips();
}
/**
* Close all Bootstrap tooltips for ever.
*/
public closeBootstrapTooltip(): void {
this.tooltips.forEach((tooltip) => {
tooltip.hide();
// * Prevent pending tooltips beeing shown after we asked to hide them
tooltip.disable();
});
}
/**
* Initialize tooltips
*/
private initTooltips() {
this.closeBootstrapTooltip();
/** data-bs-toggle tooltips elements */
const elements = this.parentSelector ? [].slice
.call(document.querySelectorAll(`${this.parentSelector} [data-bs-toggle="tooltip"]`)) :
this.elements;
this.tooltips = elements.map((tooltipTriggerEl) => {
return new Tooltip(tooltipTriggerEl, this.options);
});
}
}

View file

@ -38,9 +38,9 @@ export default {
allErrors: Record<string, Array<string>> = {}
): string | null {
let errorList = [] as Array<string>;
Object.keys(allErrors).filter((value, index) => {
Object.keys(allErrors).filter((value) => {
if (value == inputName) {
errorList = allErrors[index];
errorList = allErrors[inputName];
}
});
return errorList.length

View file

@ -16,7 +16,7 @@ const route = {
routeName: string,
parameters: Record<
string,
string | number | Array<string | number>
string | number | null | undefined | Array<string | number>
> = {},
keepActualQueries: boolean = false,
keepActualHashes: boolean = false

View file

@ -110,6 +110,25 @@
</button>
<div class="collapse" id="minicrud">
<div class="card card-body rounded-0">
<div class="row mb-3">
<div class="col">
@can('addPartPiece', $sectionModel)
<a class="btn btn-primary"
href="{{ route('bo.sections.addpartpiece', $sectionModel) }}">{{ __('Ajouter une partie') }}</a>
@endcan
@can('removePartPiece', $sectionModel)
<div class="btn-group">
@for ($i = 1; $i <= $sectionModel->countPartPieces(); $i++)
<a class="btn btn-danger confirmDeleteByLinkTS"
href="{{ route('bo.sections.removepartpiece', [
'section' => $sectionModel,
'index' => $i,
]) }}">{{ __('Enlever partie :index', ['index' => $i]) }}</a>
@endfor
</div>
@endcan
</div>
</div>
<div class="row">
@include('modules.minicrud-form-inputs', [
'data' => $sectionModel->data,

View file

@ -8,7 +8,7 @@
alt="{{ __('Volena - Vollailers Engagés pour l\'Avenir') }}" role="img"
aria-label="{{ __('Volena - Vollailers Engagés pour l\'Avenir') }}">
</a>
<button class="btn btn-primary rounded-pill ms-4"data-bs-dismiss="offcanvas" title="{{ __('Fermer le Menu') }}"
<button class="btn btn-primary rounded-pill ms-4" data-bs-dismiss="offcanvas" title="{{ __('Fermer le Menu') }}"
aria-controls="main-menu">{{ __('Fermer') }}</button>
</div>
<hr>

View file

@ -1,15 +1,20 @@
<div class="col-12">
<img class="img-fluid" src="{{ asset($data["bloc_banniere_{$partName}"]['value']) }}"
<img class="img-fluid rounded-4" src="{{ asset($data["bloc_banniere_{$partName}"]['value']) }}"
title="{{ $data["bloc_banniere_{$partName}"]['title'] }}" alt="{{ $data["bloc_banniere_{$partName}"]['alt'] }}">
</div>
<div class="col-12">
<div class="col-md-8">
@php $k = 0; @endphp
@for ($i = 1; $i <= $itemMax; $i++)
@if (empty($data["bloc_image{$i}_{$partName}"]))
@continue
@endif
@include('fo.pages.produce.flex-row', [
'image_link' => $data["bloc_lien_de_limage{$i}_{$partName}"]['value'] ?? null,
'image' => $data["bloc_image{$i}_{$partName}"]['value'],
'alt' => $data["bloc_image{$i}_{$partName}"]['alt'],
'title' => $data["bloc_image{$i}_{$partName}"]['title'],
'text' => $data["bloc_accompagnant_limage{$i}_{$partName}_content"]['value'],
'switch' => $i % 2 ? false : true,
'switch' => $k++ % 2 ? true : false,
])
@endfor
</div>

View file

@ -2,10 +2,16 @@
'd-flex flex-column flex-sm-row justify-content-center align-items-center my-3',
'flex-sm-row-reverse' => $switch,
])>
<img src="{{ asset($image) }}" title="{{ $title }}" alt="{{ $alt }}" role="img" aria-label="{{ $alt }}">
@if (!empty($image_link))
<a href="{{ $image_link }}" title="{{ __('Lien de :title', ['title' => $title]) }}" target="_blank">
@endisset
<img src="{{ asset($image) }}" title="{{ $title }}" alt="{{ $alt }}" role="img" aria-label="{{ $alt }}">
@isset($image_link)
</a>
@endif
<div class="d-flex justify-content-start align-items-center mx-3">
<div class="text-start">
{!! $text !!}
<div class="ck-content">{!! clean($text) !!}</div>
</div>
</div>
</div>

View file

@ -20,9 +20,14 @@
<section class="anchor-padding container my-4 text-center" id="local">
<div class="row">
<div class="col-12">
<h2 class="h2 fw-bold text-primary">{{ $localSection->h2_title }}</h2>
<h2 class="fw-bold text-primary mb-3">{{ $localSection->h2_title }}</h2>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="ck-content">{!! clean($localSection->text) !!}</div>
</div>
</div>
<div class="row justify-content-center align-items-center flex-md-column-reverse">
<div class="col-12 my-3">
<div class="col-md-8 my-3">
@include('fo.pages.produce.flex-row', [
'image' => $localSection->data['bloc_image']['value'],
'alt' => $localSection->data['bloc_image']['alt'],
@ -39,9 +44,9 @@
<section class="border-top-curved-tertiary-light">
<div class="bg-tertiary-light overflow-hidden pb-5">
<div class="container text-center">
<h2 class="h2 fw-bold text-primary">{{ $gmsSection->h2_title }}</h2>
<div class="row">
<div class="col-12 mb-3">
<h2 class="fw-bold text-primary mb-3">{{ $gmsSection->h2_title }}</h2>
<div class="row justify-content-center">
<div class="col-md-8 mb-3">
<div class="ck-content">
{!! clean($gmsSection->text) !!}
</div>
@ -72,24 +77,24 @@
<section class="border-top-curved-upper-tertiary-light">
<div class="overflow-hidden bg-white pb-5">
<div class="container text-center">
<h2 class="h2 fw-bold text-primary">{{ $rhdSection->h2_title }}</h2>
<div class="row">
<h2 class="fw-bold text-primary mb-3">{{ $rhdSection->h2_title }}</h2>
<div class="row justify-content-center">
{{-- PART 1 --}}
@include('fo.pages.produce.banner-elements', [
'partName' => 'de_la_partie1',
'data' => $rhdSection->data,
'itemMax' => 3,
'itemMax' => 10,
])
<div class="col-12 mb-3">
<div class="col-md-8 mb-3">
<div class="ck-content">
{!! clean($rhdSection->text) !!}
</div>
</div>
<div class="col-12 mb-3">
<div class="col-md-8 mb-3">
<button class="btn btn-primary rounded-pill">{{ __('LDC restauration') }}</button>
</div>
{{-- PART 2 --}}
<div class="col-12 mb-3">
<div class="col-md-8 mb-3">
<div class="row w-100 bg-nine rounded-4 mx-auto p-4">
<div class="col-4 d-none d-md-flex flex-column justify-content-center align-items-start">
<h3 class="h3 text-primary fw-bold text-uppercase m-0">{{ __('le saviez-vous ?') }}</h3>
@ -116,12 +121,12 @@
<section class="border-top-curved-primary">
<div class="bg-primary overflow-hidden pb-5 text-white">
<div class="container text-center">
<h2 class="h2 fw-bold">{{ $exportSection->h2_title }}</h2>
<div class="row">
<h2 class="fw-bold mb-3">{{ $exportSection->h2_title }}</h2>
<div class="row justify-content-center">
@include('fo.pages.produce.banner-elements', [
'partName' => 'de_la_partie',
'data' => $exportSection->data,
'itemMax' => 3,
'itemMax' => 10,
])
</div>
</div>
@ -131,9 +136,9 @@
<section class="border-top-curved-upper-primary">
<div class="overflow-hidden bg-white pb-5">
<div class="container">
<div class="row gx-5">
<div class="col-12 mb-3 text-center">
<h2 class="h2 fw-bold">{{ $qualitySection->h2_title }}</h2>
<div class="row justify-content-center gx-5">
<div class="col-md-8 mb-3 text-center">
<h2 class="fw-bold mb-3">{{ $qualitySection->h2_title }}</h2>
<div class="ck-content">{!! clean($qualitySection->text) !!}</div>
@if ($qualitySection->data['bloc_button__on_vous_explique_comment_lire_cetteetiquette']['value'])
<a class="btn btn-primary rounded-pill mt-3" href="https://www.etiquettebienetreanimal.fr/"
@ -145,36 +150,43 @@
'data' => $qualitySection->data,
'itemMax' => 1,
])
<div class="col-12 mb-3 text-center">
<div class="col-md-8 mb-3 text-center">
<div class="bg-tertiary-light rounded-4 my-3 p-4">
<div class="ck-content">{!! clean($qualitySection->data['bloc_text_de_la_partie2_content']['value']) !!}</div>
<div class="d-flex justify-content-center align-items-center mt-3">
<img class="img-fluid" src="{{ Vite::asset('resources/assets/images/produce/schema.png') }}"
title="{{ __('Schéma') }}" alt="{{ __('Plein de formes différentes') }}">
<img class="img-fluid" src="{{ $qualitySection->data['bloc_image_de_la_partie2_content']['value'] }}"
title="{{ $qualitySection->data['bloc_image_de_la_partie2_content']['title'] }}"
alt="{{ $qualitySection->data['bloc_image_de_la_partie2_content']['alt'] }}">
</div>
</div>
</div>
<div class="col-12 col-md-6 my-3">
<div class="bg-sixt-light rounded-2 px-3 py-4 text-center">
</div>
<div class="row justify-content-center">
<div class="col-md-4 my-3">
<div class="bg-sixt-light rounded-2 h-100 d-flex flex-column justify-content-between px-3 py-4 text-center">
<div class="ck-content">
{!! $qualitySection->data['bloc_accompagnant_le_bouton_on_vousecoute_content']['value'] !!}
</div>
@if ($qualitySection->data['bloc_button__on_vousecoute']['value'])
<a class="btn btn-primary rounded-pill rounded-2 mt-3" href="{{ route('fo.listen.show') }}">
{{ __('On vous écoute') }}
</a>
<div>
<a class="btn btn-primary rounded-pill rounded-2 mt-3" href="{{ route('fo.listen.show') }}">
{{ __('On vous écoute') }}
</a>
</div>
@endif
</div>
</div>
<div class="col-12 col-md-6 my-3">
<div class="bg-quint-light rounded-2 px-3 py-4 text-center">
<div class="col-md-4 my-3">
<div class="bg-quint-light rounded-2 h-100 d-flex flex-column justify-content-between px-3 py-4 text-center">
<div class="ck-content">
{!! $qualitySection->data['bloc_accompagnant_le_bouton_on_se_devoile_content']['value'] !!}
</div>
@if ($qualitySection->data['bloc_button__on_se_devoile']['value'])
<a class="btn btn-primary rounded-pill rounded-2 mt-3" href="{{ route('fo.unveil.show') }}">
{{ __('On se dévoile') }}
</a>
<div>
<a class="btn btn-primary rounded-pill rounded-2 mt-3" href="{{ route('fo.unveil.show') }}">
{{ __('On se dévoile') }}
</a>
</div>
@endif
</div>
</div>

View file

@ -208,7 +208,7 @@
'width' => $input['dimensions']['width'],
'height' => $input['dimensions']['height'],
'model' => $model ?? null,
'value' => old("data.{$k}.value", $model->data[$k]['value'] ?? null),
'value' => $model->data[$k]['value'],
'helper' => __('crud.minicrud.dimensions_must_be', [
'width' => $input['dimensions']['width'],
'height' => $input['dimensions']['height'],

View file

@ -118,6 +118,11 @@ Route::prefix('bo')
->only(['index', 'edit', 'update'])
->withoutMiddleware('htmlsanitize')
->middleware(['htmlsanitize:text,data']);
Route::get('sections/{section}/addpartpiece', [SectionController::class, 'addPartPiece'])
->name('sections.addpartpiece');
Route::get('sections/{section}/removepartpiece/{index}', [SectionController::class, 'removePartPiece'])
->where('index', '[0-9]+')
->name('sections.removepartpiece');
// * JOB VIDEOS.
Route::resource('jobvideos', JobVideoController::class)->except('show')