add text_color and published fields on the menu configuration

This commit is contained in:
Alexis Gousseau 2024-04-25 11:48:49 +02:00
parent 0fb382c0a6
commit 4f952a89ca
25 changed files with 737 additions and 276 deletions

View file

@ -2,10 +2,13 @@
namespace App\Http\Requests\Bo\GeneralSettings;
use App\Enums\Settings\MenuNodeType;
use App\Enums\Settings\SettingsType;
use App\Traits\Requests\HasPicture;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
use Illuminate\Validation\Rules\Enum;
class UpdateGeneralSettingRequest extends FormRequest
{
@ -41,7 +44,10 @@ class UpdateGeneralSettingRequest extends FormRequest
return;
}
$this->merge([
'value' => json_decode($this->input('jsonValue', '[]'))
'value' => collect(json_decode($this->input('jsonValue', '[]'), true))->map(function ($element) {
$element['published'] = (array_key_exists('published', $element)) ? $element['published'] : false;
return $element;
})->toArray()
]);
}
@ -65,11 +71,44 @@ class UpdateGeneralSettingRequest extends FormRequest
$this->pictureRules('picture', 200, 100, 200, 100, false),
);
} else {
/** @var array<array-key, mixed> $routesName Get all front routes name */
$routesName = collect(Route::getRoutes())->map(function ($route) {
if (preg_match('/fo.([a-zA-Z])+.([a-zA-Z])+/', $route->getName())) {
return $route->getName();
}
})->filter()->toArray();
/** @var int $count Count all node published */
$count = collect($this->value)->filter(function ($element) {
if ($element['published']) {
return $element;
}
})->count();
return [
'jsonValue' => 'required|json',
'value' => 'required'
'jsonValue' => 'required|json',
'value' => [
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
'present', 'array', function ($attribute, array $value, $fail) use ($count) {
if ($count > 8) {
$fail(\trans('Maximum 8 liens doivent être présents dans le menu'));
}
}
],
'value.*.type' => ['required', new Enum(MenuNodeType::class)],
'value.*.published' => 'required|boolean',
'value.*.title' => 'required|string|between:3,100',
'value.*.link.url' => 'nullable|string|url:https',
'value.*.link.routeName' => [
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
'nullable', 'string', function ($attribute, string $value, $fail) use ($routesName) {
if (!in_array($value, $routesName)) {
$fail(\trans('Doît être un lien front existant'));
}
}
],
];
}
} //end if
}
/**
@ -80,7 +119,31 @@ class UpdateGeneralSettingRequest extends FormRequest
public function attributes(): array
{
return [
'value' => trans('validation.custom.value')
'text' => trans('validation.attributes.text'),
'picture' => trans('validation.attributes.image'),
'picture_alt' => trans('validation.custom.attribute_alt'),
'picture_title' => trans('validation.custom.attribute_title'),
'value' => trans('validation.custom.value'),
'value.*.type' => trans(':field :internoeud', [
'field' => trans('validation.custom.type'),
'inter' => trans('validation.custom.of_the.male'),
]),
'value.*.published' => trans(':field :internoeud', [
'field' => trans('validation.custom.published'),
'inter' => trans('validation.custom.of_the.male'),
]),
'value.*.title' => trans(':field :internoeud', [
'field' => trans('validation.attributes.title'),
'inter' => trans('validation.custom.of_the.male'),
]),
'value.*.link.url' => trans(':field :internoeud', [
'field' => trans('validation.custom.url'),
'inter' => trans('validation.custom.of_the.male'),
]),
'value.*.link.routeName' => trans(':field :internoeud', [
'field' => trans('validation.custom.route_name'),
'inter' => trans('validation.custom.of_the.male'),
]),
];
}
}

View file

@ -5,6 +5,7 @@ namespace App\Http\Requests\Bo\MenuNodeData;
use App\Traits\Requests\HasPicture;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use LVR\Colour\Hex;
class UpdateMenuNodeDataRequest extends FormRequest
{
@ -30,6 +31,9 @@ class UpdateMenuNodeDataRequest extends FormRequest
if ($this->has('text')) {
$this->merge(['text' => str($this->input('text'))->value()]);
}
if ($this->has('text_color')) {
$this->merge(['text_color' => str($this->input('text_color'))->value()]);
}
if ($this->has('link_target')) {
$this->merge(['link_target' => $this->boolean('link_target')]);
}
@ -58,6 +62,7 @@ class UpdateMenuNodeDataRequest extends FormRequest
'sub_label' => 'sometimes|required|string|min:4|max:255',
'title' => 'sometimes|required|string|min:4|max:255',
'text' => 'sometimes|required|string|max:255',
'text_color' => ['sometimes', 'required', new Hex()],
'link_url' => 'sometimes|required|string|max:255',
'link_title' => 'sometimes|required|string|between:3,255',
'link_target' => 'sometimes|present|boolean',

View file

@ -6,9 +6,10 @@ use App\Enums\Settings\MenuNodeType;
use App\Lib\Link;
/**
* @property-read string $type The node menu type.
* @property-read string $title The node menu title.
* @property-read \App\Lib\Link $link The node menu link.
* @property-read string $type The node menu type.
* @property-read string $title The node menu title.
* @property-read \App\Lib\Link $link The node menu link.
* @property-read boolean $published The node menu publish status.
* @package App\Lib\Definitions
*/
class MenuNode extends \stdClass
@ -22,7 +23,7 @@ class MenuNode extends \stdClass
*/
public function __get(string $name)
{
if (in_array($name, ['type', 'title', 'link'])) {
if (in_array($name, ['type', 'title', 'link', 'published'])) {
return $this->{$name};
}
throw new \TypeError("undefined property {$name}");
@ -31,13 +32,14 @@ class MenuNode extends \stdClass
/**
* Make and instance
*
* @param integer $type The node menu type.
* @param string $title The node menu title.
* @param array $link The node menu link.
* @param integer $type The node menu type.
* @param boolean $published The node menu published.
* @param string $title The node menu title.
* @param array $link The node menu link.
* @return \App\Lib\Definitions\MenuNode
* @throws \RuntimeException If $type or $data is wrong.
*/
public static function make(int $type, string $title, array $link): self
public static function make(int $type, bool $published, string $title, array $link): self
{
self::assertTypeIsCorrect($type);
@ -45,6 +47,8 @@ class MenuNode extends \stdClass
// @phpstan-ignore-next-line
$menuNode->type = MenuNodeType::make($type);
// @phpstan-ignore-next-line
$menuNode->published = $published;
// @phpstan-ignore-next-line
$menuNode->title = $title;
// @phpstan-ignore-next-line
$menuNode->link = new Link(...$link);

View file

@ -2,6 +2,7 @@
namespace App\Models;
use App\Casts\HtmlColor;
use App\Enums\Settings\MenuNodeType;
use App\Lib\Helpers\FileStorageHelper;
use App\Traits\Models\IsCachedAsCollection;
@ -13,6 +14,7 @@ use Illuminate\Database\Eloquent\Model;
* @property string $sub_label (Left part) Link label.
* @property string $title (Right part) Sub title.
* @property string $text (Right part) Sub text.
* @property string $text_color (Right part) Sub text color.
* @property string $link_url (Right part) Link url.
* @property string $link_title (Right part) Link title.
* @property boolean $link_target (Right part) Link target.
@ -62,6 +64,7 @@ class MenuNodeData extends Model
'sub_label',
'title',
'text',
'text_color',
'link_url',
'link_title',
'link_target',
@ -82,6 +85,7 @@ class MenuNodeData extends Model
*/
protected $casts = [
'type' => MenuNodeType::class,
'text_color' => HtmlColor::class,
'link_target' => 'boolean',
'link_status' => 'boolean',
'picture_status' => 'boolean',

View file

@ -17,6 +17,7 @@ class MenuNodeDataFactory extends Factory
'sub_label' => fake()->sentence(),
'title' => fake()->sentence(),
'text' => fake()->sentence(),
'text_color' => fake()->unique()->safeHexColor(),
'link_url' => fake()->url(),
'link_title' => fake()->sentence(),
'link_target' => fake()->boolean(),

View file

@ -1,9 +1,7 @@
<?php
use Database\Seeders\Mandatory\GeneralSettingSeeder;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
@ -20,10 +18,6 @@ return new class extends Migration
$table->json('value')->comment('The actual setting value');
$table->timestamps();
});
Artisan::call('db:seed', [
'class' => GeneralSettingSeeder::class
]);
}
/**

View file

@ -1,9 +1,7 @@
<?php
use Database\Seeders\Mandatory\MenuNodeDataSeeder;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
@ -39,9 +37,6 @@ return new class extends Migration
$table->timestamps();
});
Artisan::call('db:seed', [
'class' => MenuNodeDataSeeder::class
]);
}
/**

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
// phpcs:disable Generic.Files.LineLength.TooLong
DB::statement("ALTER TABLE `general_settings` CHANGE `type` `type` TINYINT UNSIGNED NOT NULL COMMENT 'General settings enum type (SettingsType).'");
DB::statement("ALTER TABLE `general_settings` CHANGE `value` `value` LONGTEXT NOT NULL COMMENT 'General settings value (json).'");
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
DB::statement("ALTER TABLE `general_settings` CHANGE `type` `type` TINYINT UNSIGNED NOT NULL COMMENT 'Enum SettingsType'");
DB::statement("ALTER TABLE `general_settings` CHANGE `value` `value` LONGTEXT NOT NULL COMMENT 'The actual setting value'");
// phpcs:enable Generic.Files.LineLength.TooLong
}
};

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table('menu_node_data', function (Blueprint $table) {
$table->unsignedBigInteger('text_color')->comment('(Right part) Sub text color')->after('text');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::table('menu_node_data', function (Blueprint $table) {
$table->dropColumn('text_color');
});
}
};

View file

@ -3,6 +3,7 @@
namespace Database\Seeders;
use Database\Seeders\Mandatory\GeneralSettingSeeder;
use Database\Seeders\Mandatory\MenuNodeDataSeeder;
use Database\Seeders\Mandatory\StaticPageSeeder;
use Illuminate\Database\Seeder;
@ -17,7 +18,8 @@ class MandatorySeeder extends Seeder
{
$this->call([
StaticPageSeeder::class,
GeneralSettingSeeder::class
GeneralSettingSeeder::class,
MenuNodeDataSeeder::class,
]);
}
}

View file

@ -254,6 +254,7 @@ return [
'publication' => 'publication',
'publish_at' => 'publish at',
'published' => 'published',
'route_name' => 'name of the route',
'seo_description' => 'seo description',
'seo_title' => 'seo title',
'slug' => 'slug',
@ -262,6 +263,7 @@ return [
'subtitle' => 'subtitle',
'sustitle' => 'sustitle',
'target' => 'target',
'type' => 'type',
'unpublish_at' => 'unpublish at',
'unpublished' => 'unpublished',
'url' => 'url',

View file

@ -70,7 +70,7 @@ return [
'picture_title_label' => 'Attribut du titre de l\'image',
'picture_alt_helper' => 'Doit décrire votre image pour les moteurs de recherche et les aveugles (5 à 6 mots)',
'picture_alt_label' => 'Attribut Alt de l\'image',
'text_warning' => 'Relisez votre contenu avant de le publier',
'text_warning' => 'Relisez votre contenu avant de le publier.',
],
'pagination' => [

View file

@ -255,6 +255,7 @@ return [
'publication' => 'publication',
'publish_at' => 'date de publication',
'published' => 'publié',
'route_name' => 'nom de la route',
'seo_description' => 'description SEO',
'seo_title' => 'titre SEO',
'slug' => 'slug',
@ -263,6 +264,7 @@ return [
'subtitle' => 'sous-titre',
'sustitle' => 'sous-titre',
'target' => 'cible',
'type' => 'type',
'unpublish_at' => 'date de fin de publication',
'unpublished' => 'non publié',
'url' => 'lien',

View file

@ -1,55 +1,67 @@
<template>
<fieldset
class="container-fluid main-menu-component my-3"
ref="mainMenu"
class="container-fluid main-menu-component"
:disabled="loading"
>
<div class="row justify-content-start mb-3">
<div class="col">
<span class="h5">{{ trans.methods.__('Réorganisez le menu principal comme vous le souhaitez ou ajoutez un lien.') }}</span>
</div>
</div>
<!-- ORDERABLE NODES -->
<div class="row justify-content-start">
<div class="col col-xl-4 my-3">
<fieldset
class="border p-4"
:disabled="nodeToEdit !== null"
>
<legend>
<span class="fs-5">{{ trans.methods.__('Ordonancement du menu') }}</span>
<!-- ORDERABLE NODES -->
<div class="col-12 col-xl-4 p-0">
<fieldset :disabled="nodeToEdit !== null">
<div class="d-flex justify-content-between align-items-center mb-2">
<p class="fw-bold m-0">
{{ trans.methods.__('Ordonancement du menu') }}
<span
tabindex="0"
:title="trans.methods.__('Réorganisez le menu principal comme vous le souhaitez ou ajoutez un lien.')"
data-bs-toggle="tooltip"
><FontAwesomeIcon icon="fa-solid fa-circle-info fa-xs" /></span>
</p>
<button
class="btn btn-sm btn-primary float-end mt-1"
:title="trans.methods.__('Ajouter un lien au menu')"
data-bs-toggle="tooltip"
@click.prevent="addLinkNode"
>
<FontAwesomeIcon icon="fa fa-plus" />
</button>
</legend>
</div>
<div class="list-group">
<Draggable
:list="nodeList"
item-key="type"
@change="orderChanged"
>
<!-- LIST ITEM -->
<template #item="{ element, index } : {element: MenuNodeInterface, index: number}">
<div class="list-group-item d-flex flux-column justify-content-between">
<span class="h6">
<FontAwesomeIcon
icon="fa-solid fa-align-justify"
class="handle text-secondary"
/>
<span>&nbsp;&nbsp;&nbsp;</span>
<strong>{{ element.title }}</strong>
</span>
<div class="d-flex justify-content-center align-items-center h6 m-0">
<span class="me-3">
<FontAwesomeIcon
icon="fa-solid fa-align-justify"
class="handle text-secondary"
/>
</span>
<span
v-if="element.published"
class="me-3"
:title="trans.methods.__('Ce noeud est publié')"
data-bs-toggle="tooltip"
>
<FontAwesomeIcon
icon="fa fa-circle-check"
class="text-success"
/>
</span>
{{ element.title }}
</div>
<!-- ACTIONS -->
<div class="btn-group">
<button
v-if="element.type !== MenuNodeTypeEnum.siteLogo"
class="btn btn-sm btn-primary"
@click.prevent="editNode(index)"
:title="trans.methods.__('Modifier le noeud')"
data-bs-toggle="tooltip"
>
<FontAwesomeIcon icon="fa fa-pencil" />
</button>
@ -67,27 +79,28 @@
</div>
</fieldset>
</div>
<div class="col-12 col-xl-8 main-menu-node-editor-component my-3 overflow-y-scroll">
<!-- NODE EDITOR -->
<MainMenuNodeEditor
<div class="col-12 col-xl-8 main-menu-node-editor-component p-0 pt-3 ps-xl-3 pt-xl-0">
<div
v-if="nodeToEdit !== null"
:menu-node="nodeToEdit"
:enum="getEnum(nodeToEdit.type)"
@close-editor="closeEditor"
@updated-node="(menuNode) => nodeIndexToEdit !== null ? saveEditedNode(nodeIndexToEdit, menuNode) : null"
/>
<!-- NODE DATA EDITOR -->
<MainMenuNodeDataEditor
v-if="nodeDataToEdit !== null && nodeDataToEdit.type !== MenuNodeTypeEnum.link"
:menu-node-data="getNodeData(nodeDataToEdit.type)"
:errors="menuNodeDataErrors"
@updated-node-data="(prop, value) => nodeDataToEdit !== null ? saveMenuNodeData(nodeDataToEdit.type, prop, value) : null"
/>
class="border p-3"
>
<!-- NODE EDITOR -->
<MainMenuNodeEditor
v-if="nodeToEdit !== null"
:menu-node="nodeToEdit"
:enum="getEnum(nodeToEdit.type)"
@close-editor="closeEditor"
@updated-node="(menuNode) => nodeIndexToEdit !== null ? saveEditedNode(nodeIndexToEdit, menuNode) : null"
/>
<!-- NODE DATA EDITOR -->
<MainMenuNodeDataEditor
v-if="nodeDataToEdit !== null && nodeDataToEdit.type !== MenuNodeTypeEnum.link"
:menu-node-data="getNodeData(nodeDataToEdit.type)"
:errors="menuNodeDataErrors"
@updated-node-data="(prop, value) => nodeDataToEdit !== null ? saveMenuNodeData(nodeDataToEdit.type, prop, value) : null"
/>
</div>
</div>
<!-- INPUTS FOR SUBMISSION -->
<input
type="hidden"
@ -113,12 +126,16 @@ import type { MenuNodeInterface } from "./../../types/models/MenuNode";
import type { MenuNodeDataInterface } from "./../../types/models/MenuNodeData";
import MainMenuNodeDataEditor from "./MainMenuNodeDataEditor.vue";
import MainMenuNodeEditor from "./MainMenuNodeEditor.vue";
import { Tooltips } from "./../../modules/bsTooltips";
defineOptions({
name: "MainMenu",
inheritAttrs: true,
});
// * REFS
const mainMenu = ref<HTMLDivElement|null>(null);
// * ATTRIBUTES
const attrs = useAttrs();
@ -132,8 +149,7 @@ let nodeIndexToEdit = ref<number|null>(null);
let nodeToEdit = ref<MenuNodeInterface|null>(null);
let nodeDataToEdit = ref<MenuNodeDataInterface|null>(null);
let menuNodeDataErrors = reactive<LaravelValidationErrors>({});
// * COMPUTED
const tooltips = ref<Tooltips|null>(null);
// * LIFECYCLE
onMounted(() => {
@ -141,51 +157,61 @@ onMounted(() => {
data = JSON.parse(json);
nodeList.value = data.nodeList ?? [];
nodeTypeEnumList = data.nodeTypeEnumList ?? [];
fetchMenuNodeDataList().then((dataList) => { menuNodeDataList.value = dataList; });
fetchMenuNodeDataList().then((dataList) => {
menuNodeDataList.value = dataList;
initTooltips();
});
});
// * METHODS
function orderChanged(): void {
console.log("sdsd");
}
/** Add a new link node to the menu */
function addLinkNode(): void {
const newNode = {
type: MenuNodeTypeEnum.link,
title: trans.methods.__("Lien"),
published: false,
link: {
routeName: null,
url: "https://example.net"
}
};
console.log("push nodeList", newNode);
nodeList.value.push(newNode);
}
/** Remove a specific link node by index */
function removeLinkNode(index: number): void {
nodeList.value.splice(index, 1);
}
function editNode(index: number):void {
/** Edit a specific link node by index */
function editNode(index: number): void {
nodeIndexToEdit.value = index;
nodeToEdit.value = nodeList.value[index];
nodeDataToEdit.value = menuNodeDataList.value.find((nodeData) => nodeData.type === nodeToEdit.value?.type) ?? null;
}
function saveEditedNode(index: number, menuNode: MenuNodeInterface):void {
/** Save a specific link node by index */
function saveEditedNode(index: number, menuNode: MenuNodeInterface): void {
nodeList.value[index] = menuNode;
initTooltips();
}
/** Close the editor part */
function closeEditor(): void {
nodeIndexToEdit.value = null;
nodeToEdit.value = null;
nodeDataToEdit.value = null;
}
function refreshEditor(): void
{
/** Refresh the editor part */
function refreshEditor(): void {
if (nodeIndexToEdit.value !== null) {
editNode(nodeIndexToEdit.value);
}
}
/** Get the node data */
function getNodeData(type: MenuNodeTypeEnum): MenuNodeDataInterface
{
const NodeData = menuNodeDataList.value.find((nodeData) => nodeData.type === type);
@ -246,36 +272,6 @@ async function saveMenuNodeData(
fetchMenuNodeDataList().then((dataList) => { menuNodeDataList.value = dataList; refreshEditor(); });
resolve(true);
};
const catchAction = (error: AxiosError<{errors: LaravelValidationErrors|{}}>) => {
let message = "Une erreur est survenue merci de ressayer plus tard";
if (error!.response!.status === 422) {
// ts:ignore
menuNodeDataErrors = error.response?.data?.errors ?? {};
toast.methods.errorNotify(trans.methods.__("La valeur saisie est incorrecte"));
return;
}
if (error!.response!.status === 401) {
// ts:ignore
toast.methods.errorNotify(trans.methods.__("Vous devez vous reconnecter"));
console.warn(trans.methods.__("Vous devez vous reconnecter"));
return;
}
if (error!.response!.status === 419) {
// ts:ignore
toast.methods.errorNotify(trans.methods.__("Vous devez rafraichir la page"));
return;
}
if (error!.response) {
console.error(error.response.data);
} else if (error.request) {
console.error(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.error("Error", error.message);
}
toast.methods.errorNotify(message);
resolve(false);
};
if(menuNodeData instanceof File) {
const formData = new FormData();
@ -286,18 +282,53 @@ async function saveMenuNodeData(
headers: {
"content-type": "multipart/form-data"
}
}).then(succeedAction).catch(catchAction)
}).then(succeedAction).catch(ajaxErrorHandler(resolve))
.finally(() => { loading.value = false; });
return;
}
axios.patch<boolean>(menuNodeDataListRoute, {
[prop]: menuNodeData
}).then(succeedAction).catch(catchAction)
}).then(succeedAction).catch(ajaxErrorHandler(resolve))
.finally(() => { loading.value = false; });
});
}
/** Catch ajax errors */
function ajaxErrorHandler(resolve: (value: boolean | PromiseLike<boolean>) => void) {
return (error: AxiosError<{errors: LaravelValidationErrors|{}}>) => {
let message = "Une erreur est survenue merci de ressayer plus tard";
if (error!.response!.status === 422) {
// ts:ignore
menuNodeDataErrors = error.response?.data?.errors ?? {};
toast.methods.errorNotify(trans.methods.__("La valeur saisie est incorrecte"));
return;
}
if (error!.response!.status === 401) {
// ts:ignore
toast.methods.errorNotify(trans.methods.__("Vous devez vous reconnecter"));
console.warn(trans.methods.__("Vous devez vous reconnecter"));
return;
}
if (error!.response!.status === 419) {
// ts:ignore
toast.methods.errorNotify(trans.methods.__("Vous devez rafraichir la page"));
return;
}
if (error!.response) {
console.error(error.response.data);
} else if (error.request) {
console.error(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.error("Error", error.message);
}
toast.methods.errorNotify(message);
resolve(false);
};
}
/** Get the enum value */
function getEnum(tsEnum: MenuNodeTypeEnum): Enum {
const enumValue: Enum|undefined = Object.values(nodeTypeEnumList).find((value) => value.value === tsEnum);
if (!enumValue) {
@ -305,6 +336,16 @@ function getEnum(tsEnum: MenuNodeTypeEnum): Enum {
}
return enumValue;
}
/** Initialise all tooltips in the component */
function initTooltips(): void {
setTimeout(() => {
tooltips.value = Tooltips.make({
type: "dom",
elements: mainMenu.value!.querySelectorAll("[data-bs-toggle=\"tooltip\"]")
});
}, 500);
}
</script>
<style lang="scss" scoped>

View file

@ -1,11 +1,10 @@
<template>
<fieldset class="main-menu-node-data-editor-component container-fluid p-4 border mb-3" >
<hr>
<fieldset class="main-menu-node-data-editor-component container-fluid">
<legend>
<span class="fs-5">{{ trans.methods.__("Modification de données d'un noeud") }}</span>
</legend>
<span class="d-block fs-5">{{ trans.methods.__('Partie gauche') }}</span>
<span class="d-block fw-bold">{{ trans.methods.__('Partie gauche') }}</span>
<!-- SUB LABEL -->
<div class="mb-3 row">
<label
@ -14,7 +13,11 @@
>
{{ trans.methods.__("Label du sous-menu") }}
</label>
<div class="col-sm-4">
<div class="col">
<WordCounter
id="title-sub-label"
class="ck-word-count"
/>
<input
id="title-sub-label"
type="text"
@ -22,14 +25,27 @@
:value="menuNodeData.sub_label"
@blur="($event) => saveNodeDataInput('sub_label', ($event.target as HTMLInputElement).value)"
>
<small v-if="errors.sub_label" v-for="(message) of errors.sub_label" class="text-danger">"{{ message }}"</small>
<small class="form-text text-body-secondary">
{{ trans.methods.__('validation.between.string', {
attribute: "label du sous-menu",
min: "3",
max: "255",
}) }}
</small>
<template
v-for="(message, messageIndex) of errors.sub_label"
:key="messageIndex"
>
<small
v-if="errors.sub_label"
class="text-danger"
>"{{ message }}"</small>
</template>
</div>
</div>
<!-- SEPARATION -->
<hr>
<span class="d-block fs-5">{{ trans.methods.__('Partie droite') }}</span>
<span class="d-block fw-bold">{{ trans.methods.__('Partie droite') }}</span>
<!-- TITLE -->
<div class="mb-3 row">
<label
@ -38,7 +54,11 @@
>
{{ trans.methods.__("Titre") }}
</label>
<div class="col-sm-4">
<div class="col">
<WordCounter
id="title-text"
class="ck-word-count"
/>
<input
id="title-text"
type="text"
@ -46,38 +66,103 @@
:value="menuNodeData.title"
@blur="($event) => saveNodeDataInput('title', ($event.target as HTMLInputElement).value)"
>
<small v-if="errors.title" v-for="(message) of errors.title" class="text-danger">"{{ message }}"</small>
<small class="form-text text-body-secondary">
{{ trans.methods.__('validation.between.string', {
attribute: "titre",
min: "3",
max: "255",
}) }}
</small>
<template
v-for="(message, messageIndex) of errors.title"
:key="messageIndex"
>
<small
v-if="errors.title"
class="text-danger"
>"{{ message }}"</small>
</template>
</div>
</div>
<!-- TEXT -->
<div class="mb-3 row">
<label
for="text-text"
class="col-sm-2 col-form-label"
>
{{ trans.methods.__("Text") }}
{{ trans.methods.__("Texte") }}
</label>
<div class="col-sm-4">
<div class="col">
<WordCounter
id="text-text"
class="ck-word-count"
/>
<textarea
id="text-text"
class="form-control"
@blur="($event) => $nextTick(() => saveNodeDataInput('text', ($event.target as HTMLTextAreaElement).value))"
:value="menuNodeData.text"></textarea>
<small v-if="errors.text" v-for="(message) of errors.text" class="text-danger">"{{ message }}"</small>
:value="menuNodeData.text"
/>
<small class="form-text text-body-secondary">
{{ trans.methods.__('validation.between.string', {
attribute: "texte",
min: "3",
max: "255",
}) + " " + trans.methods.__('crud.minicrud.text_warning') }}
</small>
<template
v-for="(message, messageIndex) of errors.text"
:key="messageIndex"
>
<small
v-if="errors.text"
class="text-danger"
>"{{ message }}"</small>
</template>
</div>
</div>
<!-- COLOR -->
<div class="mb-3 row">
<label
for="text_color"
class="col-sm-2 col-form-label"
>
{{ trans.methods.__("Couleur du texte") }}
</label>
<div class="col">
<ColorPickerComponent
name="text_color"
:value="menuNodeData.text_color"
@update-color="($event) => $nextTick(() => saveNodeDataInput('text_color', ($event as HTMLInputElement).value))"
/>
<small class="form-text text-body-secondary">
{{ trans.methods.__('Choisissez une couleur qui se distingue bien de l\'image.') }}
</small>
<template
v-for="(message, messageIndex) of errors.text_color"
:key="messageIndex"
>
<small
v-if="errors.text_color"
class="text-danger"
>"{{ message }}"</small>
</template>
</div>
</div>
<!-- LINK -->
<div class="mb-3 row">
<!-- LINK URL -->
<label
for="link-text"
class="col-sm-2 col-form-label p-2"
class="col-sm-2 col-form-label"
>
{{ trans.methods.__("Link url") }}
{{ trans.methods.__("URL du lien") }}
</label>
<div class="col-sm-4 p-2">
<div class="col-sm-10 p-2">
<WordCounter
id="link-text"
class="ck-word-count"
/>
<input
id="link-text"
type="text"
@ -85,16 +170,35 @@
:value="menuNodeData.link_url"
@blur="($event) => saveNodeDataInput('link_url', ($event.target as HTMLInputElement).value)"
>
<small v-if="errors.link_url" v-for="(message) of errors.link_url" class="text-danger">"{{ message }}"</small>
<small class="form-text text-body-secondary">
{{ trans.methods.__('validation.between.string', {
attribute: "titre du lien",
min: "3",
max: "255",
}) + " " + trans.methods.__('Doît être une url valide.') }}
</small>
<template
v-for="(message, messageIndex) of errors.link_url"
:key="messageIndex"
>
<small
v-if="errors.link_url"
class="text-danger"
>"{{ message }}"</small>
</template>
</div>
<!-- LINK TITLE -->
<label
for="link-title-text"
class="col-sm-2 col-form-label p-2"
class="col-sm-2 col-form-label"
>
{{ trans.methods.__("Link title") }}
{{ trans.methods.__("Titre du lien") }}
</label>
<div class="col-sm-4 p-2">
<div class="col-sm-10 p-2">
<WordCounter
id="link-title-text"
class="ck-word-count"
/>
<input
id="link-title-text"
type="text"
@ -102,17 +206,32 @@
:value="menuNodeData.link_title"
@blur="($event) => saveNodeDataInput('link_title', ($event.target as HTMLInputElement).value)"
>
<small v-if="errors.link_title" v-for="(message) of errors.link_title" class="text-danger">"{{ message }}"</small>
<small class="form-text text-body-secondary">
{{ trans.methods.__('validation.between.string', {
attribute: "titre du lien",
min: "3",
max: "255",
}) }}
</small>
<template
v-for="(message, messageIndex) of errors.link_title"
:key="messageIndex"
>
<small
v-if="errors.link_title"
class="text-danger"
>"{{ message }}"</small>
</template>
</div>
<!-- LINK TARGET -->
<label
for="link-target-checkbox"
class="col-sm-2 col-form-label p-2"
class="col-sm-2 col-form-label"
>
{{ trans.methods.__("Link target") }}
{{ trans.methods.__("Cible du lien") }}
</label>
<div class="col-sm-4 p-2">
<div class="form-check form-switch">
<div class="form-check form-switch py-0">
<input
name="link_target"
class="form-check-input pt-3"
@ -122,18 +241,27 @@
:checked="menuNodeData.link_target"
@change="($event) => saveNodeDataInput('link_target', ($event.target as HTMLInputElement).checked)"
>
<small v-if="errors.link_target" v-for="(message) of errors.link_target" class="text-danger">"{{ message }}"</small>
<template
v-for="(message, messageIndex) of errors.link_target"
:key="messageIndex"
>
<small
v-if="errors.link_target"
class="text-danger"
>"{{ message }}"</small>
</template>
</div>
<small class="form-text text-body-secondary">{{ trans.methods.__('Ouverture dans un nouvel onglet. Deux réponses possibles : oui ou non.') }}</small>
</div>
<!-- LINK STATUS -->
<label
class="col-sm-2 col-form-label p-2"
class="col-sm-2 col-form-label"
for="link-status-checkbox"
>
{{ trans.methods.__("Link status") }}
{{ trans.methods.__("Statut du lien") }}
</label>
<div class="col-sm-4 p-2">
<div class="form-check form-switch">
<div class="form-check form-switch py-0">
<input
name="link_status"
class="form-check-input pt-3"
@ -143,45 +271,67 @@
:checked="menuNodeData.link_status"
@change="($event) => saveNodeDataInput('link_status', ($event.target as HTMLInputElement).checked)"
>
<small v-if="errors.link_status" v-for="(message) of errors.link_status" class="text-danger">"{{ message }}"</small>
<template
v-for="(message, messageIndex) of errors.link_status"
:key="messageIndex"
>
<small
v-if="errors.link_status"
class="text-danger"
>"{{ message }}"</small>
</template>
</div>
<small class="form-text text-body-secondary">{{ trans.methods.__('Visibilité du lien. Deux réponses possibles : visible ou caché.') }}</small>
</div>
</div>
<!-- --- -->
<hr class="col-8 mx-auto">
<!-- PICTURE -->
<div class="mb-3 row">
<!-- PICTURE -->
<label
for="picture-input"
class="col-sm-2 col-form-label p-2"
class="col-sm-2 col-form-label"
>
{{ trans.methods.__("Picture") }}
{{ trans.methods.__("Image") }}
</label>
<ImageInputComponent
id="picture-input"
:required="true"
:show-labels="true"
name="picture"
helper="picture"
:width="180"
:height="228"
:value="menuNodeData.picture"
:preview="false"
:browse-event="false"
@image-file="(file) => file ? saveNodeDataInput('picture', file) : null"
/>
<small v-if="errors.picture" v-for="(message) of errors.picture" class="text-danger">"{{ message }}"</small>
<div class="col-sm-10 p-2">
<ImageInputComponent
id="picture-input"
:required="true"
:show-labels="true"
name="picture"
:helper="trans.methods.__('crud.minicrud.dimensions_must_be', {
widthpx: '180px',
heightpx: '228px',
})"
:width="180"
:height="228"
:value="menuNodeData.picture"
:preview="false"
:browse-event="false"
@image-file="(file) => file ? saveNodeDataInput('picture', file) : null"
/>
</div>
<template
v-for="(message, messageIndex) of errors.picture"
:key="messageIndex"
>
<small
v-if="errors.picture"
class="text-danger"
>"{{ message }}"</small>
</template>
<!-- PICTURE ALT -->
<label
for="picture-alt-text"
class="col-sm-2 col-form-label p-2"
class="col-sm-2 col-form-label"
>
{{ trans.methods.__("Picture alt") }}
{{ trans.methods.__("Text alternatif de l'image") }}
</label>
<div class="col-sm-4 p-2">
<div class="col-sm-10 p-2">
<WordCounter
id="picture-alt-text"
class="ck-word-count"
/>
<input
id="picture-alt-text"
type="text"
@ -189,16 +339,29 @@
:value="menuNodeData.picture_alt"
@blur="($event) => saveNodeDataInput('picture_alt', ($event.target as HTMLInputElement).value)"
>
<small v-if="errors.picture_alt" v-for="(message) of errors.picture_alt" class="text-danger">"{{ message }}"</small>
<small class="form-text text-body-secondary">{{ trans.methods.__('crud.minicrud.picture_alt_helper') }}</small>
<template
v-for="(message, messageIndex) of errors.picture_alt"
:key="messageIndex"
>
<small
v-if="errors.picture_alt"
class="text-danger"
>"{{ message }}"</small>
</template>
</div>
<!-- PICTURE TITLE -->
<label
for="picture-title-text"
class="col-sm-2 col-form-label p-2"
class="col-sm-2 col-form-label"
>
{{ trans.methods.__("Picture title") }}
{{ trans.methods.__("Titre de l'image") }}
</label>
<div class="col-sm-4 p-2">
<div class="col-sm-10 p-2">
<WordCounter
id="picture-title-text"
class="ck-word-count"
/>
<input
id="picture-title-text"
type="text"
@ -206,17 +369,26 @@
:value="menuNodeData.picture_title"
@blur="($event) => saveNodeDataInput('picture_title', ($event.target as HTMLInputElement).value)"
>
<small v-if="errors.picture_title" v-for="(message) of errors.picture_title" class="text-danger">"{{ message }}"</small>
<small class="form-text text-body-secondary">{{ trans.methods.__('crud.minicrud.picture_title_helper') }}</small>
<template
v-for="(message, messageIndex) of errors.picture_title"
:key="messageIndex"
>
<small
v-if="errors.picture_title"
class="text-danger"
>"{{ message }}"</small>
</template>
</div>
<!-- PICTURE STATUS -->
<label
class="col-sm-2 col-form-label p-2"
class="col-sm-2 col-form-label"
for="picture-status-checkbox"
>
{{ trans.methods.__("Picture status") }}
{{ trans.methods.__("Statut de l'image") }}
</label>
<div class="col-sm-4 p-2">
<div class="form-check form-switch">
<div class="form-check form-switch py-0">
<input
name="picture_status"
class="form-check-input pt-3"
@ -226,45 +398,67 @@
:checked="menuNodeData.picture_status"
@change="($event) => saveNodeDataInput('picture_status', ($event.target as HTMLInputElement).checked)"
>
<small v-if="errors.picture_status" v-for="(message) of errors.picture_status" class="text-danger">"{{ message }}"</small>
<template
v-for="(message, messageIndex) of errors.picture_status"
:key="messageIndex"
>
<small
v-if="errors.picture_status"
class="text-danger"
>"{{ message }}"</small>
</template>
</div>
<small class="form-text text-body-secondary">{{ trans.methods.__('Visibilité de l\'image. Deux réponses possibles : visible ou cachée.') }}</small>
</div>
</div>
<!-- --- -->
<hr class="col-8 mx-auto">
<!-- BACKGROUND PICTURE-->
<div class="mb-3 row">
<label
for="background-picture-input"
class="col-sm-2 col-form-label p-2"
>
{{ trans.methods.__("Background Picture") }}
{{ trans.methods.__("Image de fond") }}
</label>
<!-- BACKGROUND PICTURE -->
<ImageInputComponent
id="background-picture-input"
:required="true"
:show-labels="true"
name="background_picture"
helper="background_picture"
:width="500"
:height="231"
:value="menuNodeData.background_picture"
:preview="false"
:browse-event="false"
@image-file="(file) => file ? saveNodeDataInput('background_picture', file) : null"
/>
<small v-if="errors.background_picture" v-for="(message) of errors.background_picture" class="text-danger">"{{ message }}"</small>
<div class="col-sm-10 p-2">
<ImageInputComponent
id="background-picture-input"
:required="true"
:show-labels="true"
name="background_picture"
:helper="trans.methods.__('crud.minicrud.dimensions_must_be', {
widthpx: '500px',
heightpx: '231px',
})"
:width="500"
:height="231"
:value="menuNodeData.background_picture"
:preview="false"
:browse-event="false"
@image-file="(file) => file ? saveNodeDataInput('background_picture', file) : null"
/>
</div>
<template
v-for="(message, messageIndex) of errors.background_picture"
:key="messageIndex"
>
<small
v-if="errors.background_picture"
class="text-danger"
>"{{ message }}"</small>
</template>
<!-- BACKGROUND PICTURE ALT -->
<label
for="background-picture-alt-text"
class="col-sm-2 col-form-label p-2"
>
{{ trans.methods.__("Background Picture alt") }}
{{ trans.methods.__("Text alternatif de l'image de fond") }}
</label>
<div class="col-sm-4 p-2">
<div class="col-sm-10 p-2">
<WordCounter
id="background-picture-alt-text"
class="ck-word-count"
/>
<input
id="background-picture-alt-text"
type="text"
@ -272,16 +466,29 @@
:value="menuNodeData.background_picture_alt"
@blur="($event) => saveNodeDataInput('background_picture_alt', ($event.target as HTMLInputElement).value)"
>
<small v-if="errors.background_picture_alt" v-for="(message) of errors.background_picture_alt" class="text-danger">"{{ message }}"</small>
<small class="form-text text-body-secondary">{{ trans.methods.__('crud.minicrud.picture_alt_helper') }}</small>
<template
v-for="(message, messageIndex) of errors.background_picture_alt"
:key="messageIndex"
>
<small
v-if="errors.background_picture_alt"
class="text-danger"
>"{{ message }}"</small>
</template>
</div>
<!-- BACKGROUND PICTURE TITLE -->
<label
for="background-picture-title-text"
class="col-sm-2 col-form-label p-2"
>
{{ trans.methods.__("Background picture title") }}
{{ trans.methods.__("Titre de l'image de fond") }}
</label>
<div class="col-sm-4 p-2">
<div class="col-sm-10 p-2">
<WordCounter
id="background-picture-title-text"
class="ck-word-count"
/>
<input
id="background-picture-title-text"
type="text"
@ -289,18 +496,29 @@
:value="menuNodeData.background_picture_title"
@blur="($event) => saveNodeDataInput('background_picture_title', ($event.target as HTMLInputElement).value)"
>
<small v-if="errors.background_picture_title" v-for="(message) of errors.background_picture_title" class="text-danger">"{{ message }}"</small>
<small class="form-text text-body-secondary">{{ trans.methods.__('crud.minicrud.picture_title_helper') }}</small>
<template
v-for="(message, messageIndex) of errors.background_picture_title"
:key="messageIndex"
>
<small
v-if="errors.background_picture_title"
class="text-danger"
>"{{ message }}"</small>
</template>
</div>
</div>
</fieldset>
</template>
<script lang="ts" setup>
import { defineOptions, onMounted, reactive, watch, type PropType } from "vue";
import { defineOptions, reactive, watch, type PropType } from "vue";
import type { IMenuNodeDataInputInterface } from "../../interfaces/IMenuNodeDataInputInterface";
import trans from "../../modules/trans";
import type { MenuNodeDataInterface } from "../../types/models/MenuNodeData";
import ImageInputComponent from "./ImageInputComponent.vue";
import ColorPickerComponent from "./ColorPickerComponent.vue";
import WordCounter from "./WordCounterComponent.vue";
defineOptions({
name: "MainMenuNodeDataEditor"
@ -311,9 +529,6 @@ const emits = defineEmits<{
updatedNodeData: [prop: string, value: ValueOf<IMenuNodeDataInputInterface> ],
}>();
// * ATTRIBUTES
// const attrs = useAttrs();
// * PROPS
const props = defineProps({
menuNodeData: {
@ -328,21 +543,14 @@ const props = defineProps({
// * DATA
let menuNodeData = reactive<MenuNodeDataInterface>(Object.assign({}, props.menuNodeData));
// const urlIsValid = ref<boolean>(true);
// * LIFECYCLE
onMounted(() => {});
// * WATCHERS
watch(props, () => {
menuNodeData = Object.assign({}, props.menuNodeData);
})
});
// * METHODS
function saveNodeDataInput(inputName: string, inputValue: ValueOf<IMenuNodeDataInputInterface>): void {
emits("updatedNodeData", inputName, inputValue);
}
</script>
<style lang="scss" scoped>
.main-menu-node-data-editor-component {
}
</style>

View file

@ -1,11 +1,15 @@
<template>
<!-- NODE PROPS EDITOR -->
<fieldset class="main-menu-node-editor-component container-fluid p-4 border mb-3">
<fieldset
ref="mainMenuNodeEditor"
class="main-menu-node-editor-component container-fluid mb-3"
>
<legend>
<span class="fs-5">{{ trans.methods.__("Modification d'un noeud") }}</span>
<button
class="btn btn-sm btn-dark float-end mt-2"
:title="trans.methods.__('Fermer la modification')"
data-bs-toggle="tooltip"
@click.prevent="closeEditor"
>
<FontAwesomeIcon
@ -13,17 +17,17 @@
/>
</button>
</legend>
<!-- NODE TYPE -->
<div class="mb-3 row">
<label
for="type"
for="type-node"
class="col-sm-2 col-form-label"
>
{{ trans.methods.__("Type de Noeud") }}
{{ trans.methods.__("Type de noeud") }}
</label>
<div class="col-sm-4">
<div class="col-sm-4 p-2">
<input
id="type-node"
type="text"
class="form-control"
:value="props.enum.label"
@ -31,27 +35,59 @@
>
</div>
</div>
<!-- Menu Name -->
<div
class="mb-3 row"
>
<!-- MENU PUBLISHED -->
<div class="mb-3 row">
<!-- LINK TARGET -->
<label
for="type"
class="col-sm-6 col-form-label"
for="published-checkbox"
class="col-sm-2 col-form-label"
>
{{ trans.methods.__("Titre du noeud principal affiché dans le menu") }}
{{ trans.methods.__("Visibilité du noeud") }}
</label>
<div class="col input-group">
<div class="col p-2">
<div class="form-check form-switch py-0">
<input
name="published"
class="form-check-input pt-3"
type="checkbox"
role="switch"
id="published-checkbox"
:checked="menuNode.published"
@blur="menuNode.published = ($event.target as HTMLInputElement).checked"
>
</div>
<small class="form-text text-body-secondary">{{ trans.methods.__('Deux réponses possibles : oui ou non (maximum 8 liens publiés).') }}</small>
</div>
</div>
<!-- MENU NAME -->
<div class="mb-3 row">
<label
for="title-node"
class="col-sm-2 col-form-label"
>
{{ trans.methods.__("Titre du noeud principal") }}
</label>
<div class="col-sm-10 p-2">
<WordCounter
id="title-node"
class="ck-word-count"
/>
<input
id="title-node"
type="text"
class="form-control"
:value="menuNode.title"
@blur="menuNode.title = ($event.target as HTMLInputElement).value"
>
<small class="form-text text-body-secondary">
{{ trans.methods.__('validation.between.string', {
attribute: "titre du noeud principal",
min: "3",
max: "255",
}) }}
</small>
</div>
</div>
<!-- ROUTE NAME -->
<div
v-if="props.enum.value !== MenuNodeTypeEnum.link"
@ -61,9 +97,9 @@
for="type"
class="col-sm-2 col-form-label"
>
{{ trans.methods.__("Route") }}
{{ trans.methods.__("Lien du noeud") }}
</label>
<div class="col input-group">
<div class="col input-group p-2">
<input
type="text"
class="form-control"
@ -89,14 +125,24 @@
>
{{ trans.methods.__("Url") }}
</label>
<div class="col input-group">
<input
ref="urlInput"
type="text"
class="form-control"
:value="String(menuNode.link.url)"
@blur="saveUrl(($event.target as HTMLInputElement | null)!.value)"
>
<div class="col p-2">
<div>
<WordCounter
id="node-url"
class="ck-word-count"
/>
<input
ref="urlInput"
id="node-url"
type="text"
class="form-control"
:value="String(menuNode.link.url)"
@blur="saveUrl(($event.target as HTMLInputElement | null)!.value)"
>
</div>
<small class="form-text text-body-secondary">
{{ trans.methods.__('Doît être une url valide.') }}
</small>
</div>
<small
v-if="!urlIsValid"
@ -114,6 +160,8 @@ import trans from "../../modules/trans";
import type { MenuNodeInterface } from "../../types/models/MenuNode";
import { MenuNodeTypeEnum } from "../enums";
import MainMenuNodeDataEditor from "./MainMenuNodeDataEditor.vue";
import WordCounter from "./WordCounterComponent.vue";
import { Tooltips } from "./../../modules/bsTooltips";
defineOptions({
name: "MainMenuNodeEditor",
@ -126,9 +174,6 @@ const emits = defineEmits<{
updatedNode: [menuNode: MenuNodeInterface],
}>();
// * ATTRIBUTES
// const attrs = useAttrs();
// * PROPS
const props = defineProps({
menuNode: {
@ -141,33 +186,52 @@ const props = defineProps({
}
});
// * REFS
const mainMenuNodeEditor = ref<HTMLInputElement|null>(null);
const urlInput = ref<HTMLInputElement|null>(null);
// * DATA
const menuNode = ref<MenuNodeInterface>(Object.assign({}, props.menuNode));
const urlIsValid = ref<boolean>(true);
const tooltips = ref<Tooltips|null>(null);
// * LIFECYCLE
onMounted(() => {
urlIsValid.value = isUrlValid(String(menuNode.value.link.url));
initTooltips();
});
// * WATCHERS
watch(menuNode, (newValue) => {
emits("updatedNode", newValue);
}, { deep: true });
// * REFS
const urlInput = ref<HTMLInputElement|null>(null);
// * METHODS
function saveUrl(newUrl: string): void {
urlIsValid.value = isUrlValid(newUrl);
menuNode.value.link.url = urlInput.value!.value;
}
function closeEditor(): void {
tooltips.value?.closeBootstrapTooltip();
emits("closeEditor");
}
function isUrlValid(url: string):boolean {
const regex = RegExp("(https?:\\/\\/)?((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|((\\d{1,3}\\.){3}\\d{1,3}))(\\:\\d+)?(\\/[-a-z\\d%_.~+@]*)*(\\?[;&a-z\\d%_.~+=-@]*)?(\\#[-a-z\\d_@]*)?$", "i");
return url.match(regex)?.[0] !== undefined;
}
/** Initialise all tooltips in the component */
function initTooltips(): void {
setTimeout(() => {
tooltips.value = Tooltips.make({
type: "dom",
elements: mainMenuNodeEditor.value!.querySelectorAll("[data-bs-toggle=\"tooltip\"]")
});
}, 500);
}
</script>
<style lang="scss" scoped>

View file

@ -4,6 +4,7 @@ import type { LinkInterface } from "resources/ts/types/models/Link";
export interface MenuNodeInterface {
type: MenuNodeTypeEnum,
published: boolean,
title: string,
link: LinkInterface
}

View file

@ -6,6 +6,7 @@ export interface MenuNodeDataInterface {
sub_label: string,
title: string,
text: string,
text_color: string,
link_url: string
link_title: string
link_target: boolean

View file

@ -1,30 +1,36 @@
@foreach ($menuNodeList as $index => $menuNode)
@switch($menuNode->type)
@case(\App\Enums\Settings\MenuNodeType::siteLogo)
@empty($noLogo)
<li class="nav-item menu-logo no-wrap mx-auto">
<img src="{{ Vite::asset('resources/assets/images/legaulois-logo.png') }}" alt="{{ config('app.name') }}" role="img">
</li>
@endempty
@break
@if ($menuNode->published)
@switch($menuNode->type)
@case(\App\Enums\Settings\MenuNodeType::siteLogo)
@empty($noLogo)
<li class="nav-item menu-logo no-wrap mx-auto">
<a href="{{ route('fo.home.show') }}" title="Accéder à la page d'accueil">
<img src="{{ Vite::asset('resources/assets/images/legaulois-logo.png') }}" alt="{{ config('app.name') }}"
title="{{ __(':appname, Volaille 100% Française', ['appname' => config('app.name')]) }}"
role="img">
</a>
</li>
@endempty
@break
@case(\App\Enums\Settings\MenuNodeType::link)
@empty($logoOnly)
<li class="nav-item">
@include('fo.layouts.main-menu.menu-nodes.generic.simple-link', [
'menuNode' => $menuNode,
])
</li>
@endempty
@break
@case(\App\Enums\Settings\MenuNodeType::link)
@empty($logoOnly)
<li class="nav-item">
@include('fo.layouts.main-menu.menu-nodes.generic.simple-link', [
'menuNode' => $menuNode,
])
</li>
@endempty
@break
@default
@empty($logoOnly)
<li class="nav-item">
@include("fo.layouts.main-menu.menu-nodes.{$menuNode->type->name()}", [
'menuNode' => $menuNode,
])
</li>
@endempty
@endswitch
@default
@empty($logoOnly)
<li class="nav-item">
@include("fo.layouts.main-menu.menu-nodes.{$menuNode->type->name()}", [
'menuNode' => $menuNode,
])
</li>
@endempty
@endswitch
@endif
@endforeach

View file

@ -31,6 +31,7 @@
@include('fo.layouts.main-menu.partials.collapse-right', [
'title' => str($menuNodeData->title)->upper(),
'text' => str($menuNodeData->text)->ucFirst(),
'textColor' => $menuNodeData->text_color,
'btnLabel' => str($menuNodeData->link_title)->upper(),
'btnLink' => $menuNodeData->link_url,
'btnLinkTarget' => $menuNodeData->link_target,

View file

@ -26,6 +26,7 @@
@include('fo.layouts.main-menu.partials.collapse-right', [
'title' => str($menuNodeData->title)->upper(),
'text' => str($menuNodeData->text)->ucFirst(),
'textColor' => $menuNodeData->text_color,
'btnLabel' => str($menuNodeData->link_title)->upper(),
'btnLink' => $menuNodeData->link_url,
'btnLinkTarget' => $menuNodeData->link_target,

View file

@ -26,6 +26,7 @@
@include('fo.layouts.main-menu.partials.collapse-right', [
'title' => str($menuNodeData->title)->upper(),
'text' => str($menuNodeData->text)->ucFirst(),
'textColor' => $menuNodeData->text_color,
'btnLabel' => str($menuNodeData->link_title)->upper(),
'btnLink' => $menuNodeData->link_url,
'btnLinkTarget' => $menuNodeData->link_target,

View file

@ -26,6 +26,7 @@
@include('fo.layouts.main-menu.partials.collapse-right', [
'title' => str($menuNodeData->title)->upper(),
'text' => str($menuNodeData->text)->ucFirst(),
'textColor' => $menuNodeData->text_color,
'btnLabel' => str($menuNodeData->link_title)->upper(),
'btnLink' => $menuNodeData->link_url,
'btnLinkTarget' => $menuNodeData->link_target,

View file

@ -26,6 +26,7 @@
@include('fo.layouts.main-menu.partials.collapse-right', [
'title' => str($menuNodeData->title)->upper(),
'text' => str($menuNodeData->text)->ucFirst(),
'textColor' => $menuNodeData->text_color,
'btnLabel' => str($menuNodeData->link_title)->upper(),
'btnLink' => $menuNodeData->link_url,
'btnLinkTarget' => $menuNodeData->link_target,

View file

@ -12,7 +12,7 @@
alt="{{ $imgAlt }}">
@endif
</div>
<div class="col-8 d-flex flex-column justify-content-center align-items-start">
<div class="col-8 d-flex flex-column justify-content-center align-items-start" style="color:{{ $textColor }};">
<p class="fs-4 f-bourton-base mb-1">{{ $title }}</p>
<p>{{ $text }}</p>
@if ($showLink)